Compare commits

..

3 Commits

Author SHA1 Message Date
ac84509b83 Merge branch 'develop' into 'master'
fix: old replication installation conflicts

See merge request slumber/multi-user!145
2021-07-20 14:28:04 +00:00
69565b3852 Merge branch 'develop' into 'master'
v0.4.0

See merge request slumber/multi-user!144
2021-07-20 13:41:29 +00:00
57fdd492ef Merge branch 'develop' into 'master'
fix: auto-updater operators registration to ensure blender 2.93 compatibility

See merge request slumber/multi-user!117
2021-04-15 13:39:47 +00:00
26 changed files with 552 additions and 1431 deletions

View File

@ -212,14 +212,14 @@ You can run the dedicated server on any platform by following these steps:
.. code-block:: bash .. code-block:: bash
replication.serve replication.server
.. hint:: .. hint::
You can also specify a custom **port** (-p), **timeout** (-t), **admin password** (-pwd), **log level (ERROR, WARNING, INFO or DEBUG)** (-l) and **log file** (-lf) with the following optional arguments You can also specify a custom **port** (-p), **timeout** (-t), **admin password** (-pwd), **log level (ERROR, WARNING, INFO or DEBUG)** (-l) and **log file** (-lf) with the following optional arguments
.. code-block:: bash .. code-block:: bash
replication.serve -p 5555 -pwd admin -t 5000 -l INFO -lf server.log replication.server -p 5555 -pwd admin -t 5000 -l INFO -lf server.log
Here, for example, a server is instantiated on port 5555, with password 'admin', a 5 second timeout, and logging enabled. Here, for example, a server is instantiated on port 5555, with password 'admin', a 5 second timeout, and logging enabled.
@ -562,7 +562,7 @@ The default Docker image essentially runs the equivalent of:
.. code-block:: bash .. code-block:: bash
replication.serve -pwd admin -p 5555 -t 5000 -l DEBUG -lf multiuser_server.log replication.server -pwd admin -p 5555 -t 5000 -l DEBUG -lf multiuser_server.log
This means the server will be launched with 'admin' as the administrator password, run on ports 5555:5558, use a timeout of 5 seconds, verbose 'DEBUG' log level, and with log files written to 'multiuser_server.log'. See :ref:`cmd-line` for a description of optional parameters. This means the server will be launched with 'admin' as the administrator password, run on ports 5555:5558, use a timeout of 5 seconds, verbose 'DEBUG' log level, and with log files written to 'multiuser_server.log'. See :ref:`cmd-line` for a description of optional parameters.
@ -572,7 +572,7 @@ For example, I would like to launch my server with a different administrator pas
.. code-block:: bash .. code-block:: bash
replication.serve -pwd supersecretpassword -p 5555 -t 3000 -l DEBUG -lf logname.log python3 -m replication.server -pwd supersecretpassword -p 5555 -t 3000 -l DEBUG -lf logname.log
Now, my configuration should look like this: Now, my configuration should look like this:
@ -691,7 +691,7 @@ We're finally ready to launch the server. Simply run:
.. code-block:: bash .. code-block:: bash
replication.serve -p 5555 -pwd admin -t 5000 -l INFO -lf server.log python3 -m replication.server -p 5555 -pwd admin -t 5000 -l INFO -lf server.log
See :ref:`cmd-line` for a description of optional parameters See :ref:`cmd-line` for a description of optional parameters

View File

@ -61,7 +61,6 @@ def register():
from . import operators from . import operators
from . import handlers from . import handlers
from . import ui from . import ui
from . import icons
from . import preferences from . import preferences
from . import addon_updater_ops from . import addon_updater_ops
@ -71,7 +70,6 @@ def register():
operators.register() operators.register()
handlers.register() handlers.register()
ui.register() ui.register()
icons.register()
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
raise Exception(module_error_msg) raise Exception(module_error_msg)
logging.error(module_error_msg) logging.error(module_error_msg)
@ -85,9 +83,7 @@ def register():
type=preferences.SessionUser type=preferences.SessionUser
) )
bpy.types.WindowManager.user_index = bpy.props.IntProperty() bpy.types.WindowManager.user_index = bpy.props.IntProperty()
bpy.types.WindowManager.server_index = bpy.props.IntProperty()
bpy.types.TOPBAR_MT_file_import.append(operators.menu_func_import) bpy.types.TOPBAR_MT_file_import.append(operators.menu_func_import)
bpy.types.TOPBAR_MT_file_export.append(operators.menu_func_export)
def unregister(): def unregister():
@ -95,17 +91,14 @@ def unregister():
from . import operators from . import operators
from . import handlers from . import handlers
from . import ui from . import ui
from . import icons
from . import preferences from . import preferences
from . import addon_updater_ops from . import addon_updater_ops
bpy.types.TOPBAR_MT_file_import.remove(operators.menu_func_import) bpy.types.TOPBAR_MT_file_import.remove(operators.menu_func_import)
bpy.types.TOPBAR_MT_file_export.remove(operators.menu_func_export)
presence.unregister() presence.unregister()
addon_updater_ops.unregister() addon_updater_ops.unregister()
ui.unregister() ui.unregister()
icons.unregister()
handlers.unregister() handlers.unregister()
operators.unregister() operators.unregister()
preferences.unregister() preferences.unregister()
@ -114,6 +107,5 @@ def unregister():
del bpy.types.ID.uuid del bpy.types.ID.uuid
del bpy.types.WindowManager.online_users del bpy.types.WindowManager.online_users
del bpy.types.WindowManager.user_index del bpy.types.WindowManager.user_index
del bpy.types.WindowManager.server_index
environment.unregister() environment.unregister()

View File

@ -43,7 +43,7 @@ __all__ = [
"bl_particle", "bl_particle",
] # Order here defines execution order ] # Order here defines execution order
if bpy.app.version >= (2,91,0): if bpy.app.version[1] >= 91:
__all__.append('bl_volume') __all__.append('bl_volume')
from . import * from . import *

View File

@ -53,12 +53,12 @@ STROKE = [
"uv_translation", "uv_translation",
"vertex_color_fill", "vertex_color_fill",
] ]
if bpy.app.version >= (2,91,0): if bpy.app.version[1] >= 91:
STROKE.append('use_cyclic') STROKE.append('use_cyclic')
else: else:
STROKE.append('draw_cyclic') STROKE.append('draw_cyclic')
if bpy.app.version >= (2,83,0): if bpy.app.version[1] >= 83:
STROKE_POINT.append('vertex_color') STROKE_POINT.append('vertex_color')
def dump_stroke(stroke): def dump_stroke(stroke):

View File

@ -37,7 +37,7 @@ class BlLightprobe(ReplicatedDatablock):
def construct(data: dict) -> object: def construct(data: dict) -> object:
type = 'CUBE' if data['type'] == 'CUBEMAP' else data['type'] type = 'CUBE' if data['type'] == 'CUBEMAP' else data['type']
# See https://developer.blender.org/D6396 # See https://developer.blender.org/D6396
if bpy.app.version >= (2,83,0): if bpy.app.version[1] >= 83:
return bpy.data.lightprobes.new(data["name"], type) return bpy.data.lightprobes.new(data["name"], type)
else: else:
logging.warning("Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396") logging.warning("Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
@ -49,7 +49,7 @@ class BlLightprobe(ReplicatedDatablock):
@staticmethod @staticmethod
def dump(datablock: object) -> dict: def dump(datablock: object) -> dict:
if bpy.app.version < (2,83,0): if bpy.app.version[1] < 83:
logging.warning("Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396") logging.warning("Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
dumper = Dumper() dumper = Dumper()

View File

@ -48,7 +48,7 @@ SHAPEKEY_BLOCK_ATTR = [
] ]
if bpy.app.version >= (2,93,0): if bpy.app.version[1] >= 93:
SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str, float) SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str, float)
else: else:
SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str) SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str)
@ -56,24 +56,14 @@ else:
blender 2.92.") blender 2.92.")
def get_node_group_properties_identifiers(node_group): def get_node_group_inputs(node_group):
props_ids = [] inputs = []
# Inputs
for inpt in node_group.inputs: for inpt in node_group.inputs:
if inpt.type in IGNORED_SOCKETS: if inpt.type in IGNORED_SOCKETS:
continue continue
else: else:
props_ids.append((inpt.identifier, inpt.type)) inputs.append(inpt)
return inputs
if inpt.type in ['INT', 'VALUE', 'BOOLEAN', 'RGBA', 'VECTOR']:
props_ids.append((f"{inpt.identifier}_attribute_name",'STR'))
props_ids.append((f"{inpt.identifier}_use_attribute", 'BOOL'))
for outpt in node_group.outputs:
if outpt.type not in IGNORED_SOCKETS and outpt.type in ['INT', 'VALUE', 'BOOLEAN', 'RGBA', 'VECTOR']:
props_ids.append((f"{outpt.identifier}_attribute_name", 'STR'))
return props_ids
# return [inpt.identifer for inpt in node_group.inputs if inpt.type not in IGNORED_SOCKETS] # return [inpt.identifer for inpt in node_group.inputs if inpt.type not in IGNORED_SOCKETS]
@ -132,35 +122,29 @@ def load_physics(dumped_settings: dict, target: bpy.types.Object):
bpy.ops.rigidbody.constraint_remove({"object": target}) bpy.ops.rigidbody.constraint_remove({"object": target})
def dump_modifier_geometry_node_props(modifier: bpy.types.Modifier) -> list: def dump_modifier_geometry_node_inputs(modifier: bpy.types.Modifier) -> list:
""" Dump geometry node modifier input properties """ Dump geometry node modifier input properties
:arg modifier: geometry node modifier to dump :arg modifier: geometry node modifier to dump
:type modifier: bpy.type.Modifier :type modifier: bpy.type.Modifier
""" """
dumped_props = [] dumped_inputs = []
for inpt in get_node_group_inputs(modifier.node_group):
input_value = modifier[inpt.identifier]
for prop_value, prop_type in get_node_group_properties_identifiers(modifier.node_group): dumped_input = None
try: if isinstance(input_value, bpy.types.ID):
prop_value = modifier[prop_value] dumped_input = input_value.uuid
except KeyError as e: elif isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS):
logging.error(f"fail to dump geomety node modifier property : {prop_value} ({e})") dumped_input = input_value
else: elif hasattr(input_value, 'to_list'):
dump = None dumped_input = input_value.to_list()
if isinstance(prop_value, bpy.types.ID): dumped_inputs.append(dumped_input)
dump = prop_value.uuid
elif isinstance(prop_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS):
dump = prop_value
elif hasattr(prop_value, 'to_list'):
dump = prop_value.to_list()
dumped_props.append((dump, prop_type)) return dumped_inputs
# logging.info(prop_value)
return dumped_props
def load_modifier_geometry_node_props(dumped_modifier: dict, target_modifier: bpy.types.Modifier): def load_modifier_geometry_node_inputs(dumped_modifier: dict, target_modifier: bpy.types.Modifier):
""" Load geometry node modifier inputs """ Load geometry node modifier inputs
:arg dumped_modifier: source dumped modifier to load :arg dumped_modifier: source dumped modifier to load
@ -169,17 +153,17 @@ def load_modifier_geometry_node_props(dumped_modifier: dict, target_modifier: bp
:type target_modifier: bpy.type.Modifier :type target_modifier: bpy.type.Modifier
""" """
for input_index, inpt in enumerate(get_node_group_properties_identifiers(target_modifier.node_group)): for input_index, inpt in enumerate(get_node_group_inputs(target_modifier.node_group)):
dumped_value, dumped_type = dumped_modifier['props'][input_index] dumped_value = dumped_modifier['inputs'][input_index]
input_value = target_modifier[inpt[0]] input_value = target_modifier[inpt.identifier]
if dumped_type in ['INT', 'VALUE', 'STR']: if isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS):
logging.info(f"{inpt[0]}/{dumped_value}") target_modifier[inpt.identifier] = dumped_value
target_modifier[inpt[0]] = dumped_value elif hasattr(input_value, 'to_list'):
elif dumped_type in ['RGBA', 'VECTOR']:
for index in range(len(input_value)): for index in range(len(input_value)):
input_value[index] = dumped_value[index] input_value[index] = dumped_value[index]
elif dumped_type in ['COLLECTION', 'OBJECT', 'IMAGE', 'TEXTURE', 'MATERIAL']: elif inpt.type in ['COLLECTION', 'OBJECT']:
target_modifier[inpt[0]] = get_datablock_from_uuid(dumped_value, None) target_modifier[inpt.identifier] = get_datablock_from_uuid(
dumped_value, None)
def load_pose(target_bone, data): def load_pose(target_bone, data):
@ -214,12 +198,12 @@ def find_data_from_name(name=None):
instance = bpy.data.speakers[name] instance = bpy.data.speakers[name]
elif name in bpy.data.lightprobes.keys(): elif name in bpy.data.lightprobes.keys():
# Only supported since 2.83 # Only supported since 2.83
if bpy.app.version >= (2,83,0): if bpy.app.version[1] >= 83:
instance = bpy.data.lightprobes[name] instance = bpy.data.lightprobes[name]
else: else:
logging.warning( logging.warning(
"Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396") "Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
elif bpy.app.version >= (2,91,0) and name in bpy.data.volumes.keys(): elif bpy.app.version[1] >= 91 and name in bpy.data.volumes.keys():
# Only supported since 2.91 # Only supported since 2.91
instance = bpy.data.volumes[name] instance = bpy.data.volumes[name]
return instance return instance
@ -266,11 +250,10 @@ def find_geometry_nodes_dependencies(modifiers: bpy.types.bpy_prop_collection) -
for mod in modifiers: for mod in modifiers:
if mod.type == 'NODES' and mod.node_group: if mod.type == 'NODES' and mod.node_group:
dependencies.append(mod.node_group) dependencies.append(mod.node_group)
for inpt, inpt_type in get_node_group_properties_identifiers(mod.node_group): # for inpt in get_node_group_inputs(mod.node_group):
inpt_value = mod.get(inpt) # parameter = mod.get(inpt.identifier)
# Avoid to handle 'COLLECTION', 'OBJECT' to avoid circular dependencies # if parameter and isinstance(parameter, bpy.types.ID):
if inpt_type in ['IMAGE', 'TEXTURE', 'MATERIAL'] and inpt_value: # dependencies.append(parameter)
dependencies.append(inpt_value)
return dependencies return dependencies
@ -404,7 +387,10 @@ def dump_modifiers(modifiers: bpy.types.bpy_prop_collection)->dict:
dumped_modifier = dumper.dump(modifier) dumped_modifier = dumper.dump(modifier)
# hack to dump geometry nodes inputs # hack to dump geometry nodes inputs
if modifier.type == 'NODES': if modifier.type == 'NODES':
dumped_modifier['props'] = dump_modifier_geometry_node_props(modifier) dumped_inputs = dump_modifier_geometry_node_inputs(
modifier)
dumped_modifier['inputs'] = dumped_inputs
elif modifier.type == 'PARTICLE_SYSTEM': elif modifier.type == 'PARTICLE_SYSTEM':
dumper.exclude_filter = [ dumper.exclude_filter = [
"is_edited", "is_edited",
@ -469,7 +455,7 @@ def load_modifiers(dumped_modifiers: list, modifiers: bpy.types.bpy_prop_collect
loader.load(loaded_modifier, dumped_modifier) loader.load(loaded_modifier, dumped_modifier)
if loaded_modifier.type == 'NODES': if loaded_modifier.type == 'NODES':
load_modifier_geometry_node_props(dumped_modifier, loaded_modifier) load_modifier_geometry_node_inputs(dumped_modifier, loaded_modifier)
elif loaded_modifier.type == 'PARTICLE_SYSTEM': elif loaded_modifier.type == 'PARTICLE_SYSTEM':
default = loaded_modifier.particle_system.settings default = loaded_modifier.particle_system.settings
dumped_particles = dumped_modifier['particle_system'] dumped_particles = dumped_modifier['particle_system']

View File

@ -440,7 +440,7 @@ class BlScene(ReplicatedDatablock):
if seq.name not in sequences: if seq.name not in sequences:
vse.sequences.remove(seq) vse.sequences.remove(seq)
# Load existing sequences # Load existing sequences
for seq_data in sequences.values(): for seq_data in sequences.value():
load_sequence(seq_data, vse) load_sequence(seq_data, vse)
# If the sequence is no longer used, clear it # If the sequence is no longer used, clear it
elif datablock.sequence_editor and not sequences: elif datablock.sequence_editor and not sequences:

View File

@ -134,7 +134,7 @@ def install_modules(dependencies: list, python_path: str, install_dir: str):
module_can_be_imported(package_name) module_can_be_imported(package_name)
def register(): def register():
if bpy.app.version >= (2,91,0): if bpy.app.version[1] >= 91:
python_binary_path = sys.executable python_binary_path = sys.executable
else: else:
python_binary_path = bpy.app.binary_path_python python_binary_path = bpy.app.binary_path_python

View File

@ -79,6 +79,8 @@ def on_scene_update(scene):
logging.debug(f"Ignoring distant update of {dependency_updates[0].id.name}") logging.debug(f"Ignoring distant update of {dependency_updates[0].id.name}")
return return
update_external_dependencies()
# NOTE: maybe we don't need to check each update but only the first # NOTE: maybe we don't need to check each update but only the first
for update in reversed(dependency_updates): for update in reversed(dependency_updates):
update_uuid = getattr(update.id, 'uuid', None) update_uuid = getattr(update.id, 'uuid', None)
@ -107,11 +109,6 @@ def on_scene_update(scene):
porcelain.commit(session.repository, scn_uuid) porcelain.commit(session.repository, scn_uuid)
porcelain.push(session.repository, 'origin', scn_uuid) porcelain.push(session.repository, 'origin', scn_uuid)
scene_graph_changed = [u for u in reversed(dependency_updates) if getattr(u.id, 'uuid', None) and isinstance(u.id,(bpy.types.Scene,bpy.types.Collection))]
if scene_graph_changed:
porcelain.purge_orphan_nodes(session.repository)
update_external_dependencies()
@persistent @persistent
def resolve_deps_graph(dummy): def resolve_deps_graph(dummy):
@ -130,29 +127,14 @@ def load_pre_handler(dummy):
if session and session.state in [STATE_ACTIVE, STATE_SYNCING]: if session and session.state in [STATE_ACTIVE, STATE_SYNCING]:
bpy.ops.session.stop() bpy.ops.session.stop()
@persistent @persistent
def update_client_frame(scene): def update_client_frame(scene):
setting = bpy.context.window_manager.session
if setting.replay_mode == 'TIMELINE' and \
setting.replay_files and \
scene.active_replay_file != setting.replay_frame_current :
index = bpy.context.scene.active_replay_file
bpy.ops.session.load(filepath=bpy.context.window_manager.session.replay_files[index].name,
draw_users=True,
replay=True)
setting.replay_frame_current = index
if session and session.state == STATE_ACTIVE: if session and session.state == STATE_ACTIVE:
porcelain.update_user_metadata(session.repository, { porcelain.update_user_metadata(session.repository, {
'frame_current': scene.frame_current 'frame_current': scene.frame_current
}) })
@persistent
def post_frame_update(scene):
if bpy.context.window_manager.session.replay_mode == 'TIMELINE' and \
not bpy.context.scene.animation_data:
bpy.context.scene.animation_data_create()
bpy.context.scene.animation_data.action = bpy.data.actions.get('replay_action')
def register(): def register():
bpy.app.handlers.undo_post.append(resolve_deps_graph) bpy.app.handlers.undo_post.append(resolve_deps_graph)

View File

@ -1,45 +0,0 @@
# ##### 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 <https://www.gnu.org/licenses/>.
#
# ##### END GPL LICENSE BLOCK #####
import bpy
import os
from pathlib import Path
import bpy.utils.previews
def register():
global icons_col
pcoll = bpy.utils.previews.new()
icons_dir = os.path.join(os.path.dirname(__file__), ".")
for png in Path(icons_dir).rglob("*.png"):
pcoll.load(png.stem, str(png), "IMAGE")
icons_col = pcoll
def unregister():
global icons_col
try:
bpy.utils.previews.remove(icons_col)
except Exception:
pass
icons_col = None

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -20,7 +20,6 @@ import asyncio
import copy import copy
import gzip import gzip
import logging import logging
from multi_user.preferences import ServerPreset
import os import os
import queue import queue
import random import random
@ -29,16 +28,13 @@ import string
import sys import sys
import time import time
import traceback import traceback
from uuid import uuid4
from datetime import datetime from datetime import datetime
from operator import itemgetter from operator import itemgetter
from pathlib import Path from pathlib import Path
from queue import Queue from queue import Queue
from time import gmtime, strftime from time import gmtime, strftime
from numpy import interp
from bpy.props import FloatProperty from bpy.props import FloatProperty
import bmesh
try: try:
import _pickle as pickle import _pickle as pickle
@ -60,164 +56,13 @@ from replication.repository import Repository
from . import bl_types, environment, shared_data, timers, ui, utils from . import bl_types, environment, shared_data, timers, ui, utils
from .handlers import on_scene_update, sanitize_deps_graph from .handlers import on_scene_update, sanitize_deps_graph
from .presence import SessionStatusWidget, renderer, view3d_find, refresh_sidebar_view, bbox_from_obj from .presence import SessionStatusWidget, renderer, view3d_find
from .timers import registry from .timers import registry
background_execution_queue = Queue() background_execution_queue = Queue()
deleyables = [] deleyables = []
stop_modal_executor = False stop_modal_executor = False
CLEARED_DATABLOCKS = ['actions', 'armatures', 'cache_files', 'cameras',
'collections', 'curves', 'fonts',
'grease_pencils', 'images', 'lattices', 'libraries',
'lightprobes', 'lights', 'linestyles', 'masks',
'materials', 'meshes', 'metaballs', 'movieclips',
'node_groups', 'objects', 'paint_curves', 'particles',
'scenes', 'shape_keys', 'sounds', 'speakers', 'texts',
'textures', 'volumes', 'worlds']
PERSISTENT_DATABLOCKS = ['LineStyle', 'Dots Stroke', 'replay_action']
def clean_scene(ignored_datablocks: list = None):
"""
Delete all datablock of the scene except PERSISTENT_DATABLOCKS and ignored
ones in ignored_datablocks.
"""
PERSISTENT_DATABLOCKS.extend(ignored_datablocks)
# Avoid to trigger a runtime error by keeping the last scene
PERSISTENT_DATABLOCKS.append(bpy.data.scenes[0].name)
for type_name in CLEARED_DATABLOCKS:
type_collection = getattr(bpy.data, type_name)
for datablock in type_collection:
if datablock.name in PERSISTENT_DATABLOCKS:
logging.debug(f"Skipping {datablock.name}")
continue
else:
logging.debug(f"Removing {datablock.name}")
type_collection.remove(datablock)
# Clear sequencer
bpy.context.scene.sequence_editor_clear()
def draw_user(username, metadata, radius=0.01, intensity=10.0):
"""
Generate a mesh representation of a given user frustum and
sight of view.
"""
view_corners = metadata.get('view_corners')
color = metadata.get('color', (1,1,1,0))
objects = metadata.get('selected_objects', None)
scene = metadata.get('scene_current', bpy.context.scene.name)
user_collection = bpy.data.collections.new(username)
# User Color
user_mat = bpy.data.materials.new(username)
user_mat.use_nodes = True
nodes = user_mat.node_tree.nodes
nodes.remove(nodes['Principled BSDF'])
emission_node = nodes.new('ShaderNodeEmission')
emission_node.inputs['Color'].default_value = color
emission_node.inputs['Strength'].default_value = intensity
output_node = nodes['Material Output']
user_mat.node_tree.links.new(
emission_node.outputs['Emission'], output_node.inputs['Surface'])
# Generate camera mesh
camera_vertices = view_corners[:4]
camera_vertices.append(view_corners[6])
camera_mesh = bpy.data.meshes.new(f"{username}_camera")
camera_obj = bpy.data.objects.new(f"{username}_camera", camera_mesh)
frustum_bm = bmesh.new()
frustum_bm.from_mesh(camera_mesh)
for p in camera_vertices:
frustum_bm.verts.new(p)
frustum_bm.verts.ensure_lookup_table()
frustum_bm.edges.new((frustum_bm.verts[0], frustum_bm.verts[2]))
frustum_bm.edges.new((frustum_bm.verts[2], frustum_bm.verts[1]))
frustum_bm.edges.new((frustum_bm.verts[1], frustum_bm.verts[3]))
frustum_bm.edges.new((frustum_bm.verts[3], frustum_bm.verts[0]))
frustum_bm.edges.new((frustum_bm.verts[0], frustum_bm.verts[4]))
frustum_bm.edges.new((frustum_bm.verts[2], frustum_bm.verts[4]))
frustum_bm.edges.new((frustum_bm.verts[1], frustum_bm.verts[4]))
frustum_bm.edges.new((frustum_bm.verts[3], frustum_bm.verts[4]))
frustum_bm.edges.ensure_lookup_table()
frustum_bm.to_mesh(camera_mesh)
frustum_bm.free() # free and prevent further access
camera_obj.modifiers.new("wireframe", "SKIN")
camera_obj.data.skin_vertices[0].data[0].use_root = True
for v in camera_obj.data.skin_vertices[0].data:
v.radius = [radius, radius]
camera_mesh.materials.append(user_mat)
user_collection.objects.link(camera_obj)
# Generate sight mesh
sight_mesh = bpy.data.meshes.new(f"{username}_sight")
sight_obj = bpy.data.objects.new(f"{username}_sight", sight_mesh)
sight_verts = view_corners[4:6]
sight_bm = bmesh.new()
sight_bm.from_mesh(sight_mesh)
for p in sight_verts:
sight_bm.verts.new(p)
sight_bm.verts.ensure_lookup_table()
sight_bm.edges.new((sight_bm.verts[0], sight_bm.verts[1]))
sight_bm.edges.ensure_lookup_table()
sight_bm.to_mesh(sight_mesh)
sight_bm.free()
sight_obj.modifiers.new("wireframe", "SKIN")
sight_obj.data.skin_vertices[0].data[0].use_root = True
for v in sight_mesh.skin_vertices[0].data:
v.radius = [radius, radius]
sight_mesh.materials.append(user_mat)
user_collection.objects.link(sight_obj)
# Draw selected objects
if objects:
for o in list(objects):
instance = bl_types.bl_datablock.get_datablock_from_uuid(o, None)
if instance:
bbox_mesh = bpy.data.meshes.new(f"{instance.name}_bbox")
bbox_obj = bpy.data.objects.new(
f"{instance.name}_bbox", bbox_mesh)
bbox_verts, bbox_ind = bbox_from_obj(instance, index=0)
bbox_bm = bmesh.new()
bbox_bm.from_mesh(bbox_mesh)
for p in bbox_verts:
bbox_bm.verts.new(p)
bbox_bm.verts.ensure_lookup_table()
for e in bbox_ind:
bbox_bm.edges.new(
(bbox_bm.verts[e[0]], bbox_bm.verts[e[1]]))
bbox_bm.to_mesh(bbox_mesh)
bbox_bm.free()
bpy.data.collections[username].objects.link(bbox_obj)
bbox_obj.modifiers.new("wireframe", "SKIN")
bbox_obj.data.skin_vertices[0].data[0].use_root = True
for v in bbox_mesh.skin_vertices[0].data:
v.radius = [radius, radius]
bbox_mesh.materials.append(user_mat)
bpy.data.scenes[scene].collection.children.link(user_collection)
def session_callback(name): def session_callback(name):
""" Session callback wrapper """ Session callback wrapper
@ -236,6 +81,7 @@ def session_callback(name):
def initialize_session(): def initialize_session():
"""Session connection init hander """Session connection init hander
""" """
settings = utils.get_preferences()
runtime_settings = bpy.context.window_manager.session runtime_settings = bpy.context.window_manager.session
if not runtime_settings.is_host: if not runtime_settings.is_host:
@ -262,6 +108,7 @@ def initialize_session():
for d in deleyables: for d in deleyables:
d.register() d.register()
# Step 5: Clearing history # Step 5: Clearing history
utils.flush_history() utils.flush_history()
@ -295,12 +142,32 @@ def on_connection_end(reason="none"):
if isinstance(handler, logging.FileHandler): if isinstance(handler, logging.FileHandler):
logger.removeHandler(handler) logger.removeHandler(handler)
if reason != "user": if reason != "user":
bpy.ops.session.notify('INVOKE_DEFAULT', message=f"Disconnected from session. Reason: {reason}. ") #TODO: change op session.notify to add ui + change reason (in replication->interface) bpy.ops.session.notify('INVOKE_DEFAULT', message=f"Disconnected from session. Reason: {reason}. ")
# OPERATORS
class SessionStartOperator(bpy.types.Operator):
bl_idname = "session.start"
bl_label = "start"
bl_description = "connect to a net server"
host: bpy.props.BoolProperty(default=False)
@classmethod
def poll(cls, context):
return True
def execute(self, context):
global deleyables
def setup_logging():
""" Session setup logging (host/connect)
"""
settings = utils.get_preferences() settings = utils.get_preferences()
runtime_settings = context.window_manager.session
users = bpy.data.window_managers['WinMan'].online_users
admin_pass = settings.password
users.clear()
deleyables.clear()
logger = logging.getLogger() logger = logging.getLogger()
if len(logger.handlers) == 1: if len(logger.handlers) == 1:
formatter = logging.Formatter( formatter = logging.Formatter(
@ -324,10 +191,82 @@ def setup_logging():
handler.setFormatter(formatter) handler.setFormatter(formatter)
def setup_timer(): bpy_protocol = bl_types.get_data_translation_protocol()
""" Session setup timer (host/connect)
""" # Check if supported_datablocks are up to date before starting the
settings = utils.get_preferences() # the session
for dcc_type_id in bpy_protocol.implementations.keys():
if dcc_type_id not in settings.supported_datablocks:
logging.info(f"{dcc_type_id} not found, \
regenerate type settings...")
settings.generate_supported_types()
if bpy.app.version[1] >= 91:
python_binary_path = sys.executable
else:
python_binary_path = bpy.app.binary_path_python
repo = Repository(
rdp=bpy_protocol,
username=settings.username)
# Host a session
if self.host:
if settings.init_method == 'EMPTY':
utils.clean_scene()
runtime_settings.is_host = True
runtime_settings.internet_ip = environment.get_ip()
try:
# Init repository
for scene in bpy.data.scenes:
porcelain.add(repo, scene)
porcelain.remote_add(
repo,
'origin',
'127.0.0.1',
settings.port,
admin_password=admin_pass)
session.host(
repository= repo,
remote='origin',
timeout=settings.connection_timeout,
password=admin_pass,
cache_directory=settings.cache_directory,
server_log_level=logging.getLevelName(
logging.getLogger().level),
)
except Exception as e:
self.report({'ERROR'}, repr(e))
logging.error(f"Error: {e}")
traceback.print_exc()
# Join a session
else:
if not runtime_settings.admin:
utils.clean_scene()
# regular session, no password needed
admin_pass = None
try:
porcelain.remote_add(
repo,
'origin',
settings.ip,
settings.port,
admin_password=admin_pass)
session.connect(
repository= repo,
timeout=settings.connection_timeout,
password=admin_pass
)
except Exception as e:
self.report({'ERROR'}, str(e))
logging.error(str(e))
# Background client updates service
deleyables.append(timers.ClientUpdate()) deleyables.append(timers.ClientUpdate())
deleyables.append(timers.DynamicRightSelectTimer()) deleyables.append(timers.DynamicRightSelectTimer())
deleyables.append(timers.ApplyTimer(timeout=settings.depsgraph_update_rate)) deleyables.append(timers.ApplyTimer(timeout=settings.depsgraph_update_rate))
@ -348,163 +287,6 @@ def setup_timer():
deleyables.append(session_listen) deleyables.append(session_listen)
deleyables.append(timers.AnnotationUpdates()) deleyables.append(timers.AnnotationUpdates())
def get_active_server_preset(context):
active_index = context.window_manager.server_index
server_presets = utils.get_preferences().server_preset
active_index = active_index if active_index <= len(server_presets)-1 else 0
return server_presets[active_index]
# OPERATORS
class SessionConnectOperator(bpy.types.Operator):
bl_idname = "session.connect"
bl_label = "connect"
bl_description = "connect to a net server"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
global deleyables
settings = utils.get_preferences()
users = bpy.data.window_managers['WinMan'].online_users
active_server = get_active_server_preset(context)
admin_pass = active_server.admin_password if active_server.use_admin_password else None
server_pass = active_server.server_password if active_server.use_server_password else ''
users.clear()
deleyables.clear()
setup_logging()
bpy_protocol = bl_types.get_data_translation_protocol()
# Check if supported_datablocks are up to date before starting the
# the session
for dcc_type_id in bpy_protocol.implementations.keys():
if dcc_type_id not in settings.supported_datablocks:
logging.info(f"{dcc_type_id} not found, \
regenerate type settings...")
settings.generate_supported_types()
if bpy.app.version >= (2,91,0):
python_binary_path = sys.executable
else:
python_binary_path = bpy.app.binary_path_python
repo = Repository(
rdp=bpy_protocol,
username=settings.username)
# Join a session
if not active_server.use_admin_password:
clean_scene()
try:
porcelain.remote_add(
repo,
'origin',
active_server.ip,
active_server.port,
server_password=server_pass,
admin_password=admin_pass)
session.connect(
repository= repo,
timeout=settings.connection_timeout,
server_password=server_pass,
admin_password=admin_pass
)
except Exception as e:
self.report({'ERROR'}, str(e))
logging.error(str(e))
# Background client updates service
setup_timer()
return {"FINISHED"}
class SessionHostOperator(bpy.types.Operator):
bl_idname = "session.host"
bl_label = "host"
bl_description = "host server"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
global deleyables
settings = utils.get_preferences()
runtime_settings = context.window_manager.session
users = bpy.data.window_managers['WinMan'].online_users
admin_pass = settings.host_admin_password if settings.host_use_admin_password else None
server_pass = settings.host_server_password if settings.host_use_server_password else ''
users.clear()
deleyables.clear()
setup_logging()
bpy_protocol = bl_types.get_data_translation_protocol()
# Check if supported_datablocks are up to date before starting the
# the session
for dcc_type_id in bpy_protocol.implementations.keys():
if dcc_type_id not in settings.supported_datablocks:
logging.info(f"{dcc_type_id} not found, \
regenerate type settings...")
settings.generate_supported_types()
if bpy.app.version >= (2,91,0):
python_binary_path = sys.executable
else:
python_binary_path = bpy.app.binary_path_python
repo = Repository(
rdp=bpy_protocol,
username=settings.username)
# Host a session
if settings.init_method == 'EMPTY':
clean_scene()
try:
# Init repository
for scene in bpy.data.scenes:
porcelain.add(repo, scene)
porcelain.remote_add(
repo,
'origin',
'127.0.0.1',
settings.host_port,
server_password=server_pass,
admin_password=admin_pass)
session.host(
repository= repo,
remote='origin',
timeout=settings.connection_timeout,
server_password=server_pass,
admin_password=admin_pass,
cache_directory=settings.cache_directory,
server_log_level=logging.getLevelName(
logging.getLogger().level),
)
except Exception as e:
self.report({'ERROR'}, repr(e))
logging.error(f"Error: {e}")
traceback.print_exc()
# Background client updates service
setup_timer()
return {"FINISHED"} return {"FINISHED"}
@ -537,7 +319,7 @@ class SessionInitOperator(bpy.types.Operator):
def execute(self, context): def execute(self, context):
if self.init_method == 'EMPTY': if self.init_method == 'EMPTY':
clean_scene() utils.clean_scene()
for scene in bpy.data.scenes: for scene in bpy.data.scenes:
porcelain.add(session.repository, scene) porcelain.add(session.repository, scene)
@ -907,6 +689,7 @@ class SessionPurgeOperator(bpy.types.Operator):
def execute(self, context): def execute(self, context):
try: try:
sanitize_deps_graph(remove_nodes=True)
porcelain.purge_orphan_nodes(session.repository) porcelain.purge_orphan_nodes(session.repository)
except Exception as e: except Exception as e:
self.report({'ERROR'}, repr(e)) self.report({'ERROR'}, repr(e))
@ -1001,6 +784,7 @@ class SessionStopAutoSaveOperator(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper): class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
bl_idname = "session.load" bl_idname = "session.load"
bl_label = "Load session save" bl_label = "Load session save"
@ -1016,89 +800,14 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
maxlen=255, # Max internal buffer length, longer would be clamped. maxlen=255, # Max internal buffer length, longer would be clamped.
) )
draw_users: bpy.props.BoolProperty(
name="Load users",
description="Draw users in the scene",
default=False,
)
replay: bpy.props.BoolProperty(
name="Replay mode",
description="Enable replay functions",
default=False,
)
user_skin_radius: bpy.props.FloatProperty(
name="Wireframe radius",
description="Wireframe radius",
default=0.01,
)
user_color_intensity: bpy.props.FloatProperty(
name="Shading intensity",
description="Shading intensity",
default=1.0,
)
files: bpy.props.CollectionProperty(
name='File paths',
type=bpy.types.OperatorFileListElement
)
def draw(self, context):
pass
def execute(self, context): def execute(self, context):
from replication.repository import Repository from replication.repository import Repository
runtime_settings = context.window_manager.session
# init the factory with supported types # init the factory with supported types
bpy_protocol = bl_types.get_data_translation_protocol() bpy_protocol = bl_types.get_data_translation_protocol()
repo = Repository(bpy_protocol) repo = Repository(bpy_protocol)
try:
repo.loads(self.filepath) repo.loads(self.filepath)
except TypeError: utils.clean_scene()
# Load legacy snapshots
db = pickle.load(gzip.open(self.filepath, "rb"))
nodes = db.get("nodes")
logging.info(f"Loading legacy {len(nodes)} node")
repo.object_store.clear()
for node, node_data in nodes:
instance = Node(
uuid=node,
data=node_data.get('data'),
owner=node_data.get('owner'),
dependencies=node_data.get('dependencies'),
state=FETCHED)
# Patch data for compatibility
type_id = node_data.get('str_type')[2:]
if type_id == "File":
type_id = "WindowsPath"
instance.data['type_id'] = type_id
repo.do_commit(instance)
instance.state = FETCHED
# Persitstent collection
ignored_datablocks = []
persistent_collection = bpy.data.collections.get("multiuser_timelapse")
if self.replay and \
runtime_settings.replay_persistent_collection and \
persistent_collection:
collection_repo = Repository(
rdp=bpy_protocol,
username="None")
porcelain.add(collection_repo, persistent_collection)
porcelain.commit(collection_repo, persistent_collection.uuid)
for node in collection_repo.graph.values():
ignored_datablocks.append(node.data.get('name'))
clean_scene(ignored_datablocks=ignored_datablocks)
nodes = [repo.graph.get(n) for n in repo.index_sorted] nodes = [repo.graph.get(n) for n in repo.index_sorted]
@ -1113,69 +822,6 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
for node in nodes: for node in nodes:
porcelain.apply(repo, node.uuid) porcelain.apply(repo, node.uuid)
if len(self.files) > 1:
runtime_settings.replay_files.clear()
context.scene.active_replay_file = len(self.files)-1
directory = Path(self.filepath).parent
file_list = [f['name'] for f in self.files]
file_list.sort()
for f in file_list:
snap = runtime_settings.replay_files.add()
snap.name = str(Path(directory, f))
print(f)
if runtime_settings.replay_mode == 'TIMELINE':
replay_action = bpy.data.actions.get('replay_action', bpy.data.actions.new('replay_action'))
bpy.context.scene.animation_data_create()
bpy.context.scene.animation_data.action = replay_action
if len(replay_action.fcurves) > 0 and replay_action.fcurves[0].data_path == 'active_replay_file':
replay_fcurve = replay_action.fcurves[0]
else:
replay_fcurve = replay_action.fcurves.new('active_replay_file')
for p in reversed(replay_fcurve.keyframe_points):
replay_fcurve.keyframe_points.remove(p, fast=True)
duration = runtime_settings.replay_duration
file_count = len(self.files)-1
for index in range(0, file_count):
frame = interp(index, [0, file_count], [bpy.context.scene.frame_start, duration])
replay_fcurve.keyframe_points.insert(frame, index)
if self.draw_users:
f = gzip.open(self.filepath, "rb")
db = pickle.load(f)
users = db.get("users")
for username, user_data in users.items():
metadata = user_data['metadata']
if metadata:
draw_user(username, metadata, radius=self.user_skin_radius, intensity=self.user_color_intensity)
# Relink the persistent collection
if self.replay and persistent_collection:
logging.info(f"Relinking {persistent_collection.name}")
bpy.context.scene.collection.children.link(persistent_collection)
# Reasign scene action
if self.replay and \
runtime_settings.replay_mode == 'TIMELINE' and \
not bpy.context.scene.animation_data :
bpy.context.scene.animation_data_create()
bpy.context.scene.animation_data.action = bpy.data.actions.get('replay_action')
bpy.context.scene.frame_end = runtime_settings.replay_duration
# Reasign the scene camera
if self.replay and \
runtime_settings.replay_persistent_collection and \
runtime_settings.replay_camera:
bpy.context.scene.camera = runtime_settings.replay_camera
return {'FINISHED'} return {'FINISHED'}
@ -1183,169 +829,54 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
def poll(cls, context): def poll(cls, context):
return True return True
class SESSION_PT_ImportUser(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "Users"
bl_parent_id = "FILE_PT_operator"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
sfile = context.space_data
operator = sfile.active_operator
return operator.bl_idname == "SESSION_OT_load"
def draw_header(self, context):
sfile = context.space_data
operator = sfile.active_operator
self.layout.prop(operator, "draw_users", text="")
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
sfile = context.space_data
operator = sfile.active_operator
layout.enabled = operator.draw_users
layout.prop(operator, "user_skin_radius")
layout.prop(operator, "user_color_intensity")
class SessionPresetServerAdd(bpy.types.Operator): class SessionPresetServerAdd(bpy.types.Operator):
"""Add a server to the server list preset""" """Add a server to the server list preset"""
bl_idname = "session.preset_server_add" bl_idname = "session.preset_server_add"
bl_label = "Add server preset" bl_label = "add server preset"
bl_description = "add a server to the server preset list" bl_description = "add the current server to the server preset list"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
server_name: bpy.props.StringProperty(default="") name : bpy.props.StringProperty(default="server_preset")
ip: bpy.props.StringProperty(default="127.0.0.1")
port: bpy.props.IntProperty(default=5555)
use_server_password: bpy.props.BoolProperty(default=False)
server_password: bpy.props.StringProperty(default="", subtype = "PASSWORD")
use_admin_password: bpy.props.BoolProperty(default=False)
admin_password: bpy.props.StringProperty(default="", subtype = "PASSWORD")
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return True return True
def invoke(self, context, event): def invoke(self, context, event):
self.server_name = ""
self.ip = "127.0.0.1"
self.port = 5555
self.use_server_password = False
self.server_password = ""
self.use_admin_password = False
self.admin_password = ""
assert(context) assert(context)
return context.window_manager.invoke_props_dialog(self) return context.window_manager.invoke_props_dialog(self)
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
row = layout.row() col = layout.column()
row.prop(self, "server_name", text="Server name") settings = utils.get_preferences()
row = layout.row(align = True)
row.prop(self, "ip", text="IP+port") col.prop(settings, "server_name", text="server name")
row.prop(self, "port", text="")
row = layout.row()
col = row.column()
col.prop(self, "use_server_password", text="Server password:")
col = row.column()
col.enabled = True if self.use_server_password else False
col.prop(self, "server_password", text="")
row = layout.row()
col = row.column()
col.prop(self, "use_admin_password", text="Admin password:")
col = row.column()
col.enabled = True if self.use_admin_password else False
col.prop(self, "admin_password", text="")
def execute(self, context): def execute(self, context):
assert(context) assert(context)
settings = utils.get_preferences() settings = utils.get_preferences()
existing_preset = settings.get_server_preset(self.server_name)
existing_preset = settings.server_preset.get(settings.server_name)
new_server = existing_preset if existing_preset else settings.server_preset.add() new_server = existing_preset if existing_preset else settings.server_preset.add()
new_server.name = str(uuid4()) new_server.name = settings.server_name
new_server.server_name = self.server_name new_server.server_ip = settings.ip
new_server.ip = self.ip new_server.server_port = settings.port
new_server.port = self.port new_server.server_password = settings.password
new_server.use_server_password = self.use_server_password
new_server.server_password = self.server_password
new_server.use_admin_password = self.use_admin_password
new_server.admin_password = self.admin_password
refresh_sidebar_view() settings.server_preset_interface = settings.server_name
if new_server == existing_preset : if new_server == existing_preset :
self.report({'INFO'}, "Server '" + self.server_name + "' edited") self.report({'INFO'}, "Server '" + settings.server_name + "' override")
else : else :
self.report({'INFO'}, "New '" + self.server_name + "' server preset") self.report({'INFO'}, "New '" + settings.server_name + "' server preset")
return {'FINISHED'} return {'FINISHED'}
class SessionPresetServerEdit(bpy.types.Operator): # TODO : use preset, not settings
"""Edit a server to the server list preset"""
bl_idname = "session.preset_server_edit"
bl_label = "Edit server preset"
bl_description = "Edit a server from the server preset list"
bl_options = {"REGISTER"}
target_server_name: bpy.props.StringProperty(default="None")
@classmethod
def poll(cls, context):
return True
def invoke(self, context, event):
assert(context)
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
settings = utils.get_preferences()
settings_active_server = settings.server_preset.get(self.target_server_name)
row = layout.row()
row.prop(settings_active_server, "server_name", text="Server name")
row = layout.row(align = True)
row.prop(settings_active_server, "ip", text="IP+port")
row.prop(settings_active_server, "port", text="")
row = layout.row()
col = row.column()
col.prop(settings_active_server, "use_server_password", text="Server password:")
col = row.column()
col.enabled = True if settings_active_server.use_server_password else False
col.prop(settings_active_server, "server_password", text="")
row = layout.row()
col = row.column()
col.prop(settings_active_server, "use_admin_password", text="Admin password:")
col = row.column()
col.enabled = True if settings_active_server.use_admin_password else False
col.prop(settings_active_server, "admin_password", text="")
def execute(self, context):
assert(context)
settings = utils.get_preferences()
settings_active_server = settings.server_preset.get(self.target_server_name)
refresh_sidebar_view()
self.report({'INFO'}, "Server '" + settings_active_server.server_name + "' edited")
return {'FINISHED'}
class SessionPresetServerRemove(bpy.types.Operator): class SessionPresetServerRemove(bpy.types.Operator):
"""Remove a server to the server list preset""" """Remove a server to the server list preset"""
bl_idname = "session.preset_server_remove" bl_idname = "session.preset_server_remove"
@ -1353,8 +884,6 @@ class SessionPresetServerRemove(bpy.types.Operator):
bl_description = "remove the current server from the server preset list" bl_description = "remove the current server from the server preset list"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
target_server_name: bpy.props.StringProperty(default="None")
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return True return True
@ -1363,97 +892,19 @@ class SessionPresetServerRemove(bpy.types.Operator):
assert(context) assert(context)
settings = utils.get_preferences() settings = utils.get_preferences()
settings.server_preset.remove(settings.server_preset.find(self.target_server_name))
settings.server_preset.remove(settings.server_preset.find(settings.server_preset_interface))
return {'FINISHED'} return {'FINISHED'}
class RefreshServerStatus(bpy.types.Operator):
bl_idname = "session.get_info"
bl_label = "Get session info"
bl_description = "Get session info"
target_server: bpy.props.StringProperty(default="127.0.0.1:5555")
@classmethod
def poll(cls, context):
return (session.state != STATE_ACTIVE)
def execute(self, context):
settings = utils.get_preferences()
for server in settings.server_preset:
infos = porcelain.request_session_info(f"{server.ip}:{server.port}", timeout=settings.ping_timeout)
server.is_online = True if infos else False
if server.is_online:
server.is_private = infos.get("private")
return {'FINISHED'}
class GetDoc(bpy.types.Operator):
"""Get the documentation of the addon"""
bl_idname = "doc.get"
bl_label = "Multi-user's doc"
bl_description = "Go to the doc of the addon"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
assert(context)
bpy.ops.wm.url_open(url="https://slumber.gitlab.io/multi-user/index.html")
return {'FINISHED'}
class FirstLaunch(bpy.types.Operator):
"""First time lauching the addon"""
bl_idname = "firstlaunch.verify"
bl_label = "First launch"
bl_description = "First time lauching the addon"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
assert(context)
settings = utils.get_preferences()
settings.is_first_launch = False
settings.server_preset.clear()
prefs = bpy.context.preferences.addons[__package__].preferences
prefs.generate_default_presets()
return {'FINISHED'}
def menu_func_import(self, context): def menu_func_import(self, context):
self.layout.operator(SessionLoadSaveOperator.bl_idname, text='Multi-user session snapshot (.db)') self.layout.operator(SessionLoadSaveOperator.bl_idname, text='Multi-user session snapshot (.db)')
def menu_func_export(self, context):
self.layout.operator(SessionSaveBackupOperator.bl_idname, text='Multi-user session snapshot (.db)')
class SessionRenderReplay(bpy.types.Operator):
bl_idname = "session.render_replay"
bl_label = "Render Replay"
bl_description = "Render Replay"
@classmethod
def poll(cls, context):
return context.window_manager.session.replay_files
def execute(self, context):
base_path = str(context.scene.render.filepath)
for frame in range(0,context.scene.frame_end):
logging.info(f"Rendering frame {frame} to {base_path}_{frame}.png")
context.scene.frame_current = frame
filename = Path(bpy.context.window_manager.session.replay_files[context.scene.active_replay_file].name)
context.scene.render.filepath = f"{base_path}{frame}_{filename.stem}"
bpy.ops.render.render(write_still=True)
context.scene.render.filepath = base_path
return {'FINISHED'}
classes = ( classes = (
SessionConnectOperator, SessionStartOperator,
SessionHostOperator,
SessionStopOperator, SessionStopOperator,
SessionPropertyRemoveOperator, SessionPropertyRemoveOperator,
SessionSnapUserOperator, SessionSnapUserOperator,
@ -1467,16 +918,10 @@ classes = (
SessionNotifyOperator, SessionNotifyOperator,
SessionSaveBackupOperator, SessionSaveBackupOperator,
SessionLoadSaveOperator, SessionLoadSaveOperator,
SESSION_PT_ImportUser,
SessionStopAutoSaveOperator, SessionStopAutoSaveOperator,
SessionPurgeOperator, SessionPurgeOperator,
SessionPresetServerAdd, SessionPresetServerAdd,
SessionPresetServerEdit,
SessionPresetServerRemove, SessionPresetServerRemove,
RefreshServerStatus,
GetDoc,
FirstLaunch,
SessionRenderReplay,
) )

View File

@ -17,17 +17,15 @@
import random import random
import logging import logging
from uuid import uuid4
import bpy import bpy
import string import string
import re import re
import os import os
from numpy import interp
from pathlib import Path from pathlib import Path
from . import bl_types, environment, addon_updater_ops, presence, ui from . import bl_types, environment, addon_updater_ops, presence, ui
from .utils import get_preferences, get_expanded_icon, get_folder_size from .utils import get_preferences, get_expanded_icon
from replication.constants import RP_COMMON from replication.constants import RP_COMMON
from replication.interface import session from replication.interface import session
@ -35,21 +33,15 @@ from replication.interface import session
IP_REGEX = re.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$") IP_REGEX = re.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")
HOSTNAME_REGEX = re.compile("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$") HOSTNAME_REGEX = re.compile("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$")
#SERVER PRESETS AT LAUNCH
DEFAULT_PRESETS = { DEFAULT_PRESETS = {
"localhost" : { "localhost" : {
"server_name": "localhost", "server_ip": "localhost",
"ip": "localhost", "server_port": 5555,
"port": 5555, "server_password": "admin"
"use_admin_password": True,
"admin_password": "admin",
"server_password": ""
}, },
"public session" : { "public session" : {
"server_name": "public session", "server_ip": "51.75.71.183",
"ip": "51.75.71.183", "server_port": 5555,
"port": 5555,
"admin_password": "",
"server_password": "" "server_password": ""
}, },
} }
@ -86,6 +78,11 @@ def update_ip(self, context):
logging.error("Wrong IP format") logging.error("Wrong IP format")
self['ip'] = "127.0.0.1" self['ip'] = "127.0.0.1"
def update_server_preset_interface(self, context):
self.server_name = self.server_preset.get(self.server_preset_interface).name
self.ip = self.server_preset.get(self.server_preset_interface).server_ip
self.port = self.server_preset.get(self.server_preset_interface).server_port
self.password = self.server_preset.get(self.server_preset_interface).server_password
def update_directory(self, context): def update_directory(self, context):
new_dir = Path(self.cache_directory) new_dir = Path(self.cache_directory)
@ -100,77 +97,6 @@ def update_directory(self, context):
def set_log_level(self, value): def set_log_level(self, value):
logging.getLogger().setLevel(value) logging.getLogger().setLevel(value)
def set_active_replay(self, value):
files_count = len(bpy.context.window_manager.session.replay_files)
if files_count == 0:
return
max_index = files_count-1
if value > max_index:
value = max_index
if hasattr(self, 'active_replay_file'):
self["active_replay_file"] = value
else:
self.active_replay_file = value
if bpy.context.window_manager.session.replay_mode == 'MANUAL':
bpy.ops.session.load(
filepath=bpy.context.window_manager.session.replay_files[value].name,
draw_users=True,
replay=True)
def get_active_replay(self):
return self.get('active_replay_file', 0)
def set_replay_persistent_collection(self, value):
if hasattr(self, 'replay_persistent_collection'):
self["replay_persistent_collection"] = value
else:
self.replay_persistent_collection = value
collection = bpy.data.collections.get("multiuser_timelapse", None)
if collection is None and value:
collection = bpy.data.collections.new('multiuser_timelapse')
bpy.context.scene.collection.children.link(collection)
elif collection and not value:
for o in collection.objects:
bpy.data.objects.remove(o)
bpy.data.collections.remove(collection)
def get_replay_persistent_collection(self):
return self.get('replay_persistent_collection', False)
def set_replay_duration(self, value):
if hasattr(self, 'replay_duration'):
self["replay_duration"] = value
else:
self.replay_duration = value
# Update the animation fcurve
replay_action = bpy.data.actions.get('replay_action')
replay_fcurve = None
for fcurve in replay_action.fcurves:
if fcurve.data_path == 'active_replay_file':
replay_fcurve = fcurve
if replay_fcurve:
for p in reversed(replay_fcurve.keyframe_points):
replay_fcurve.keyframe_points.remove(p, fast=True)
bpy.context.scene.frame_end = value
files_count = len(bpy.context.window_manager.session.replay_files)-1
for index in range(0, files_count):
frame = interp(index,[0, files_count],[bpy.context.scene.frame_start, value])
replay_fcurve.keyframe_points.insert(frame, index)
def get_replay_duration(self):
return self.get('replay_duration', 10)
def get_log_level(self): def get_log_level(self):
return logging.getLogger().level return logging.getLogger().level
@ -184,15 +110,9 @@ class ReplicatedDatablock(bpy.types.PropertyGroup):
icon: bpy.props.StringProperty() icon: bpy.props.StringProperty()
class ServerPreset(bpy.types.PropertyGroup): class ServerPreset(bpy.types.PropertyGroup):
server_name: bpy.props.StringProperty(default="") server_ip: bpy.props.StringProperty()
ip: bpy.props.StringProperty(default="127.0.0.1", update=update_ip) server_port: bpy.props.IntProperty(default=5555)
port: bpy.props.IntProperty(default=5555) server_password: bpy.props.StringProperty(default="admin", subtype = "PASSWORD")
use_server_password: bpy.props.BoolProperty(default=False)
server_password: bpy.props.StringProperty(default="", subtype = "PASSWORD")
use_admin_password: bpy.props.BoolProperty(default=False)
admin_password: bpy.props.StringProperty(default="", subtype = "PASSWORD")
is_online: bpy.props.BoolProperty(default=False)
is_private: bpy.props.BoolProperty(default=False)
def set_sync_render_settings(self, value): def set_sync_render_settings(self, value):
self['sync_render_settings'] = value self['sync_render_settings'] = value
@ -242,60 +162,35 @@ class ReplicationFlags(bpy.types.PropertyGroup):
class SessionPrefs(bpy.types.AddonPreferences): class SessionPrefs(bpy.types.AddonPreferences):
bl_idname = __package__ bl_idname = __package__
# User settings ip: bpy.props.StringProperty(
name="ip",
description='Distant host ip',
default="localhost",
update=update_ip)
username: bpy.props.StringProperty( username: bpy.props.StringProperty(
name="Username", name="Username",
default=f"user_{random_string_digits()}" default=f"user_{random_string_digits()}"
) )
client_color: bpy.props.FloatVectorProperty( client_color: bpy.props.FloatVectorProperty(
name="client_instance_color", name="client_instance_color",
description='User color',
subtype='COLOR', subtype='COLOR',
default=randomColor() default=randomColor())
port: bpy.props.IntProperty(
name="port",
description='Distant host port',
default=5555
) )
# Current server settings
server_name: bpy.props.StringProperty( server_name: bpy.props.StringProperty(
name="server_name", name="server_name",
description="Custom name of the server", description="Custom name of the server",
default='localhost', default='localhost',
) )
server_index: bpy.props.IntProperty( password: bpy.props.StringProperty(
name="server_index", name="password",
description="index of the server", default=random_string_digits(),
)
# User host session settings
host_port: bpy.props.IntProperty(
name="host_port",
description='Distant host port',
default=5555
)
host_use_server_password: bpy.props.BoolProperty(
name="use_server_password",
description='Use session password',
default=False
)
host_server_password: bpy.props.StringProperty(
name="server_password",
description='Session password', description='Session password',
subtype='PASSWORD' subtype='PASSWORD'
) )
host_use_admin_password: bpy.props.BoolProperty(
name="use_admin_password",
description='Use admin password',
default=True
)
host_admin_password: bpy.props.StringProperty(
name="admin_password",
description='Admin password',
subtype='PASSWORD',
default='admin'
)
# Other
is_first_launch: bpy.props.BoolProperty(
name="is_fnirst_launch",
description="First time lauching the addon",
default=True
)
sync_flags: bpy.props.PointerProperty( sync_flags: bpy.props.PointerProperty(
type=ReplicationFlags type=ReplicationFlags
) )
@ -319,11 +214,6 @@ class SessionPrefs(bpy.types.AddonPreferences):
description='connection timeout before disconnection', description='connection timeout before disconnection',
default=5000 default=5000
) )
ping_timeout: bpy.props.IntProperty(
name='ping timeout',
description='check if servers are online',
default=500
)
# Replication update settings # Replication update settings
depsgraph_update_rate: bpy.props.FloatProperty( depsgraph_update_rate: bpy.props.FloatProperty(
name='depsgraph update rate (s)', name='depsgraph update rate (s)',
@ -335,12 +225,11 @@ class SessionPrefs(bpy.types.AddonPreferences):
description="Remove filecache from memory", description="Remove filecache from memory",
default=False default=False
) )
# For UI # for UI
category: bpy.props.EnumProperty( category: bpy.props.EnumProperty(
name="Category", name="Category",
description="Preferences Category", description="Preferences Category",
items=[ items=[
('PREF', "Preferences", "Preferences of this add-on"),
('CONFIG', "Configuration", "Configuration of this add-on"), ('CONFIG', "Configuration", "Configuration of this add-on"),
('UPDATE', "Update", "Update this add-on"), ('UPDATE', "Update", "Update this add-on"),
], ],
@ -384,58 +273,38 @@ class SessionPrefs(bpy.types.AddonPreferences):
step=1, step=1,
subtype='PERCENTAGE', subtype='PERCENTAGE',
) )
presence_text_distance: bpy.props.FloatProperty( presence_mode_distance: bpy.props.FloatProperty(
name="Distance text visibilty", name="Distance mode visibilty",
description="Adjust the distance visibilty of user's mode/name", description="Adjust the distance visibilty of user's mode",
min=0.1, min=0.1,
max=10000, max=1000,
default=100, default=100,
) )
conf_session_identity_expanded: bpy.props.BoolProperty( conf_session_identity_expanded: bpy.props.BoolProperty(
name="Identity", name="Identity",
description="Identity", description="Identity",
default=False default=True
) )
conf_session_net_expanded: bpy.props.BoolProperty( conf_session_net_expanded: bpy.props.BoolProperty(
name="Net", name="Net",
description="net", description="net",
default=False default=True
) )
conf_session_hosting_expanded: bpy.props.BoolProperty( conf_session_hosting_expanded: bpy.props.BoolProperty(
name="Rights", name="Rights",
description="Rights", description="Rights",
default=False default=False
) )
conf_session_rep_expanded: bpy.props.BoolProperty(
name="Replication",
description="Replication",
default=False
)
conf_session_cache_expanded: bpy.props.BoolProperty( conf_session_cache_expanded: bpy.props.BoolProperty(
name="Cache", name="Cache",
description="cache", description="cache",
default=False default=False
) )
conf_session_log_expanded: bpy.props.BoolProperty(
name="conf_session_log_expanded",
description="conf_session_log_expanded",
default=False
)
conf_session_ui_expanded: bpy.props.BoolProperty( conf_session_ui_expanded: bpy.props.BoolProperty(
name="Interface", name="Interface",
description="Interface", description="Interface",
default=False default=False
) )
sidebar_repository_shown: bpy.props.BoolProperty(
name="sidebar_repository_shown",
description="sidebar_repository_shown",
default=False
)
sidebar_advanced_shown: bpy.props.BoolProperty(
name="sidebar_advanced_shown",
description="sidebar_advanced_shown",
default=False
)
sidebar_advanced_rep_expanded: bpy.props.BoolProperty( sidebar_advanced_rep_expanded: bpy.props.BoolProperty(
name="sidebar_advanced_rep_expanded", name="sidebar_advanced_rep_expanded",
description="sidebar_advanced_rep_expanded", description="sidebar_advanced_rep_expanded",
@ -446,11 +315,6 @@ class SessionPrefs(bpy.types.AddonPreferences):
description="sidebar_advanced_log_expanded", description="sidebar_advanced_log_expanded",
default=False default=False
) )
sidebar_advanced_uinfo_expanded: bpy.props.BoolProperty(
name="sidebar_advanced_uinfo_expanded",
description="sidebar_advanced_uinfo_expanded",
default=False
)
sidebar_advanced_net_expanded: bpy.props.BoolProperty( sidebar_advanced_net_expanded: bpy.props.BoolProperty(
name="sidebar_advanced_net_expanded", name="sidebar_advanced_net_expanded",
description="sidebar_advanced_net_expanded", description="sidebar_advanced_net_expanded",
@ -507,6 +371,12 @@ class SessionPrefs(bpy.types.AddonPreferences):
name="server preset", name="server preset",
type=ServerPreset, type=ServerPreset,
) )
server_preset_interface: bpy.props.EnumProperty(
name="servers",
description="servers enum",
items=server_list_callback,
update=update_server_preset_interface,
)
# Custom panel # Custom panel
panel_category: bpy.props.StringProperty( panel_category: bpy.props.StringProperty(
@ -516,28 +386,38 @@ class SessionPrefs(bpy.types.AddonPreferences):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.row().prop(self, "category", expand=True) layout.row().prop(self, "category", expand=True)
if self.category == 'PREF':
grid = layout.column()
box = grid.box()
row = box.row()
# USER SETTINGS
split = row.split(factor=0.7, align=True)
split.prop(self, "username", text="User")
split.prop(self, "client_color", text="")
row = box.row()
row.label(text="Hide settings:")
row = box.row()
row.prop(self, "sidebar_advanced_shown", text="Hide “Advanced” settings in side pannel (Not in session)")
row = box.row()
row.prop(self, "sidebar_repository_shown", text="Hide “Repository” settings in side pannel (In session)")
if self.category == 'CONFIG': if self.category == 'CONFIG':
grid = layout.column() grid = layout.column()
# USER INFORMATIONS
box = grid.box()
box.prop(
self, "conf_session_identity_expanded", text="User information",
icon=get_expanded_icon(self.conf_session_identity_expanded),
emboss=False)
if self.conf_session_identity_expanded:
box.row().prop(self, "username", text="name")
box.row().prop(self, "client_color", text="color")
# NETWORK SETTINGS
box = grid.box()
box.prop(
self, "conf_session_net_expanded", text="Networking",
icon=get_expanded_icon(self.conf_session_net_expanded),
emboss=False)
if self.conf_session_net_expanded:
box.row().prop(self, "ip", text="Address")
row = box.row()
row.label(text="Port:")
row.prop(self, "port", text="")
row = box.row()
row.label(text="Init the session from:")
row.prop(self, "init_method", text="")
# HOST SETTINGS # HOST SETTINGS
box = grid.box() box = grid.box()
box.prop( box.prop(
@ -545,57 +425,9 @@ class SessionPrefs(bpy.types.AddonPreferences):
icon=get_expanded_icon(self.conf_session_hosting_expanded), icon=get_expanded_icon(self.conf_session_hosting_expanded),
emboss=False) emboss=False)
if self.conf_session_hosting_expanded: if self.conf_session_hosting_expanded:
row = box.row()
row.prop(self, "host_port", text="Port: ")
row = box.row() row = box.row()
row.label(text="Init the session from:") row.label(text="Init the session from:")
row.prop(self, "init_method", text="") row.prop(self, "init_method", text="")
row = box.row()
col = row.column()
col.prop(self, "host_use_server_password", text="Server password:")
col = row.column()
col.enabled = True if self.host_use_server_password else False
col.prop(self, "host_server_password", text="")
row = box.row()
col = row.column()
col.prop(self, "host_use_admin_password", text="Admin password:")
col = row.column()
col.enabled = True if self.host_use_admin_password else False
col.prop(self, "host_admin_password", text="")
# NETWORKING
box = grid.box()
box.prop(
self, "conf_session_net_expanded", text="Network",
icon=get_expanded_icon(self.conf_session_net_expanded),
emboss=False)
if self.conf_session_net_expanded:
row = box.row()
row.label(text="Timeout (ms):")
row.prop(self, "connection_timeout", text="")
row = box.row()
row.label(text="Server ping (ms):")
row.prop(self, "ping_timeout", text="")
# REPLICATION
box = grid.box()
box.prop(
self, "conf_session_rep_expanded", text="Replication",
icon=get_expanded_icon(self.conf_session_rep_expanded),
emboss=False)
if self.conf_session_rep_expanded:
row = box.row()
row.prop(self.sync_flags, "sync_render_settings")
row = box.row()
row.prop(self.sync_flags, "sync_active_camera")
row = box.row()
row.prop(self.sync_flags, "sync_during_editmode")
row = box.row()
if self.sync_flags.sync_during_editmode:
warning = row.box()
warning.label(text="Don't use this with heavy meshes !", icon='ERROR')
row = box.row()
row.prop(self, "depsgraph_update_rate", text="Apply delay")
# CACHE SETTINGS # CACHE SETTINGS
box = grid.box() box = grid.box()
@ -606,18 +438,25 @@ class SessionPrefs(bpy.types.AddonPreferences):
if self.conf_session_cache_expanded: if self.conf_session_cache_expanded:
box.row().prop(self, "cache_directory", text="Cache directory") box.row().prop(self, "cache_directory", text="Cache directory")
box.row().prop(self, "clear_memory_filecache", text="Clear memory filecache") box.row().prop(self, "clear_memory_filecache", text="Clear memory filecache")
box.row().operator('session.clear_cache', text=f"Clear cache ({get_folder_size(self.cache_directory)})")
# LOGGING # INTERFACE SETTINGS
box = grid.box() box = grid.box()
box.prop( box.prop(
self, "conf_session_log_expanded", text="Logging", self, "conf_session_ui_expanded", text="Interface",
icon=get_expanded_icon(self.conf_session_log_expanded), icon=get_expanded_icon(self.conf_session_ui_expanded),
emboss=False) emboss=False)
if self.conf_session_log_expanded: if self.conf_session_ui_expanded:
box.row().prop(self, "panel_category", text="Panel category", expand=True)
row = box.row() row = box.row()
row.label(text="Log level:") row.label(text="Session widget:")
row.prop(self, 'logging_level', text="")
col = box.column(align=True)
col.prop(self, "presence_hud_scale", expand=True)
col.prop(self, "presence_hud_hpos", expand=True)
col.prop(self, "presence_hud_vpos", expand=True)
col.prop(self, "presence_mode_distance", expand=True)
if self.category == 'UPDATE': if self.category == 'UPDATE':
from . import addon_updater_ops from . import addon_updater_ops
@ -638,31 +477,18 @@ class SessionPrefs(bpy.types.AddonPreferences):
new_db.icon = impl.bl_icon new_db.icon = impl.bl_icon
new_db.bl_name = impl.bl_id new_db.bl_name = impl.bl_id
# Get a server preset through its name
def get_server_preset(self, name):
existing_preset = None
for server_preset in self.server_preset : # custom at launch server preset
if server_preset.server_name == name :
existing_preset = server_preset
return existing_preset
# Custom at launch server preset
def generate_default_presets(self): def generate_default_presets(self):
for preset_name, preset_data in DEFAULT_PRESETS.items(): for preset_name, preset_data in DEFAULT_PRESETS.items():
existing_preset = self.get_server_preset(preset_name) existing_preset = self.server_preset.get(preset_name)
if existing_preset : if existing_preset :
continue continue
new_server = self.server_preset.add() new_server = self.server_preset.add()
new_server.name = str(uuid4()) new_server.name = preset_name
new_server.server_name = preset_data.get('server_name') new_server.server_ip = preset_data.get('server_ip')
new_server.ip = preset_data.get('ip') new_server.server_port = preset_data.get('server_port')
new_server.port = preset_data.get('port')
new_server.use_server_password = preset_data.get('use_server_password',False)
new_server.server_password = preset_data.get('server_password',None) new_server.server_password = preset_data.get('server_password',None)
new_server.use_admin_password = preset_data.get('use_admin_password',False)
new_server.admin_password = preset_data.get('admin_password',None)
def client_list_callback(scene, context): def client_list_callback(scene, context):
@ -750,6 +576,11 @@ class SessionProps(bpy.types.PropertyGroup):
description='Connect as admin', description='Connect as admin',
default=False default=False
) )
internet_ip: bpy.props.StringProperty(
name="internet ip",
default="no found",
description='Internet interface ip',
)
user_snap_running: bpy.props.BoolProperty( user_snap_running: bpy.props.BoolProperty(
default=False default=False
) )
@ -759,37 +590,6 @@ class SessionProps(bpy.types.PropertyGroup):
is_host: bpy.props.BoolProperty( is_host: bpy.props.BoolProperty(
default=False default=False
) )
replay_files: bpy.props.CollectionProperty(
name='File paths',
type=bpy.types.OperatorFileListElement
)
replay_persistent_collection: bpy.props.BoolProperty(
name="replay_persistent_collection",
description='Enable a collection that persist accross frames loading',
get=get_replay_persistent_collection,
set=set_replay_persistent_collection,
)
replay_mode: bpy.props.EnumProperty(
name='replay method',
description='Replay in keyframe (timeline) or manually',
items={
('TIMELINE', 'TIMELINE', 'Replay from the timeline.'),
('MANUAL', 'MANUAL', 'Replay manually, from the replay frame widget.')},
default='TIMELINE')
replay_duration: bpy.props.IntProperty(
name='replay interval',
default=250,
min=10,
set=set_replay_duration,
get=get_replay_duration,
)
replay_frame_current: bpy.props.IntProperty(
name='replay_frame_current',
)
replay_camera: bpy.props.PointerProperty(
name='Replay camera',
type=bpy.types.Object
)
classes = ( classes = (
@ -816,16 +616,6 @@ def register():
# at launch server presets # at launch server presets
prefs.generate_default_presets() prefs.generate_default_presets()
bpy.types.Scene.active_replay_file = bpy.props.IntProperty(
name="active_replay_file",
default=0,
min=0,
description='Active snapshot',
set=set_active_replay,
get=get_active_replay,
options={'ANIMATABLE'}
)
def unregister(): def unregister():
@ -833,5 +623,3 @@ def unregister():
for cls in reversed(classes): for cls in reversed(classes):
unregister_class(cls) unregister_class(cls)
del bpy.types.Scene.active_replay_file

View File

@ -203,7 +203,6 @@ class DynamicRightSelectTimer(Timer):
for node_id in to_lock: for node_id in to_lock:
node = session.repository.graph.get(node_id) node = session.repository.graph.get(node_id)
if node and hasattr(node,'data'):
instance_mode = node.data.get('instance_type') instance_mode = node.data.get('instance_type')
if instance_mode and instance_mode == 'COLLECTION': if instance_mode and instance_mode == 'COLLECTION':
to_lock.remove(node_id) to_lock.remove(node_id)

View File

@ -16,9 +16,7 @@
# ##### END GPL LICENSE BLOCK ##### # ##### END GPL LICENSE BLOCK #####
from logging import log
import bpy import bpy
import bpy.utils.previews
from .utils import get_preferences, get_expanded_icon, get_folder_size, get_state_str from .utils import get_preferences, get_expanded_icon, get_folder_size, get_state_str
from replication.constants import (ADDED, ERROR, FETCHED, from replication.constants import (ADDED, ERROR, FETCHED,
@ -73,180 +71,162 @@ class SESSION_PT_settings(bpy.types.Panel):
def draw_header(self, context): def draw_header(self, context):
layout = self.layout layout = self.layout
settings = get_preferences()
from multi_user import icons
offline_icon = icons.icons_col["session_status_offline"]
waiting_icon = icons.icons_col["session_status_waiting"]
online_icon = icons.icons_col["session_status_online"]
if session and session.state != STATE_INITIAL: if session and session.state != STATE_INITIAL:
cli_state = session.state cli_state = session.state
state = session.state state = session.state
connection_icon = offline_icon connection_icon = "KEYTYPE_MOVING_HOLD_VEC"
if state == STATE_ACTIVE: if state == STATE_ACTIVE:
connection_icon = online_icon connection_icon = 'PROP_ON'
else: else:
connection_icon = waiting_icon connection_icon = 'PROP_CON'
layout.label(text=f"{str(settings.server_name)} - {get_state_str(cli_state)}", icon_value=connection_icon.icon_id) layout.label(text=f"Session - {get_state_str(cli_state)}", icon=connection_icon)
else: else:
layout.label(text=f"Multi-user - v{__version__}", icon="ANTIALIASED") layout.label(text=f"Session - v{__version__}",icon="PROP_OFF")
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
row = layout.row()
runtime_settings = context.window_manager.session runtime_settings = context.window_manager.session
settings = get_preferences() settings = get_preferences()
if settings.is_first_launch:
# USER SETTINGS
row = layout.row()
row.label(text="1. Enter your username and color:")
row = layout.row()
split = row.split(factor=0.7, align=True)
split.prop(settings, "username", text="")
split.prop(settings, "client_color", text="")
# DOC
row = layout.row()
row.label(text="2. New here ? See the doc:")
row = layout.row()
row.operator("doc.get", text="Documentation", icon="HELP")
# START
row = layout.row()
row.label(text="3: Start the Multi-user:")
row = layout.row()
row.scale_y = 2
row.operator("firstlaunch.verify", text="Continue")
if not settings.is_first_launch:
if hasattr(context.window_manager, 'session'): if hasattr(context.window_manager, 'session'):
# STATE INITIAL # STATE INITIAL
if not session \ if not session \
or (session and session.state == STATE_INITIAL): or (session and session.state == STATE_INITIAL):
layout = self.layout pass
settings = get_preferences()
server_preset = settings.server_preset
selected_server = context.window_manager.server_index if context.window_manager.server_index<=len(server_preset)-1 else 0
active_server_name = server_preset[selected_server].name if len(server_preset)>=1 else ""
is_server_selected = True if active_server_name else False
# SERVER LIST
row = layout.row()
box = row.box()
box.scale_y = 0.7
split = box.split(factor=0.7)
split.label(text="Server")
split.label(text="Online")
col = row.column(align=True)
col.operator("session.get_info", icon="FILE_REFRESH", text="")
row = layout.row()
col = row.column(align=True)
col.template_list("SESSION_UL_network", "", settings, "server_preset", context.window_manager, "server_index")
col.separator()
connectOp = col.row()
connectOp.enabled =is_server_selected
connectOp.operator("session.connect", text="Connect")
col = row.column(align=True)
col.operator("session.preset_server_add", icon="ADD", text="") # TODO : add conditions (need a name, etc..)
row_visible = col.row(align=True)
col_visible = row_visible.column(align=True)
col_visible.enabled = is_server_selected
col_visible.operator("session.preset_server_remove", icon="REMOVE", text="").target_server_name = active_server_name
col_visible.separator()
col_visible.operator("session.preset_server_edit", icon="GREASEPENCIL", text="").target_server_name = active_server_name
else: else:
exitbutton = layout.row()
exitbutton.scale_y = 1.5
exitbutton.operator("session.stop", icon='QUIT', text="Disconnect")
progress = session.state_progress progress = session.state_progress
row = layout.row()
current_state = session.state current_state = session.state
info_msg = None info_msg = None
if current_state == STATE_LOBBY: if current_state in [STATE_ACTIVE]:
usr = session.online_users.get(settings.username) row = row.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=True)
row.prop(settings.sync_flags, "sync_render_settings",text="",icon_only=True, icon='SCENE')
row.prop(settings.sync_flags, "sync_during_editmode", text="",icon_only=True, icon='EDITMODE_HLT')
row.prop(settings.sync_flags, "sync_active_camera", text="",icon_only=True, icon='VIEW_CAMERA')
row= layout.row() row= layout.row()
if current_state in [STATE_ACTIVE] and runtime_settings.is_host:
info_msg = f"LAN: {runtime_settings.internet_ip}"
if current_state == STATE_LOBBY:
info_msg = "Waiting for the session to start." info_msg = "Waiting for the session to start."
if usr and usr['admin']:
info_msg = "Init the session to start." if info_msg:
info_box = layout.row() info_box = row.box()
info_box.label(text=info_msg,icon='INFO')
init_row = layout.row()
init_row.operator("session.init", icon='TOOL_SETTINGS', text="Init")
else:
info_box = layout.row()
info_box.row().label(text=info_msg,icon='INFO') info_box.row().label(text=info_msg,icon='INFO')
# PROGRESS BAR # Progress bar
if current_state in [STATE_SYNCING, STATE_SRV_SYNC, STATE_WAITING]: if current_state in [STATE_SYNCING, STATE_SRV_SYNC, STATE_WAITING]:
row= layout.row()
row.label(text=f"Status: {get_state_str(current_state)}")
row= layout.row()
info_box = row.box() info_box = row.box()
info_box.label(text=printProgressBar( info_box.row().label(text=printProgressBar(
progress['current'], progress['current'],
progress['total'], progress['total'],
length=16 length=16
)) ))
class SESSION_PT_host_settings(bpy.types.Panel): layout.row().operator("session.stop", icon='QUIT', text="Exit")
bl_idname = "MULTIUSER_SETTINGS_HOST_PT_panel"
bl_label = "Hosting" class SESSION_PT_settings_network(bpy.types.Panel):
bl_idname = "MULTIUSER_SETTINGS_NETWORK_PT_panel"
bl_label = "Network"
bl_space_type = 'VIEW_3D' bl_space_type = 'VIEW_3D'
bl_region_type = 'UI' bl_region_type = 'UI'
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel' bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
bl_options = {'DEFAULT_CLOSED'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
settings = get_preferences()
return not session \ return not session \
or (session and session.state == 0) \ or (session and session.state == 0)
and not settings.sidebar_advanced_shown \
and not settings.is_first_launch
def draw_header(self, context): def draw_header(self, context):
self.layout.label(text="", icon='NETWORK_DRIVE') self.layout.label(text="", icon='URL')
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
runtime_settings = context.window_manager.session
settings = get_preferences() settings = get_preferences()
#HOST # USER SETTINGS
host_selection = layout.row().box() row = layout.row()
host_selection_row = host_selection.row() row.prop(runtime_settings, "session_mode", expand=True)
host_selection_row.label(text="Init the session from:") row = layout.row()
host_selection_row.prop(settings, "init_method", text="")
host_selection_row = host_selection.row()
host_selection_row.label(text="Port:")
host_selection_row.prop(settings, "host_port", text="")
host_selection_row = host_selection.row()
host_selection_col = host_selection_row.column()
host_selection_col.prop(settings, "host_use_server_password", text="Server password:")
host_selection_col = host_selection_row.column()
host_selection_col.enabled = True if settings.host_use_server_password else False
host_selection_col.prop(settings, "host_server_password", text="")
host_selection_row = host_selection.row()
host_selection_col = host_selection_row.column()
host_selection_col.prop(settings, "host_use_admin_password", text="Admin password:")
host_selection_col = host_selection_row.column()
host_selection_col.enabled = True if settings.host_use_admin_password else False
host_selection_col.prop(settings, "host_admin_password", text="")
host_selection = layout.column() col = row.row(align=True)
host_selection.operator("session.host", text="Host") col.prop(settings, "server_preset_interface", text="")
col.operator("session.preset_server_add", icon='ADD', text="")
col.operator("session.preset_server_remove", icon='REMOVE', text="")
row = layout.row()
box = row.box()
if runtime_settings.session_mode == 'HOST':
row = box.row()
row.label(text="Port:")
row.prop(settings, "port", text="")
row = box.row()
row.label(text="Start from:")
row.prop(settings, "init_method", text="")
row = box.row()
row.label(text="Admin password:")
row.prop(settings, "password", text="")
row = box.row()
row.operator("session.start", text="HOST").host = True
else:
row = box.row()
row.prop(settings, "ip", text="IP")
row = box.row()
row.label(text="Port:")
row.prop(settings, "port", text="")
row = box.row()
row.prop(runtime_settings, "admin", text='Connect as admin', icon='DISCLOSURE_TRI_DOWN' if runtime_settings.admin
else 'DISCLOSURE_TRI_RIGHT')
if runtime_settings.admin:
row = box.row()
row.label(text="Password:")
row.prop(settings, "password", text="")
row = box.row()
row.operator("session.start", text="CONNECT").host = False
class SESSION_PT_settings_user(bpy.types.Panel):
bl_idname = "MULTIUSER_SETTINGS_USER_PT_panel"
bl_label = "User info"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
@classmethod
def poll(cls, context):
return not session \
or (session and session.state == 0)
def draw_header(self, context):
self.layout.label(text="", icon='USER')
def draw(self, context):
layout = self.layout
runtime_settings = context.window_manager.session
settings = get_preferences()
row = layout.row()
# USER SETTINGS
row.prop(settings, "username", text="name")
row = layout.row()
row.prop(settings, "client_color", text="color")
row = layout.row()
class SESSION_PT_advanced_settings(bpy.types.Panel): class SESSION_PT_advanced_settings(bpy.types.Panel):
bl_idname = "MULTIUSER_SETTINGS_REPLICATION_PT_panel" bl_idname = "MULTIUSER_SETTINGS_REPLICATION_PT_panel"
bl_label = "General Settings" bl_label = "Advanced"
bl_space_type = 'VIEW_3D' bl_space_type = 'VIEW_3D'
bl_region_type = 'UI' bl_region_type = 'UI'
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel' bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
@ -254,34 +234,19 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
settings = get_preferences()
return not session \ return not session \
or (session and session.state == 0) \ or (session and session.state == 0)
and not settings.sidebar_advanced_shown \
and not settings.is_first_launch
def draw_header(self, context): def draw_header(self, context):
self.layout.label(text="", icon='PREFERENCES') self.layout.label(text="", icon='PREFERENCES')
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
runtime_settings = context.window_manager.session
settings = get_preferences() settings = get_preferences()
#ADVANCED USER INFO
uinfo_section = layout.row().box()
uinfo_section.prop(
settings,
"sidebar_advanced_uinfo_expanded",
text="User Info",
icon=get_expanded_icon(settings.sidebar_advanced_uinfo_expanded),
emboss=False)
if settings.sidebar_advanced_uinfo_expanded:
uinfo_section_row = uinfo_section.row()
uinfo_section_split = uinfo_section_row.split(factor=0.7, align=True)
uinfo_section_split.prop(settings, "username", text="")
uinfo_section_split.prop(settings, "client_color", text="")
#ADVANCED NET
net_section = layout.row().box() net_section = layout.row().box()
net_section.prop( net_section.prop(
settings, settings,
@ -289,15 +254,12 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
text="Network", text="Network",
icon=get_expanded_icon(settings.sidebar_advanced_net_expanded), icon=get_expanded_icon(settings.sidebar_advanced_net_expanded),
emboss=False) emboss=False)
if settings.sidebar_advanced_net_expanded: if settings.sidebar_advanced_net_expanded:
net_section_row = net_section.row() net_section_row = net_section.row()
net_section_row.label(text="Timeout (ms):") net_section_row.label(text="Timeout (ms):")
net_section_row.prop(settings, "connection_timeout", text="") net_section_row.prop(settings, "connection_timeout", text="")
net_section_row = net_section.row()
net_section_row.label(text="Server ping (ms):")
net_section_row.prop(settings, "ping_timeout", text="")
#ADVANCED REPLICATION
replication_section = layout.row().box() replication_section = layout.row().box()
replication_section.prop( replication_section.prop(
settings, settings,
@ -305,12 +267,16 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
text="Replication", text="Replication",
icon=get_expanded_icon(settings.sidebar_advanced_rep_expanded), icon=get_expanded_icon(settings.sidebar_advanced_rep_expanded),
emboss=False) emboss=False)
if settings.sidebar_advanced_rep_expanded: if settings.sidebar_advanced_rep_expanded:
replication_section_row = replication_section.row()
replication_section_row = replication_section.row() replication_section_row = replication_section.row()
replication_section_row.prop(settings.sync_flags, "sync_render_settings") replication_section_row.prop(settings.sync_flags, "sync_render_settings")
replication_section_row = replication_section.row() replication_section_row = replication_section.row()
replication_section_row.prop(settings.sync_flags, "sync_active_camera") replication_section_row.prop(settings.sync_flags, "sync_active_camera")
replication_section_row = replication_section.row() replication_section_row = replication_section.row()
replication_section_row.prop(settings.sync_flags, "sync_during_editmode") replication_section_row.prop(settings.sync_flags, "sync_during_editmode")
replication_section_row = replication_section.row() replication_section_row = replication_section.row()
if settings.sync_flags.sync_during_editmode: if settings.sync_flags.sync_during_editmode:
@ -319,7 +285,7 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
replication_section_row = replication_section.row() replication_section_row = replication_section.row()
replication_section_row.prop(settings, "depsgraph_update_rate", text="Apply delay") replication_section_row.prop(settings, "depsgraph_update_rate", text="Apply delay")
#ADVANCED CACHE
cache_section = layout.row().box() cache_section = layout.row().box()
cache_section.prop( cache_section.prop(
settings, settings,
@ -337,8 +303,6 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
cache_section_row.prop(settings, "clear_memory_filecache", text="") cache_section_row.prop(settings, "clear_memory_filecache", text="")
cache_section_row = cache_section.row() cache_section_row = cache_section.row()
cache_section_row.operator('session.clear_cache', text=f"Clear cache ({get_folder_size(settings.cache_directory)})") cache_section_row.operator('session.clear_cache', text=f"Clear cache ({get_folder_size(settings.cache_directory)})")
#ADVANCED LOG
log_section = layout.row().box() log_section = layout.row().box()
log_section.prop( log_section.prop(
settings, settings,
@ -346,11 +310,11 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
text="Logging", text="Logging",
icon=get_expanded_icon(settings.sidebar_advanced_log_expanded), icon=get_expanded_icon(settings.sidebar_advanced_log_expanded),
emboss=False) emboss=False)
if settings.sidebar_advanced_log_expanded: if settings.sidebar_advanced_log_expanded:
log_section_row = log_section.row() log_section_row = log_section.row()
log_section_row.label(text="Log level:") log_section_row.label(text="Log level:")
log_section_row.prop(settings, 'logging_level', text="") log_section_row.prop(settings, 'logging_level', text="")
class SESSION_PT_user(bpy.types.Panel): class SESSION_PT_user(bpy.types.Panel):
bl_idname = "MULTIUSER_USER_PT_panel" bl_idname = "MULTIUSER_USER_PT_panel"
bl_label = "Online users" bl_label = "Online users"
@ -360,8 +324,7 @@ class SESSION_PT_user(bpy.types.Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return session \ return session and session.state in [STATE_ACTIVE, STATE_LOBBY]
and session.state in [STATE_ACTIVE, STATE_LOBBY]
def draw_header(self, context): def draw_header(self, context):
self.layout.label(text="", icon='USER') self.layout.label(text="", icon='USER')
@ -373,8 +336,9 @@ class SESSION_PT_user(bpy.types.Panel):
settings = get_preferences() settings = get_preferences()
active_user = online_users[selected_user] if len( active_user = online_users[selected_user] if len(
online_users)-1 >= selected_user else 0 online_users)-1 >= selected_user else 0
runtime_settings = context.window_manager.session
#USER LIST # Create a simple row.
row = layout.row() row = layout.row()
box = row.box() box = row.box()
split = box.split(factor=0.35) split = box.split(factor=0.35)
@ -389,7 +353,6 @@ class SESSION_PT_user(bpy.types.Panel):
layout.template_list("SESSION_UL_users", "", context.window_manager, layout.template_list("SESSION_UL_users", "", context.window_manager,
"online_users", context.window_manager, "user_index") "online_users", context.window_manager, "user_index")
#OPERATOR ON USER
if active_user != 0 and active_user.username != settings.username: if active_user != 0 and active_user.username != settings.username:
row = layout.row() row = layout.row()
user_operations = row.split() user_operations = row.split()
@ -473,8 +436,57 @@ class SESSION_UL_users(bpy.types.UIList):
split.label(text=scene_current) split.label(text=scene_current)
split.label(text=ping) split.label(text=ping)
class SESSION_PT_presence(bpy.types.Panel):
bl_idname = "MULTIUSER_MODULE_PT_panel"
bl_label = "Presence overlay"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return not session \
or (session and session.state in [STATE_INITIAL, STATE_ACTIVE])
def draw_header(self, context):
self.layout.prop(context.window_manager.session,
"enable_presence", text="",icon='OVERLAY')
def draw(self, context):
layout = self.layout
settings = context.window_manager.session
pref = get_preferences()
layout.active = settings.enable_presence
row = layout.row()
row = row.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=True)
row.prop(settings, "presence_show_selected",text="",icon_only=True, icon='CUBE')
row.prop(settings, "presence_show_user", text="",icon_only=True, icon='CAMERA_DATA')
row.prop(settings, "presence_show_mode", text="",icon_only=True, icon='OBJECT_DATAMODE')
row.prop(settings, "presence_show_far_user", text="",icon_only=True, icon='SCENE_DATA')
col = layout.column()
if settings.presence_show_mode :
row = col.column()
row.prop(pref, "presence_mode_distance", expand=True)
col.prop(settings, "presence_show_session_status")
if settings.presence_show_session_status :
row = col.column()
row.active = settings.presence_show_session_status
row.prop(pref, "presence_hud_scale", expand=True)
row = col.column(align=True)
row.active = settings.presence_show_session_status
row.prop(pref, "presence_hud_hpos", expand=True)
row.prop(pref, "presence_hud_vpos", expand=True)
def draw_property(context, parent, property_uuid, level=0): def draw_property(context, parent, property_uuid, level=0):
settings = get_preferences() settings = get_preferences()
runtime_settings = context.window_manager.session
item = session.repository.graph.get(property_uuid) item = session.repository.graph.get(property_uuid)
type_id = item.data.get('type_id') type_id = item.data.get('type_id')
area_msg = parent.row(align=True) area_msg = parent.row(align=True)
@ -494,18 +506,15 @@ def draw_property(context, parent, property_uuid, level=0):
detail_item_box.label(text=f"{name}") detail_item_box.label(text=f"{name}")
# Operations # Operations
have_right_to_modify = (item.owner == settings.username or \ have_right_to_modify = (item.owner == settings.username or \
item.owner == RP_COMMON) and item.state != ERROR item.owner == RP_COMMON) and item.state != ERROR
from multi_user import icons
sync_status = icons.icons_col["repository_push"] #TODO: Link all icons to the right sync (push/merge/issue). For issue use "UNLINKED" for icon
# sync_status = icons.icons_col["repository_merge"]
if have_right_to_modify: if have_right_to_modify:
detail_item_box.operator( detail_item_box.operator(
"session.commit", "session.commit",
text="", text="",
icon_value=sync_status.icon_id).target = item.uuid icon='TRIA_UP').target = item.uuid
detail_item_box.separator() detail_item_box.separator()
if item.state in [FETCHED, UP]: if item.state in [FETCHED, UP]:
@ -537,72 +546,12 @@ def draw_property(context, parent, property_uuid, level=0):
else: else:
detail_item_box.label(text="", icon="DECORATE_LOCKED") detail_item_box.label(text="", icon="DECORATE_LOCKED")
class SESSION_PT_sync(bpy.types.Panel):
bl_idname = "MULTIUSER_SYNC_PT_panel"
bl_label = "Synchronize"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return session \
and session.state in [STATE_ACTIVE]
def draw_header(self, context):
self.layout.label(text="", icon='UV_SYNC_SELECT')
def draw(self, context):
layout = self.layout
settings = get_preferences()
row= layout.row()
row = row.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=True)
row.prop(settings.sync_flags, "sync_render_settings",text="",icon_only=True, icon='SCENE')
row.prop(settings.sync_flags, "sync_during_editmode", text="",icon_only=True, icon='EDITMODE_HLT')
row.prop(settings.sync_flags, "sync_active_camera", text="",icon_only=True, icon='VIEW_CAMERA')
class SESSION_PT_replay(bpy.types.Panel):
bl_idname = "MULTIUSER_REPLAY_PT_panel"
bl_label = "Replay"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return context.window_manager.session.replay_files
def draw_header(self, context):
self.layout.label(text="", icon='RECOVER_LAST')
def draw(self, context):
layout = self.layout
settings = context.window_manager.session
row= layout.row()
row.prop(settings,'replay_mode', toggle=True, expand=True)
row= layout.row()
if settings.replay_mode == 'MANUAL':
row.prop(bpy.context.scene, 'active_replay_file', text="Snapshot index")
else:
row.prop(settings, 'replay_duration', text="Replay Duration")
row= layout.row()
row.prop(settings, 'replay_persistent_collection', text="persistent collection", toggle=True, icon='OUTLINER_COLLECTION')
if settings.replay_persistent_collection:
row= layout.row()
row.prop(settings, 'replay_camera', text="", icon='VIEW_CAMERA')
class SESSION_PT_repository(bpy.types.Panel): class SESSION_PT_repository(bpy.types.Panel):
bl_idname = "MULTIUSER_PROPERTIES_PT_panel" bl_idname = "MULTIUSER_PROPERTIES_PT_panel"
bl_label = "Repository" bl_label = "Repository"
bl_space_type = 'VIEW_3D' bl_space_type = 'VIEW_3D'
bl_region_type = 'UI' bl_region_type = 'UI'
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel' bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
bl_options = {'DEFAULT_CLOSED'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@ -615,8 +564,8 @@ class SESSION_PT_repository(bpy.types.Panel):
admin = usr['admin'] admin = usr['admin']
return hasattr(context.window_manager, 'session') and \ return hasattr(context.window_manager, 'session') and \
session and \ session and \
session.state == STATE_ACTIVE and \ (session.state == STATE_ACTIVE or \
not settings.sidebar_repository_shown session.state == STATE_LOBBY and admin)
def draw_header(self, context): def draw_header(self, context):
self.layout.label(text="", icon='OUTLINER_OB_GROUP_INSTANCE') self.layout.label(text="", icon='OUTLINER_OB_GROUP_INSTANCE')
@ -630,18 +579,19 @@ class SESSION_PT_repository(bpy.types.Panel):
usr = session.online_users.get(settings.username) usr = session.online_users.get(settings.username)
row = layout.row()
if session.state == STATE_ACTIVE: if session.state == STATE_ACTIVE:
if 'SessionBackupTimer' in registry: if 'SessionBackupTimer' in registry:
row = layout.row()
row.alert = True row.alert = True
row.operator('session.cancel_autosave', icon="CANCEL") row.operator('session.cancel_autosave', icon="CANCEL")
row.alert = False row.alert = False
# else: else:
# row.operator('session.save', icon="FILE_TICK") row.operator('session.save', icon="FILE_TICK")
box = layout.box() box = layout.box()
row = box.row() row = box.row()
row.prop(runtime_settings, "filter_owned", text="Only show owned data blocks", icon_only=True, icon="DECORATE_UNLOCKED") row.prop(runtime_settings, "filter_owned", text="Show only owned Nodes", icon_only=True, icon="DECORATE_UNLOCKED")
row = box.row() row = box.row()
row.prop(runtime_settings, "filter_name", text="Filter") row.prop(runtime_settings, "filter_name", text="Filter")
row = box.row() row = box.row()
@ -661,6 +611,10 @@ class SESSION_PT_repository(bpy.types.Panel):
else: else:
layout.row().label(text="Empty") layout.row().label(text="Empty")
elif session.state == STATE_LOBBY and usr and usr['admin']:
row.operator("session.init", icon='TOOL_SETTINGS', text="Init")
else:
row.label(text="Waiting to start")
class VIEW3D_PT_overlay_session(bpy.types.Panel): class VIEW3D_PT_overlay_session(bpy.types.Panel):
bl_space_type = 'VIEW_3D' bl_space_type = 'VIEW_3D'
@ -680,70 +634,41 @@ class VIEW3D_PT_overlay_session(bpy.types.Panel):
layout.active = settings.enable_presence layout.active = settings.enable_presence
row = layout.row() row = layout.row()
row.prop(settings, "enable_presence",text="Presence Overlay") row = row.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=True)
row.prop(settings, "presence_show_selected",text="",icon_only=True, icon='CUBE')
row = layout.row() row.prop(settings, "presence_show_user", text="",icon_only=True, icon='CAMERA_DATA')
row.prop(settings, "presence_show_selected",text="Selected Objects") row.prop(settings, "presence_show_mode", text="",icon_only=True, icon='OBJECT_DATAMODE')
row.prop(settings, "presence_show_far_user", text="",icon_only=True, icon='SCENE_DATA')
row = layout.row(align=True)
row.prop(settings, "presence_show_user", text="Users camera")
row.prop(settings, "presence_show_mode", text="Users mode")
col = layout.column() col = layout.column()
if settings.presence_show_mode or settings.presence_show_user: if settings.presence_show_mode :
row = col.column() row = col.column()
row.prop(pref, "presence_text_distance", expand=True) row.prop(pref, "presence_mode_distance", expand=True)
row = col.column()
row.prop(settings, "presence_show_far_user", text="Users on different scenes")
col.prop(settings, "presence_show_session_status") col.prop(settings, "presence_show_session_status")
if settings.presence_show_session_status : if settings.presence_show_session_status :
split = layout.split() row = col.column()
text_pos = split.column(align=True) row.active = settings.presence_show_session_status
text_pos.active = settings.presence_show_session_status row.prop(pref, "presence_hud_scale", expand=True)
text_pos.prop(pref, "presence_hud_hpos", expand=True) row = col.column(align=True)
text_pos.prop(pref, "presence_hud_vpos", expand=True) row.active = settings.presence_show_session_status
text_scale = split.column() row.prop(pref, "presence_hud_hpos", expand=True)
text_scale.active = settings.presence_show_session_status row.prop(pref, "presence_hud_vpos", expand=True)
text_scale.prop(pref, "presence_hud_scale", expand=True)
class SESSION_UL_network(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag):
settings = get_preferences()
server_name = '-'
server_status = 'BLANK1'
server_private = 'BLANK1'
server_name = item.server_name
split = layout.split(factor=0.7)
if item.is_private:
server_private = 'LOCKED'
split.label(text=server_name, icon=server_private)
else:
split.label(text=server_name)
from multi_user import icons
server_status = icons.icons_col["server_offline"]
if item.is_online:
server_status = icons.icons_col["server_online"]
split.label(icon_value=server_status.icon_id)
classes = ( classes = (
SESSION_UL_users, SESSION_UL_users,
SESSION_UL_network,
SESSION_PT_settings, SESSION_PT_settings,
SESSION_PT_host_settings, SESSION_PT_settings_user,
SESSION_PT_settings_network,
SESSION_PT_presence,
SESSION_PT_advanced_settings, SESSION_PT_advanced_settings,
SESSION_PT_user, SESSION_PT_user,
SESSION_PT_sync,
SESSION_PT_repository, SESSION_PT_repository,
VIEW3D_PT_overlay_session, VIEW3D_PT_overlay_session,
SESSION_PT_replay,
) )
register, unregister = bpy.utils.register_classes_factory(classes) register, unregister = bpy.utils.register_classes_factory(classes)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -38,6 +38,15 @@ from replication.constants import (STATE_ACTIVE, STATE_AUTH,
STATE_LOBBY, STATE_LOBBY,
CONNECTING) CONNECTING)
CLEARED_DATABLOCKS = ['actions', 'armatures', 'cache_files', 'cameras',
'collections', 'curves', 'filepath', 'fonts',
'grease_pencils', 'images', 'lattices', 'libraries',
'lightprobes', 'lights', 'linestyles', 'masks',
'materials', 'meshes', 'metaballs', 'movieclips',
'node_groups', 'objects', 'paint_curves', 'particles',
'scenes', 'shape_keys', 'sounds', 'speakers', 'texts',
'textures', 'volumes', 'worlds']
def find_from_attr(attr_name, attr_value, list): def find_from_attr(attr_name, attr_value, list):
for item in list: for item in list:
if getattr(item, attr_name, None) == attr_value: if getattr(item, attr_name, None) == attr_value:
@ -99,6 +108,26 @@ def get_state_str(state):
return state_str return state_str
def clean_scene():
for type_name in CLEARED_DATABLOCKS:
sub_collection_to_avoid = [
bpy.data.linestyles.get('LineStyle'),
bpy.data.materials.get('Dots Stroke')
]
type_collection = getattr(bpy.data, type_name)
items_to_remove = [i for i in type_collection if i not in sub_collection_to_avoid]
for item in items_to_remove:
try:
type_collection.remove(item)
logging.info(item.name)
except:
continue
# Clear sequencer
bpy.context.scene.sequence_editor_clear()
def get_selected_objects(scene, active_view_layer): def get_selected_objects(scene, active_view_layer):
return [obj.uuid for obj in scene.objects if obj.select_get(view_layer=active_view_layer)] return [obj.uuid for obj in scene.objects if obj.select_get(view_layer=active_view_layer)]

View File

@ -1,7 +1,7 @@
# Download base image debian jessie # Download base image debian jessie
FROM python:slim FROM python:slim
ARG replication_version=0.9.1 ARG replication_version=0.1.13
ARG version=0.1.1 ARG version=0.1.1
# Infos # Infos
@ -22,4 +22,4 @@ RUN pip install replication==$replication_version
# Run the server with parameters # Run the server with parameters
ENTRYPOINT ["/bin/sh", "-c"] ENTRYPOINT ["/bin/sh", "-c"]
CMD ["replication.serve -apwd ${password} -spwd '' -p ${port} -t ${timeout} -l ${log_level} -lf ${log_file}"] CMD ["python3 -m replication.server -pwd ${password} -p ${port} -t ${timeout} -l ${log_level} -lf ${log_file}"]

View File

@ -7,7 +7,7 @@ import bpy
from multi_user.bl_types.bl_lightprobe import BlLightprobe from multi_user.bl_types.bl_lightprobe import BlLightprobe
@pytest.mark.skipif(bpy.app.version < (2,83,0), reason="requires blender 2.83 or higher") @pytest.mark.skipif(bpy.app.version[1] < 83, reason="requires blender 2.83 or higher")
@pytest.mark.parametrize('lightprobe_type', ['PLANAR','GRID','CUBEMAP']) @pytest.mark.parametrize('lightprobe_type', ['PLANAR','GRID','CUBEMAP'])
def test_lightprobes(clear_blend, lightprobe_type): def test_lightprobes(clear_blend, lightprobe_type):
bpy.ops.object.lightprobe_add(type=lightprobe_type) bpy.ops.object.lightprobe_add(type=lightprobe_type)

20
tests/test_operators.py Normal file
View File

@ -0,0 +1,20 @@
import os
import pytest
from deepdiff import DeepDiff
import bpy
import random
def test_start_session():
result = bpy.ops.session.start()
assert 'FINISHED' in result
def test_stop_session():
result = bpy.ops.session.stop()
assert 'FINISHED' in result