Compare commits

...

36 Commits

Author SHA1 Message Date
48eded1a9b feat: update replay 2022-01-21 09:06:56 +01:00
3fbe769928 feat: groundd work on legacy snapshots loading 2021-12-10 16:57:03 +01:00
fe998214be feat: replay duration 2021-12-08 17:42:20 +01:00
3a4f691b8f feat: previous message 2021-12-08 16:30:56 +01:00
0e20d35e7d refactor: move clean to operator.py
feat: timeline action mapping for replay
feat: persistent  collection
fix: draw users in the right scene according to the snapshot
2021-12-08 16:30:40 +01:00
8d15e69b50 fix: replay count 2021-11-24 19:12:29 +01:00
8000ce9931 feat: basic file sequence loading 2021-11-24 18:56:17 +01:00
b96f600f15 feat: initial inteface 2021-11-18 18:01:35 +01:00
de32bd89e3 Merge branch '237-add-draw-user-option-for-the-session-snapshot-importer' into 'develop'
Resolve "Add draw user option for the session snapshot importer"

See merge request slumber/multi-user!156
2021-11-18 15:21:36 +00:00
50e86aea15 fix user drawing options 2021-11-18 16:05:24 +01:00
c05a12343c feat: selection drawing 2021-11-18 15:22:07 +01:00
a09193fba2 feat: expose user radius and intensity 2021-11-18 11:53:24 +01:00
60e21f2b8e fix: load user 2021-11-18 11:43:01 +01:00
421f00879f feat draw users 2021-11-18 11:40:56 +01:00
964e6a8c63 feat: uesr meshes 2021-11-16 09:55:13 +01:00
80c81dc934 Merge branch '240-adding-music-to-the-sequencer-isn-t-replicating' into 'develop'
Resolve "Adding music to the sequencer isn't replicating"

See merge request slumber/multi-user!159
2021-11-09 09:29:58 +00:00
563fdb693d fix: sound not loading
Related to #240
2021-11-09 10:26:47 +01:00
a64eea3cea Merge branch '239-blender-3-x-compatibility' into 'develop'
Ensure blender 3.x compatibility : Fix geometry node outputs replication

See merge request slumber/multi-user!158
2021-11-09 08:48:30 +00:00
03ad7c0066 fix: geometry nodes input / output 2021-11-08 17:34:02 +01:00
d685573834 Merge branch '239-blender-3-x-compatibility' into 'develop'
Ensure blender 3.x version check

See merge request slumber/multi-user!157
2021-11-05 15:20:35 +00:00
0681b53141 fix: version check 2021-11-05 15:39:46 +01:00
6f02b38b0e fix(replication): missing version update 2021-11-03 16:37:12 +01:00
92c773dae9 Merge branch 'develop' of gitlab.com:slumber/multi-user into develop 2021-11-03 16:34:43 +01:00
f48ade6390 fix python 3.10 compatibility (@NotFood) 2021-11-03 16:32:40 +01:00
63c4501b88 Merge branch '236-crash-with-empty-after-a-reconnection' into 'develop'
Resolve "Crash with empty after a reconnection"

See merge request slumber/multi-user!155
2021-10-29 09:40:04 +00:00
06e21c86ce fix none attribute error 2021-10-21 12:19:46 +02:00
9d484b00e9 Merge branch '234-user-info-in-side-panel' into 'develop'
User Info in side panel

See merge request slumber/multi-user!153
2021-08-19 16:09:24 +00:00
de9255f71c feat: presence overlay button+UInfo in side panel 2021-08-19 18:04:07 +02:00
99528ea3e0 Merge branch '232-fix-ui-host-and-lobby' into 'develop'
Resolve "fix ui host and lobby"

See merge request slumber/multi-user!152
2021-08-16 14:03:16 +00:00
bb342951a5 fix: lobby init 2021-08-16 15:59:19 +02:00
438a79177b fix: host solo 2021-08-16 12:02:10 +02:00
08fc49c40f fix: session private by default 2021-07-30 14:09:40 +02:00
d7e25b1192 fix: clean docker file 2021-07-30 13:47:31 +02:00
1671422143 Merge branch 'develop' of gitlab.com:slumber/multi-user into develop 2021-07-30 13:17:29 +02:00
a9620c0752 fix: docker server command 2021-07-30 13:16:43 +02:00
583beaf6fe Merge branch '231-server-public-session-private-issue' into 'develop'
Server "public session" private issue

See merge request slumber/multi-user!151
2021-07-28 15:34:24 +00:00
16 changed files with 674 additions and 142 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.server replication.serve
.. 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.server -p 5555 -pwd admin -t 5000 -l INFO -lf server.log replication.serve -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.server -pwd admin -p 5555 -t 5000 -l DEBUG -lf multiuser_server.log replication.serve -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
python3 -m replication.server -pwd supersecretpassword -p 5555 -t 3000 -l DEBUG -lf logname.log replication.serve -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
python3 -m replication.server -p 5555 -pwd admin -t 5000 -l INFO -lf server.log replication.serve -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

@ -43,7 +43,7 @@ __all__ = [
"bl_particle", "bl_particle",
] # Order here defines execution order ] # Order here defines execution order
if bpy.app.version[1] >= 91: if bpy.app.version >= (2,91,0):
__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[1] >= 91: if bpy.app.version >= (2,91,0):
STROKE.append('use_cyclic') STROKE.append('use_cyclic')
else: else:
STROKE.append('draw_cyclic') STROKE.append('draw_cyclic')
if bpy.app.version[1] >= 83: if bpy.app.version >= (2,83,0):
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[1] >= 83: if bpy.app.version >= (2,83,0):
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[1] < 83: if bpy.app.version < (2,83,0):
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[1] >= 93: if bpy.app.version >= (2,93,0):
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,14 +56,24 @@ else:
blender 2.92.") blender 2.92.")
def get_node_group_inputs(node_group): def get_node_group_properties_identifiers(node_group):
inputs = [] props_ids = []
# 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:
inputs.append(inpt) props_ids.append((inpt.identifier, inpt.type))
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]
@ -122,29 +132,35 @@ 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_inputs(modifier: bpy.types.Modifier) -> list: def dump_modifier_geometry_node_props(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_inputs = [] dumped_props = []
for inpt in get_node_group_inputs(modifier.node_group):
input_value = modifier[inpt.identifier]
dumped_input = None for prop_value, prop_type in get_node_group_properties_identifiers(modifier.node_group):
if isinstance(input_value, bpy.types.ID): try:
dumped_input = input_value.uuid prop_value = modifier[prop_value]
elif isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS): except KeyError as e:
dumped_input = input_value logging.error(f"fail to dump geomety node modifier property : {prop_value} ({e})")
elif hasattr(input_value, 'to_list'): else:
dumped_input = input_value.to_list() dump = None
dumped_inputs.append(dumped_input) if isinstance(prop_value, bpy.types.ID):
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()
return dumped_inputs dumped_props.append((dump, prop_type))
# logging.info(prop_value)
return dumped_props
def load_modifier_geometry_node_inputs(dumped_modifier: dict, target_modifier: bpy.types.Modifier): def load_modifier_geometry_node_props(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
@ -153,17 +169,17 @@ def load_modifier_geometry_node_inputs(dumped_modifier: dict, target_modifier: b
:type target_modifier: bpy.type.Modifier :type target_modifier: bpy.type.Modifier
""" """
for input_index, inpt in enumerate(get_node_group_inputs(target_modifier.node_group)): for input_index, inpt in enumerate(get_node_group_properties_identifiers(target_modifier.node_group)):
dumped_value = dumped_modifier['inputs'][input_index] dumped_value, dumped_type = dumped_modifier['props'][input_index]
input_value = target_modifier[inpt.identifier] input_value = target_modifier[inpt[0]]
if isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS): if dumped_type in ['INT', 'VALUE', 'STR']:
target_modifier[inpt.identifier] = dumped_value logging.info(f"{inpt[0]}/{dumped_value}")
elif hasattr(input_value, 'to_list'): target_modifier[inpt[0]] = dumped_value
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 inpt.type in ['COLLECTION', 'OBJECT']: elif dumped_type in ['COLLECTION', 'OBJECT', 'IMAGE', 'TEXTURE', 'MATERIAL']:
target_modifier[inpt.identifier] = get_datablock_from_uuid( target_modifier[inpt[0]] = get_datablock_from_uuid(dumped_value, None)
dumped_value, None)
def load_pose(target_bone, data): def load_pose(target_bone, data):
@ -198,12 +214,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[1] >= 83: if bpy.app.version >= (2,83,0):
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[1] >= 91 and name in bpy.data.volumes.keys(): elif bpy.app.version >= (2,91,0) 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
@ -250,10 +266,11 @@ 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 in get_node_group_inputs(mod.node_group): for inpt, inpt_type in get_node_group_properties_identifiers(mod.node_group):
# parameter = mod.get(inpt.identifier) inpt_value = mod.get(inpt)
# if parameter and isinstance(parameter, bpy.types.ID): # Avoid to handle 'COLLECTION', 'OBJECT' to avoid circular dependencies
# dependencies.append(parameter) if inpt_type in ['IMAGE', 'TEXTURE', 'MATERIAL'] and inpt_value:
dependencies.append(inpt_value)
return dependencies return dependencies
@ -387,10 +404,7 @@ 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_inputs = dump_modifier_geometry_node_inputs( dumped_modifier['props'] = dump_modifier_geometry_node_props(modifier)
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",
@ -455,7 +469,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_inputs(dumped_modifier, loaded_modifier) load_modifier_geometry_node_props(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.value(): for seq_data in sequences.values():
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[1] >= 91: if bpy.app.version >= (2,91,0):
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

@ -130,14 +130,29 @@ 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

@ -35,8 +35,10 @@ 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
@ -58,13 +60,164 @@ 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 from .presence import SessionStatusWidget, renderer, view3d_find, refresh_sidebar_view, bbox_from_obj
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
@ -238,7 +391,7 @@ class SessionConnectOperator(bpy.types.Operator):
settings.generate_supported_types() settings.generate_supported_types()
if bpy.app.version[1] >= 91: if bpy.app.version >= (2,91,0):
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
@ -249,7 +402,7 @@ class SessionConnectOperator(bpy.types.Operator):
# Join a session # Join a session
if not active_server.use_admin_password: if not active_server.use_admin_password:
utils.clean_scene() clean_scene()
try: try:
porcelain.remote_add( porcelain.remote_add(
@ -309,7 +462,7 @@ class SessionHostOperator(bpy.types.Operator):
settings.generate_supported_types() settings.generate_supported_types()
if bpy.app.version[1] >= 91: if bpy.app.version >= (2,91,0):
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
@ -320,7 +473,7 @@ class SessionHostOperator(bpy.types.Operator):
# Host a session # Host a session
if settings.init_method == 'EMPTY': if settings.init_method == 'EMPTY':
utils.clean_scene() clean_scene()
try: try:
# Init repository # Init repository
@ -384,7 +537,7 @@ class SessionInitOperator(bpy.types.Operator):
def execute(self, context): def execute(self, context):
if self.init_method == 'EMPTY': if self.init_method == 'EMPTY':
utils.clean_scene() clean_scene()
for scene in bpy.data.scenes: for scene in bpy.data.scenes:
porcelain.add(session.repository, scene) porcelain.add(session.repository, scene)
@ -863,14 +1016,89 @@ 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)
utils.clean_scene() except TypeError:
# 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]
@ -885,6 +1113,69 @@ 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'}
@ -892,6 +1183,39 @@ 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"
@ -1106,6 +1430,26 @@ def menu_func_import(self, context):
def menu_func_export(self, context): def menu_func_export(self, context):
self.layout.operator(SessionSaveBackupOperator.bl_idname, text='Multi-user session snapshot (.db)') 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, SessionConnectOperator,
@ -1123,6 +1467,7 @@ classes = (
SessionNotifyOperator, SessionNotifyOperator,
SessionSaveBackupOperator, SessionSaveBackupOperator,
SessionLoadSaveOperator, SessionLoadSaveOperator,
SESSION_PT_ImportUser,
SessionStopAutoSaveOperator, SessionStopAutoSaveOperator,
SessionPurgeOperator, SessionPurgeOperator,
SessionPresetServerAdd, SessionPresetServerAdd,
@ -1131,6 +1476,7 @@ classes = (
RefreshServerStatus, RefreshServerStatus,
GetDoc, GetDoc,
FirstLaunch, FirstLaunch,
SessionRenderReplay,
) )

View File

@ -22,6 +22,7 @@ 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
@ -99,6 +100,77 @@ 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
@ -374,9 +446,9 @@ class SessionPrefs(bpy.types.AddonPreferences):
description="sidebar_advanced_log_expanded", description="sidebar_advanced_log_expanded",
default=False default=False
) )
sidebar_advanced_hosting_expanded: bpy.props.BoolProperty( sidebar_advanced_uinfo_expanded: bpy.props.BoolProperty(
name="sidebar_advanced_hosting_expanded", name="sidebar_advanced_uinfo_expanded",
description="sidebar_advanced_hosting_expanded", description="sidebar_advanced_uinfo_expanded",
default=False default=False
) )
sidebar_advanced_net_expanded: bpy.props.BoolProperty( sidebar_advanced_net_expanded: bpy.props.BoolProperty(
@ -687,6 +759,37 @@ 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 = (
@ -713,6 +816,16 @@ 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():
@ -720,3 +833,5 @@ 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,6 +203,7 @@ 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

@ -149,10 +149,8 @@ class SESSION_PT_settings(bpy.types.Panel):
col.template_list("SESSION_UL_network", "", settings, "server_preset", context.window_manager, "server_index") col.template_list("SESSION_UL_network", "", settings, "server_preset", context.window_manager, "server_index")
col.separator() col.separator()
connectOp = col.row() connectOp = col.row()
connectOp.operator("session.host", text="Host") connectOp.enabled =is_server_selected
connectopcol = connectOp.column() connectOp.operator("session.connect", text="Connect")
connectopcol.enabled =is_server_selected
connectopcol.operator("session.connect", text="Connect")
col = row.column(align=True) col = row.column(align=True)
col.operator("session.preset_server_add", icon="ADD", text="") # TODO : add conditions (need a name, etc..) col.operator("session.preset_server_add", icon="ADD", text="") # TODO : add conditions (need a name, etc..)
@ -173,11 +171,17 @@ class SESSION_PT_settings(bpy.types.Panel):
info_msg = None info_msg = None
if current_state == STATE_LOBBY: if current_state == STATE_LOBBY:
usr = session.online_users.get(settings.username)
row= layout.row() row= layout.row()
info_msg = "Waiting for the session to start." info_msg = "Waiting for the session to start."
if usr and usr['admin']:
if info_msg: info_msg = "Init the session to start."
info_box = row.box() info_box = layout.row()
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
@ -192,10 +196,57 @@ class SESSION_PT_settings(bpy.types.Panel):
length=16 length=16
)) ))
class SESSION_PT_host_settings(bpy.types.Panel):
bl_idname = "MULTIUSER_SETTINGS_HOST_PT_panel"
bl_label = "Hosting"
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):
settings = get_preferences()
return not session \
or (session and session.state == 0) \
and not settings.sidebar_advanced_shown \
and not settings.is_first_launch
def draw_header(self, context):
self.layout.label(text="", icon='NETWORK_DRIVE')
def draw(self, context):
layout = self.layout
settings = get_preferences()
#HOST
host_selection = layout.row().box()
host_selection_row = host_selection.row()
host_selection_row.label(text="Init the session from:")
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()
host_selection.operator("session.host", text="Host")
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 = "Advanced" bl_label = "General Settings"
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'
@ -216,30 +267,19 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
layout = self.layout layout = self.layout
settings = get_preferences() settings = get_preferences()
#ADVANCED HOST #ADVANCED USER INFO
host_selection = layout.row().box() uinfo_section = layout.row().box()
host_selection.prop( uinfo_section.prop(
settings, "sidebar_advanced_hosting_expanded", text="Hosting", settings,
icon=get_expanded_icon(settings.sidebar_advanced_hosting_expanded), "sidebar_advanced_uinfo_expanded",
text="User Info",
icon=get_expanded_icon(settings.sidebar_advanced_uinfo_expanded),
emboss=False) emboss=False)
if settings.sidebar_advanced_hosting_expanded: if settings.sidebar_advanced_uinfo_expanded:
host_selection_row = host_selection.row() uinfo_section_row = uinfo_section.row()
host_selection_row.prop(settings, "host_port", text="Port:") uinfo_section_split = uinfo_section_row.split(factor=0.7, align=True)
host_selection_row = host_selection.row() uinfo_section_split.prop(settings, "username", text="")
host_selection_row.label(text="Init the session from:") uinfo_section_split.prop(settings, "client_color", text="")
host_selection_row.prop(settings, "init_method", 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="")
#ADVANCED NET #ADVANCED NET
net_section = layout.row().box() net_section = layout.row().box()
@ -524,6 +564,38 @@ class SESSION_PT_sync(bpy.types.Panel):
row.prop(settings.sync_flags, "sync_active_camera", text="",icon_only=True, icon='VIEW_CAMERA') 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"
@ -543,8 +615,7 @@ 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 or \ session.state == STATE_ACTIVE and \
session.state == STATE_LOBBY and admin) and \
not settings.sidebar_repository_shown not settings.sidebar_repository_shown
def draw_header(self, context): def draw_header(self, context):
@ -590,12 +661,6 @@ 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 = layout.row()
row.operator("session.init", icon='TOOL_SETTINGS', text="Init")
else:
row = layout.row()
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'
@ -614,6 +679,9 @@ class VIEW3D_PT_overlay_session(bpy.types.Panel):
pref = get_preferences() pref = get_preferences()
layout.active = settings.enable_presence layout.active = settings.enable_presence
row = layout.row()
row.prop(settings, "enable_presence",text="Presence Overlay")
row = layout.row() row = layout.row()
row.prop(settings, "presence_show_selected",text="Selected Objects") row.prop(settings, "presence_show_selected",text="Selected Objects")
@ -667,11 +735,13 @@ classes = (
SESSION_UL_users, SESSION_UL_users,
SESSION_UL_network, SESSION_UL_network,
SESSION_PT_settings, SESSION_PT_settings,
SESSION_PT_host_settings,
SESSION_PT_advanced_settings, SESSION_PT_advanced_settings,
SESSION_PT_user, SESSION_PT_user,
SESSION_PT_sync, 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)

View File

@ -38,15 +38,6 @@ 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:
@ -108,26 +99,6 @@ 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.1.13 ARG replication_version=0.9.1
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 ["python3 -m replication.server -pwd ${password} -p ${port} -t ${timeout} -l ${log_level} -lf ${log_file}"] CMD ["replication.serve -apwd ${password} -spwd '' -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[1] < 83, reason="requires blender 2.83 or higher") @pytest.mark.skipif(bpy.app.version < (2,83,0), 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)