Compare commits
68 Commits
224-doc-up
...
251-vr-col
Author | SHA1 | Date | |
---|---|---|---|
630e1c7494 | |||
1b614f4fb6 | |||
629f2e1cdb | |||
b8fed806ed | |||
8190846b59 | |||
c228b6ad7f | |||
48651ce890 | |||
26847cf459 | |||
bfa6991c00 | |||
70b6f9bcfa | |||
8d176b55e4 | |||
4c0356e724 | |||
6b04d1d8d6 | |||
edfcdd8867 | |||
bdd6599614 | |||
047bd47048 | |||
d32cbb7b30 | |||
adabce3822 | |||
62f52db5b2 | |||
745f45b682 | |||
f84860f520 | |||
c7ee67d4dd | |||
7ed4644b75 | |||
e0c4a17be9 | |||
2a6181b832 | |||
0f7c9adec5 | |||
f094ec097c | |||
2495b5b0e7 | |||
cc829b66d1 | |||
97cec4f9af | |||
3669aafcff | |||
dfcfb84c20 | |||
5390e1a60c | |||
2910ea654b | |||
ff2ecec18b | |||
7555b1332a | |||
690e450349 | |||
de32bd89e3 | |||
50e86aea15 | |||
c05a12343c | |||
a09193fba2 | |||
60e21f2b8e | |||
421f00879f | |||
5ac61b5348 | |||
189e5c6cf1 | |||
964e6a8c63 | |||
80c81dc934 | |||
563fdb693d | |||
a64eea3cea | |||
03ad7c0066 | |||
d685573834 | |||
0681b53141 | |||
6f02b38b0e | |||
92c773dae9 | |||
f48ade6390 | |||
63c4501b88 | |||
06e21c86ce | |||
e28d3860da | |||
7b247372fb | |||
9d484b00e9 | |||
de9255f71c | |||
99528ea3e0 | |||
bb342951a5 | |||
438a79177b | |||
08fc49c40f | |||
d7e25b1192 | |||
1671422143 | |||
a9620c0752 |
48
CHANGELOG.md
48
CHANGELOG.md
@ -217,3 +217,51 @@ All notable changes to this project will be documented in this file.
|
||||
- GPencil fill stroke
|
||||
- Sculpt and GPencil brushes deleted when joining a session (@Kysios)
|
||||
- Auto-updater doesn't work for master and develop builds
|
||||
|
||||
## [0.5.0] - 2022-02-10
|
||||
|
||||
### Added
|
||||
|
||||
- New overall UI and UX (@Kysios)
|
||||
- Documentation overall update (@Kysios)
|
||||
- Server presets (@Kysios)
|
||||
- Server online status (@Kysios)
|
||||
- Draw connected user color in the user list
|
||||
- Private session (access protected with a password) (@Kysios)
|
||||
|
||||
### Changed
|
||||
|
||||
- Dependencies are now installed in the addon folder and correctly cleaned during the addon removal process
|
||||
|
||||
### Fixed
|
||||
|
||||
- Python 3.10 compatibility (@notfood)
|
||||
- Blender 3.x compatibility
|
||||
- Skin vertex radius synchronization (@kromar)
|
||||
- Sequencer audio strip synchronization
|
||||
- Crash with empty after a reconnection
|
||||
|
||||
## [0.5.1] - 2022-02-10
|
||||
|
||||
### Fixed
|
||||
|
||||
- Auto updater breaks dependency auto installer
|
||||
- Auto updater update from tag
|
||||
|
||||
## [0.5.2] - 2022-02-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- Objects not selectable after user leaves session
|
||||
- Geometry nodes attribute toogle doesn't sync
|
||||
|
||||
## [0.5.3] - 2022-03-11
|
||||
|
||||
### Changed
|
||||
|
||||
- Snapshots logs
|
||||
### Fixed
|
||||
|
||||
- Server crashing during snapshots
|
||||
- Blender 3.1 numpy loading error during early connection process
|
||||
- Server docker arguments
|
@ -206,20 +206,20 @@ You can run the dedicated server on any platform by following these steps:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python -m pip install replication==0.1.13
|
||||
python -m pip install replication
|
||||
|
||||
4. Launch the server with:
|
||||
3. Launch the server with:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
replication.server
|
||||
replication.serve
|
||||
|
||||
.. 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
|
||||
|
||||
.. 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.
|
||||
|
||||
@ -572,7 +572,7 @@ For example, I would like to launch my server with a different administrator pas
|
||||
|
||||
.. 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:
|
||||
|
||||
@ -691,7 +691,7 @@ We're finally ready to launch the server. Simply run:
|
||||
|
||||
.. 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
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
bl_info = {
|
||||
"name": "Multi-User",
|
||||
"author": "Swann Martinez",
|
||||
"version": (0, 4, 0),
|
||||
"version": (0, 5, 4),
|
||||
"description": "Enable real-time collaborative workflow inside blender",
|
||||
"blender": (2, 82, 0),
|
||||
"location": "3D View > Sidebar > Multi-User tab",
|
||||
|
@ -1015,9 +1015,11 @@ class Singleton_updater(object):
|
||||
for path, dirs, files in os.walk(base):
|
||||
# prune ie skip updater folder
|
||||
dirs[:] = [d for d in dirs if os.path.join(path,d) not in [self._updater_path]]
|
||||
|
||||
for directory in dirs:
|
||||
shutil.rmtree(os.path.join(path,directory))
|
||||
|
||||
for file in files:
|
||||
for ptrn in self.remove_pre_update_patterns:
|
||||
if fnmatch.filter([file],ptrn):
|
||||
try:
|
||||
fl = os.path.join(path,file)
|
||||
os.remove(fl)
|
||||
@ -1701,7 +1703,7 @@ class GitlabEngine(object):
|
||||
def parse_tags(self, response, updater):
|
||||
if response == None:
|
||||
return []
|
||||
return [{"name": tag["name"], "zipball_url": self.get_zip_url(tag["commit"]["id"], updater)} for tag in response]
|
||||
return [{"name": tag["name"], "zipball_url": f"https://gitlab.com/slumber/multi-user/-/jobs/artifacts/{tag['name']}/download?job=build"} for tag in response]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
@ -267,7 +267,7 @@ class addon_updater_update_now(bpy.types.Operator):
|
||||
clean_install: bpy.props.BoolProperty(
|
||||
name="Clean install",
|
||||
description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install",
|
||||
default=False,
|
||||
default=True,
|
||||
options={'HIDDEN'}
|
||||
)
|
||||
|
||||
|
@ -43,7 +43,7 @@ __all__ = [
|
||||
"bl_particle",
|
||||
] # Order here defines execution order
|
||||
|
||||
if bpy.app.version[1] >= 91:
|
||||
if bpy.app.version >= (2,91,0):
|
||||
__all__.append('bl_volume')
|
||||
|
||||
from . import *
|
||||
|
@ -53,12 +53,12 @@ STROKE = [
|
||||
"uv_translation",
|
||||
"vertex_color_fill",
|
||||
]
|
||||
if bpy.app.version[1] >= 91:
|
||||
if bpy.app.version >= (2,91,0):
|
||||
STROKE.append('use_cyclic')
|
||||
else:
|
||||
STROKE.append('draw_cyclic')
|
||||
|
||||
if bpy.app.version[1] >= 83:
|
||||
if bpy.app.version >= (2,83,0):
|
||||
STROKE_POINT.append('vertex_color')
|
||||
|
||||
def dump_stroke(stroke):
|
||||
|
@ -37,7 +37,7 @@ class BlLightprobe(ReplicatedDatablock):
|
||||
def construct(data: dict) -> object:
|
||||
type = 'CUBE' if data['type'] == 'CUBEMAP' else data['type']
|
||||
# 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)
|
||||
else:
|
||||
logging.warning("Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
|
||||
@ -49,7 +49,7 @@ class BlLightprobe(ReplicatedDatablock):
|
||||
|
||||
@staticmethod
|
||||
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")
|
||||
|
||||
dumper = Dumper()
|
||||
|
@ -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)
|
||||
else:
|
||||
SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str)
|
||||
@ -56,14 +56,24 @@ else:
|
||||
blender 2.92.")
|
||||
|
||||
|
||||
def get_node_group_inputs(node_group):
|
||||
inputs = []
|
||||
def get_node_group_properties_identifiers(node_group):
|
||||
props_ids = []
|
||||
# Inputs
|
||||
for inpt in node_group.inputs:
|
||||
if inpt.type in IGNORED_SOCKETS:
|
||||
continue
|
||||
else:
|
||||
inputs.append(inpt)
|
||||
return inputs
|
||||
props_ids.append((inpt.identifier, inpt.type))
|
||||
|
||||
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]
|
||||
|
||||
|
||||
@ -122,29 +132,35 @@ def load_physics(dumped_settings: dict, target: bpy.types.Object):
|
||||
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
|
||||
|
||||
:arg modifier: geometry node modifier to dump
|
||||
:type modifier: bpy.type.Modifier
|
||||
"""
|
||||
dumped_inputs = []
|
||||
for inpt in get_node_group_inputs(modifier.node_group):
|
||||
input_value = modifier[inpt.identifier]
|
||||
dumped_props = []
|
||||
|
||||
dumped_input = None
|
||||
if isinstance(input_value, bpy.types.ID):
|
||||
dumped_input = input_value.uuid
|
||||
elif isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS):
|
||||
dumped_input = input_value
|
||||
elif hasattr(input_value, 'to_list'):
|
||||
dumped_input = input_value.to_list()
|
||||
dumped_inputs.append(dumped_input)
|
||||
for prop_value, prop_type in get_node_group_properties_identifiers(modifier.node_group):
|
||||
try:
|
||||
prop_value = modifier[prop_value]
|
||||
except KeyError as e:
|
||||
logging.error(f"fail to dump geomety node modifier property : {prop_value} ({e})")
|
||||
else:
|
||||
dump = None
|
||||
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
|
||||
|
||||
: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
|
||||
"""
|
||||
|
||||
for input_index, inpt in enumerate(get_node_group_inputs(target_modifier.node_group)):
|
||||
dumped_value = dumped_modifier['inputs'][input_index]
|
||||
input_value = target_modifier[inpt.identifier]
|
||||
if isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS):
|
||||
target_modifier[inpt.identifier] = dumped_value
|
||||
elif hasattr(input_value, 'to_list'):
|
||||
for input_index, inpt in enumerate(get_node_group_properties_identifiers(target_modifier.node_group)):
|
||||
dumped_value, dumped_type = dumped_modifier['props'][input_index]
|
||||
input_value = target_modifier[inpt[0]]
|
||||
if dumped_type in ['INT', 'VALUE', 'STR', 'BOOL']:
|
||||
logging.info(f"{inpt[0]}/{dumped_value}")
|
||||
target_modifier[inpt[0]] = dumped_value
|
||||
elif dumped_type in ['RGBA', 'VECTOR']:
|
||||
for index in range(len(input_value)):
|
||||
input_value[index] = dumped_value[index]
|
||||
elif inpt.type in ['COLLECTION', 'OBJECT']:
|
||||
target_modifier[inpt.identifier] = get_datablock_from_uuid(
|
||||
dumped_value, None)
|
||||
elif dumped_type in ['COLLECTION', 'OBJECT', 'IMAGE', 'TEXTURE', 'MATERIAL']:
|
||||
target_modifier[inpt[0]] = get_datablock_from_uuid(dumped_value, None)
|
||||
|
||||
|
||||
def load_pose(target_bone, data):
|
||||
@ -198,12 +214,12 @@ def find_data_from_name(name=None):
|
||||
instance = bpy.data.speakers[name]
|
||||
elif name in bpy.data.lightprobes.keys():
|
||||
# Only supported since 2.83
|
||||
if bpy.app.version[1] >= 83:
|
||||
if bpy.app.version >= (2,83,0):
|
||||
instance = bpy.data.lightprobes[name]
|
||||
else:
|
||||
logging.warning(
|
||||
"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
|
||||
instance = bpy.data.volumes[name]
|
||||
return instance
|
||||
@ -250,10 +266,11 @@ def find_geometry_nodes_dependencies(modifiers: bpy.types.bpy_prop_collection) -
|
||||
for mod in modifiers:
|
||||
if mod.type == 'NODES' and mod.node_group:
|
||||
dependencies.append(mod.node_group)
|
||||
# for inpt in get_node_group_inputs(mod.node_group):
|
||||
# parameter = mod.get(inpt.identifier)
|
||||
# if parameter and isinstance(parameter, bpy.types.ID):
|
||||
# dependencies.append(parameter)
|
||||
for inpt, inpt_type in get_node_group_properties_identifiers(mod.node_group):
|
||||
inpt_value = mod.get(inpt)
|
||||
# Avoid to handle 'COLLECTION', 'OBJECT' to avoid circular dependencies
|
||||
if inpt_type in ['IMAGE', 'TEXTURE', 'MATERIAL'] and inpt_value:
|
||||
dependencies.append(inpt_value)
|
||||
|
||||
return dependencies
|
||||
|
||||
@ -387,10 +404,7 @@ def dump_modifiers(modifiers: bpy.types.bpy_prop_collection)->dict:
|
||||
dumped_modifier = dumper.dump(modifier)
|
||||
# hack to dump geometry nodes inputs
|
||||
if modifier.type == 'NODES':
|
||||
dumped_inputs = dump_modifier_geometry_node_inputs(
|
||||
modifier)
|
||||
dumped_modifier['inputs'] = dumped_inputs
|
||||
|
||||
dumped_modifier['props'] = dump_modifier_geometry_node_props(modifier)
|
||||
elif modifier.type == 'PARTICLE_SYSTEM':
|
||||
dumper.exclude_filter = [
|
||||
"is_edited",
|
||||
@ -455,7 +469,7 @@ def load_modifiers(dumped_modifiers: list, modifiers: bpy.types.bpy_prop_collect
|
||||
loader.load(loaded_modifier, dumped_modifier)
|
||||
|
||||
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':
|
||||
default = loaded_modifier.particle_system.settings
|
||||
dumped_particles = dumped_modifier['particle_system']
|
||||
@ -595,6 +609,13 @@ class BlObject(ReplicatedDatablock):
|
||||
if datablock.data is None and img_uuid:
|
||||
datablock.data = get_datablock_from_uuid(img_uuid, None)
|
||||
|
||||
if hasattr(datablock, 'cycles_visibility') \
|
||||
and 'cycles_visibility' in data:
|
||||
loader.load(datablock.cycles_visibility, data['cycles_visibility'])
|
||||
|
||||
if hasattr(datablock, 'modifiers'):
|
||||
load_modifiers(data['modifiers'], datablock.modifiers)
|
||||
|
||||
if hasattr(object_data, 'skin_vertices') \
|
||||
and object_data.skin_vertices\
|
||||
and 'skin_vertices' in data:
|
||||
@ -604,13 +625,6 @@ class BlObject(ReplicatedDatablock):
|
||||
skin_data.data,
|
||||
SKIN_DATA)
|
||||
|
||||
if hasattr(datablock, 'cycles_visibility') \
|
||||
and 'cycles_visibility' in data:
|
||||
loader.load(datablock.cycles_visibility, data['cycles_visibility'])
|
||||
|
||||
if hasattr(datablock, 'modifiers'):
|
||||
load_modifiers(data['modifiers'], datablock.modifiers)
|
||||
|
||||
constraints = data.get('constraints')
|
||||
if constraints:
|
||||
load_constraints(constraints, datablock.constraints)
|
||||
|
@ -440,7 +440,7 @@ class BlScene(ReplicatedDatablock):
|
||||
if seq.name not in sequences:
|
||||
vse.sequences.remove(seq)
|
||||
# Load existing sequences
|
||||
for seq_data in sequences.value():
|
||||
for seq_data in sequences.values():
|
||||
load_sequence(seq_data, vse)
|
||||
# If the sequence is no longer used, clear it
|
||||
elif datablock.sequence_editor and not sequences:
|
||||
|
@ -26,7 +26,8 @@ import numpy as np
|
||||
BPY_TO_NUMPY_TYPES = {
|
||||
'FLOAT': np.float32,
|
||||
'INT': np.int32,
|
||||
'BOOL': np.bool}
|
||||
'BOOL': np.bool,
|
||||
'BOOLEAN': np.bool}
|
||||
|
||||
PRIMITIVE_TYPES = ['FLOAT', 'INT', 'BOOLEAN']
|
||||
|
||||
|
@ -134,7 +134,7 @@ def install_modules(dependencies: list, python_path: str, install_dir: str):
|
||||
module_can_be_imported(package_name)
|
||||
|
||||
def register():
|
||||
if bpy.app.version[1] >= 91:
|
||||
if bpy.app.version >= (2,91,0):
|
||||
python_binary_path = sys.executable
|
||||
else:
|
||||
python_binary_path = bpy.app.binary_path_python
|
||||
|
@ -23,6 +23,7 @@ from replication import porcelain
|
||||
from replication.constants import RP_COMMON, STATE_ACTIVE, STATE_SYNCING, UP
|
||||
from replication.exception import ContextError, NonAuthorizedOperationError
|
||||
from replication.interface import session
|
||||
from .timers import XrUserUpdate
|
||||
|
||||
from . import shared_data, utils
|
||||
|
||||
@ -46,14 +47,16 @@ def sanitize_deps_graph(remove_nodes: bool = False):
|
||||
rm_cpt += 1
|
||||
except NonAuthorizedOperationError:
|
||||
continue
|
||||
logging.info(f"Sanitize took { utils.current_milli_time()-start} ms, removed {rm_cpt} nodes")
|
||||
logging.info(
|
||||
f"Sanitize took { utils.current_milli_time()-start} ms, removed {rm_cpt} nodes")
|
||||
|
||||
|
||||
def update_external_dependencies():
|
||||
"""Force external dependencies(files such as images) evaluation
|
||||
"""
|
||||
external_types = ['WindowsPath', 'PosixPath', 'Image']
|
||||
nodes_ids = [n.uuid for n in session.repository.graph.values() if n.data['type_id'] in external_types]
|
||||
nodes_ids = [n.uuid for n in session.repository.graph.values()
|
||||
if n.data['type_id'] in external_types]
|
||||
for node_id in nodes_ids:
|
||||
node = session.repository.graph.get(node_id)
|
||||
if node and node.owner in [session.repository.username, RP_COMMON]:
|
||||
@ -72,11 +75,13 @@ def on_scene_update(scene):
|
||||
settings = utils.get_preferences()
|
||||
incoming_updates = shared_data.session.applied_updates
|
||||
|
||||
distant_update = [getattr(u.id, 'uuid', None) for u in dependency_updates if getattr(u.id, 'uuid', None) in incoming_updates]
|
||||
distant_update = [getattr(u.id, 'uuid', None) for u in dependency_updates if getattr(
|
||||
u.id, 'uuid', None) in incoming_updates]
|
||||
if distant_update:
|
||||
for u in distant_update:
|
||||
shared_data.session.applied_updates.remove(u)
|
||||
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
|
||||
|
||||
# NOTE: maybe we don't need to check each update but only the first
|
||||
@ -84,7 +89,8 @@ def on_scene_update(scene):
|
||||
update_uuid = getattr(update.id, 'uuid', None)
|
||||
if update_uuid:
|
||||
node = session.repository.graph.get(update.id.uuid)
|
||||
check_common = session.repository.rdp.get_implementation(update.id).bl_check_common
|
||||
check_common = session.repository.rdp.get_implementation(
|
||||
update.id).bl_check_common
|
||||
|
||||
if node and (node.owner == session.repository.username or check_common):
|
||||
logging.debug(f"Evaluate {update.id.name}")
|
||||
@ -107,12 +113,14 @@ def on_scene_update(scene):
|
||||
porcelain.commit(session.repository, 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))]
|
||||
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
|
||||
def resolve_deps_graph(dummy):
|
||||
"""Resolve deps graph
|
||||
@ -138,6 +146,13 @@ def update_client_frame(scene):
|
||||
'frame_current': scene.frame_current
|
||||
})
|
||||
|
||||
@persistent
|
||||
def xr_user_update(scene):
|
||||
if session and session.state == STATE_ACTIVE:
|
||||
xr_timer = XrUserUpdate()
|
||||
xr_timer.register()
|
||||
logging.info("XR Session timer started")
|
||||
|
||||
|
||||
def register():
|
||||
bpy.app.handlers.undo_post.append(resolve_deps_graph)
|
||||
@ -146,6 +161,8 @@ def register():
|
||||
bpy.app.handlers.load_pre.append(load_pre_handler)
|
||||
bpy.app.handlers.frame_change_pre.append(update_client_frame)
|
||||
|
||||
bpy.app.handlers.xr_session_start_pre.append(xr_user_update)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.app.handlers.undo_post.remove(resolve_deps_graph)
|
||||
@ -153,3 +170,5 @@ def unregister():
|
||||
|
||||
bpy.app.handlers.load_pre.remove(load_pre_handler)
|
||||
bpy.app.handlers.frame_change_pre.remove(update_client_frame)
|
||||
|
||||
bpy.app.handlers.xr_session_start_pre.remove(xr_user_update)
|
||||
|
Submodule multi_user/libs/replication updated: d69f259046...d722cf0d65
@ -37,6 +37,7 @@ from queue import Queue
|
||||
from time import gmtime, strftime
|
||||
|
||||
from bpy.props import FloatProperty
|
||||
import bmesh
|
||||
|
||||
try:
|
||||
import _pickle as pickle
|
||||
@ -58,13 +59,127 @@ from replication.repository import Repository
|
||||
|
||||
from . import bl_types, environment, shared_data, timers, ui, utils
|
||||
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
|
||||
|
||||
background_execution_queue = Queue()
|
||||
deleyables = []
|
||||
stop_modal_executor = False
|
||||
|
||||
|
||||
def draw_user(username, metadata, radius=0.01, intensity=10.0):
|
||||
# TODO: Draw camera model from viewmatrix
|
||||
view_corners = metadata.get('view_corners')
|
||||
color = metadata.get('color', (1,1,1,0))
|
||||
objects = metadata.get('selected_objects', None)
|
||||
|
||||
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_mesh.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.context.scene.collection.children.link(user_collection)
|
||||
|
||||
|
||||
def session_callback(name):
|
||||
""" Session callback wrapper
|
||||
|
||||
@ -238,7 +353,7 @@ class SessionConnectOperator(bpy.types.Operator):
|
||||
settings.generate_supported_types()
|
||||
|
||||
|
||||
if bpy.app.version[1] >= 91:
|
||||
if bpy.app.version >= (2,91,0):
|
||||
python_binary_path = sys.executable
|
||||
else:
|
||||
python_binary_path = bpy.app.binary_path_python
|
||||
@ -309,7 +424,7 @@ class SessionHostOperator(bpy.types.Operator):
|
||||
settings.generate_supported_types()
|
||||
|
||||
|
||||
if bpy.app.version[1] >= 91:
|
||||
if bpy.app.version >= (2,91,0):
|
||||
python_binary_path = sys.executable
|
||||
else:
|
||||
python_binary_path = bpy.app.binary_path_python
|
||||
@ -863,6 +978,25 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
|
||||
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,
|
||||
)
|
||||
user_skin_radius: bpy.props.FloatProperty(
|
||||
name="Wireframe radius",
|
||||
description="Wireframe radius",
|
||||
default=0.005,
|
||||
)
|
||||
user_color_intensity: bpy.props.FloatProperty(
|
||||
name="Shading intensity",
|
||||
description="Shading intensity",
|
||||
default=10.0,
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
|
||||
def execute(self, context):
|
||||
from replication.repository import Repository
|
||||
|
||||
@ -885,6 +1019,17 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
|
||||
for node in nodes:
|
||||
porcelain.apply(repo, node.uuid)
|
||||
|
||||
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)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@ -892,6 +1037,39 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
|
||||
def poll(cls, context):
|
||||
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):
|
||||
"""Add a server to the server list preset"""
|
||||
bl_idname = "session.preset_server_add"
|
||||
@ -1123,6 +1301,7 @@ classes = (
|
||||
SessionNotifyOperator,
|
||||
SessionSaveBackupOperator,
|
||||
SessionLoadSaveOperator,
|
||||
SESSION_PT_ImportUser,
|
||||
SessionStopAutoSaveOperator,
|
||||
SessionPurgeOperator,
|
||||
SessionPresetServerAdd,
|
||||
|
@ -374,9 +374,9 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
||||
description="sidebar_advanced_log_expanded",
|
||||
default=False
|
||||
)
|
||||
sidebar_advanced_hosting_expanded: bpy.props.BoolProperty(
|
||||
name="sidebar_advanced_hosting_expanded",
|
||||
description="sidebar_advanced_hosting_expanded",
|
||||
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(
|
||||
@ -619,6 +619,11 @@ class SessionUser(bpy.types.PropertyGroup):
|
||||
"""
|
||||
username: bpy.props.StringProperty(name="username")
|
||||
current_frame: bpy.props.IntProperty(name="current_frame")
|
||||
color: bpy.props.FloatVectorProperty(name="color", subtype="COLOR",
|
||||
min=0.0,
|
||||
max=1.0,
|
||||
size=4,
|
||||
default=(1.0, 1.0, 1.0, 1.0))
|
||||
|
||||
|
||||
class SessionProps(bpy.types.PropertyGroup):
|
||||
|
@ -26,7 +26,7 @@ import bgl
|
||||
import blf
|
||||
import bpy
|
||||
import gpu
|
||||
import mathutils
|
||||
from mathutils import Vector, Matrix, Quaternion
|
||||
from bpy_extras import view3d_utils
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from replication.constants import (STATE_ACTIVE, STATE_AUTH, STATE_CONFIG,
|
||||
@ -136,7 +136,7 @@ def bbox_from_obj(obj: bpy.types.Object, index: int = 1) -> list:
|
||||
(-radius, +radius, +radius), (+radius, +radius, +radius)]
|
||||
|
||||
base = obj.matrix_world
|
||||
bbox_corners = [base @ mathutils.Vector(corner) for corner in coords]
|
||||
bbox_corners = [base @ Vector(corner) for corner in coords]
|
||||
|
||||
vertex_pos = [(point.x, point.y, point.z) for point in bbox_corners]
|
||||
|
||||
@ -159,39 +159,12 @@ def bbox_from_instance_collection(ic: bpy.types.Object, index: int = 0) -> list:
|
||||
vertex_pos += vertex_pos_temp
|
||||
vertex_indices += vertex_indices_temp
|
||||
|
||||
bbox_corners = [ic.matrix_world @ mathutils.Vector(vertex) for vertex in vertex_pos]
|
||||
bbox_corners = [ic.matrix_world @ Vector(vertex) for vertex in vertex_pos]
|
||||
|
||||
vertex_pos = [(point.x, point.y, point.z) for point in bbox_corners]
|
||||
|
||||
return vertex_pos, vertex_indices
|
||||
|
||||
def generate_user_camera() -> list:
|
||||
""" Generate a basic camera represention of the user point of view
|
||||
|
||||
:return: list of 7 points
|
||||
"""
|
||||
area, region, rv3d = view3d_find()
|
||||
|
||||
v1 = v2 = v3 = v4 = v5 = v6 = v7 = [0, 0, 0]
|
||||
|
||||
if area and region and rv3d:
|
||||
width = region.width
|
||||
height = region.height
|
||||
|
||||
v1 = project_to_viewport(region, rv3d, (0, 0))
|
||||
v3 = project_to_viewport(region, rv3d, (0, height))
|
||||
v2 = project_to_viewport(region, rv3d, (width, height))
|
||||
v4 = project_to_viewport(region, rv3d, (width, 0))
|
||||
|
||||
v5 = project_to_viewport(region, rv3d, (width/2, height/2))
|
||||
v6 = list(rv3d.view_location)
|
||||
v7 = project_to_viewport(
|
||||
region, rv3d, (width/2, height/2), distance=-.8)
|
||||
|
||||
coords = [v1, v2, v3, v4, v5, v6, v7]
|
||||
|
||||
return coords
|
||||
|
||||
|
||||
def project_to_screen(coords: list) -> list:
|
||||
""" Project 3D coordinate to 2D screen coordinates
|
||||
@ -219,10 +192,10 @@ def get_bb_coords_from_obj(object: bpy.types.Object, instance: bpy.types.Object
|
||||
base = object.matrix_world
|
||||
|
||||
if instance:
|
||||
scale = mathutils.Matrix.Diagonal(object.matrix_world.to_scale())
|
||||
scale = Matrix.Diagonal(object.matrix_world.to_scale())
|
||||
base = instance.matrix_world @ scale.to_4x4()
|
||||
|
||||
bbox_corners = [base @ mathutils.Vector(
|
||||
bbox_corners = [base @ Vector(
|
||||
corner) for corner in object.bound_box]
|
||||
|
||||
|
||||
@ -267,9 +240,14 @@ class Widget(object):
|
||||
|
||||
class UserFrustumWidget(Widget):
|
||||
# Camera widget indices
|
||||
indices = ((1, 3), (2, 1), (3, 0),
|
||||
(2, 0), (4, 5), (1, 6),
|
||||
(2, 6), (3, 6), (0, 6))
|
||||
camera_vertex = ((0, 0, 1),
|
||||
(-1, -0.5, -1), (1, -0.5, -1), (1, 0.5, -1), (-1, 0.5, -1),
|
||||
(0, 1, -1),
|
||||
(-0.5, 0.6, -1), (0.5, 0.6, -1))
|
||||
|
||||
camera_indices = ((0, 1), (0, 2), (0, 3), (0, 4),
|
||||
(1, 2), (2, 3), (3, 4), (4, 1),
|
||||
(5, 6), (6, 7), (7, 5))
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -290,27 +268,33 @@ class UserFrustumWidget(Widget):
|
||||
return False
|
||||
|
||||
scene_current = self.data.get('scene_current')
|
||||
view_corners = self.data.get('view_corners')
|
||||
view_matrix = self.data.get('view_matrix')
|
||||
|
||||
return (scene_current == bpy.context.scene.name or
|
||||
self.settings.presence_show_far_user) and \
|
||||
view_corners and \
|
||||
view_matrix and \
|
||||
self.settings.presence_show_user and \
|
||||
self.settings.enable_presence
|
||||
|
||||
def draw(self):
|
||||
location = self.data.get('view_corners')
|
||||
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
|
||||
positions = [tuple(coord) for coord in location]
|
||||
xr_state = self.data.get('xr')
|
||||
transformation = Matrix()
|
||||
if xr_state:
|
||||
loc = Vector(xr_state.get('viewer_pose_location'))
|
||||
rot = Quaternion(xr_state.get('viewer_pose_rotation'))
|
||||
scale = Vector((1,1,1))
|
||||
transformation = Matrix.LocRotScale(loc, rot, scale)
|
||||
else:
|
||||
transformation = Matrix(self.data.get('view_matrix')).inverted()
|
||||
|
||||
if len(positions) != 7:
|
||||
return
|
||||
coords = [transformation @ Vector(vertex) for vertex in self.camera_vertex]
|
||||
|
||||
batch = batch_for_shader(
|
||||
shader,
|
||||
'LINES',
|
||||
{"pos": positions},
|
||||
indices=self.indices)
|
||||
{"pos": coords},
|
||||
indices=self.camera_indices)
|
||||
|
||||
shader.bind()
|
||||
shader.uniform_float("color", self.data.get('color'))
|
||||
@ -405,19 +389,24 @@ class UserNameWidget(Widget):
|
||||
return False
|
||||
|
||||
scene_current = self.data.get('scene_current')
|
||||
view_corners = self.data.get('view_corners')
|
||||
view_matrix = self.data.get('view_matrix')
|
||||
|
||||
return (scene_current == bpy.context.scene.name or
|
||||
self.settings.presence_show_far_user) and \
|
||||
view_corners and \
|
||||
view_matrix and \
|
||||
self.settings.presence_show_user and \
|
||||
self.settings.enable_presence
|
||||
|
||||
def draw(self):
|
||||
view_corners = self.data.get('view_corners')
|
||||
xr_state = self.data.get('xr')
|
||||
|
||||
if xr_state:
|
||||
position = xr_state.get('viewer_pose_location', [0,0,0])
|
||||
else:
|
||||
position = Matrix(self.data.get('view_matrix')).inverted().to_translation()
|
||||
|
||||
color = self.data.get('color')
|
||||
position = [tuple(coord) for coord in view_corners]
|
||||
coords = project_to_screen(position[1])
|
||||
coords = project_to_screen(position)
|
||||
|
||||
if coords:
|
||||
blf.position(0, coords[0], coords[1]+10, 0)
|
||||
|
@ -28,8 +28,7 @@ from replication import porcelain
|
||||
|
||||
from . import operators, utils
|
||||
from .presence import (UserFrustumWidget, UserNameWidget, UserModeWidget, UserSelectionWidget,
|
||||
generate_user_camera, get_view_matrix, refresh_3d_view,
|
||||
refresh_sidebar_view, renderer)
|
||||
get_view_matrix, refresh_3d_view, refresh_sidebar_view, renderer)
|
||||
|
||||
from . import shared_data
|
||||
|
||||
@ -203,6 +202,7 @@ class DynamicRightSelectTimer(Timer):
|
||||
|
||||
for node_id in to_lock:
|
||||
node = session.repository.graph.get(node_id)
|
||||
if node and hasattr(node,'data'):
|
||||
instance_mode = node.data.get('instance_type')
|
||||
if instance_mode and instance_mode == 'COLLECTION':
|
||||
to_lock.remove(node_id)
|
||||
@ -275,21 +275,24 @@ class ClientUpdate(Timer):
|
||||
|
||||
if session and renderer:
|
||||
if session.state in [STATE_ACTIVE, STATE_LOBBY]:
|
||||
local_user = session.online_users.get(
|
||||
settings.username)
|
||||
|
||||
local_user = session.online_users.get(settings.username)
|
||||
xr_session_state = bpy.context.window_manager.xr_session_state
|
||||
if not local_user:
|
||||
return
|
||||
else:
|
||||
for username, user_data in session.online_users.items():
|
||||
if username != settings.username:
|
||||
cached_user_data = self.users_metadata.get(
|
||||
username)
|
||||
cached_user_data = self.users_metadata.get(username)
|
||||
new_user_data = session.online_users[username]['metadata']
|
||||
|
||||
if cached_user_data is None:
|
||||
self.users_metadata[username] = user_data['metadata']
|
||||
elif 'view_matrix' in cached_user_data and 'view_matrix' in new_user_data and cached_user_data['view_matrix'] != new_user_data['view_matrix']:
|
||||
elif 'view_matrix' in cached_user_data and \
|
||||
'view_matrix' in new_user_data and \
|
||||
cached_user_data['view_matrix'] != new_user_data['view_matrix'] or \
|
||||
'xr' in cached_user_data and \
|
||||
'xr' in new_user_data and \
|
||||
cached_user_data['xr']['viewer_pose_location'] != new_user_data['xr']['viewer_pose_location']:
|
||||
refresh_3d_view()
|
||||
self.users_metadata[username] = user_data['metadata']
|
||||
break
|
||||
@ -299,13 +302,12 @@ class ClientUpdate(Timer):
|
||||
local_user_metadata = local_user.get('metadata')
|
||||
scene_current = bpy.context.scene.name
|
||||
local_user = session.online_users.get(settings.username)
|
||||
current_view_corners = generate_user_camera()
|
||||
current_view_matrix = get_view_matrix()
|
||||
|
||||
# Init client metadata
|
||||
if not local_user_metadata or 'color' not in local_user_metadata.keys():
|
||||
metadata = {
|
||||
'view_corners': get_view_matrix(),
|
||||
'view_matrix': get_view_matrix(),
|
||||
'view_matrix': current_view_matrix,
|
||||
'color': (settings.client_color.r,
|
||||
settings.client_color.g,
|
||||
settings.client_color.b,
|
||||
@ -321,10 +323,8 @@ class ClientUpdate(Timer):
|
||||
elif scene_current != local_user_metadata['scene_current']:
|
||||
local_user_metadata['scene_current'] = scene_current
|
||||
porcelain.update_user_metadata(session.repository, local_user_metadata)
|
||||
elif 'view_corners' in local_user_metadata and current_view_corners != local_user_metadata['view_corners']:
|
||||
local_user_metadata['view_corners'] = current_view_corners
|
||||
local_user_metadata['view_matrix'] = get_view_matrix(
|
||||
)
|
||||
elif 'view_matrix' in local_user_metadata and current_view_matrix != local_user_metadata['view_matrix']:
|
||||
local_user_metadata['view_matrix'] = current_view_matrix
|
||||
porcelain.update_user_metadata(session.repository, local_user_metadata)
|
||||
elif bpy.context.mode != local_user_metadata['mode_current']:
|
||||
local_user_metadata['mode_current'] = bpy.context.mode
|
||||
@ -386,3 +386,28 @@ class MainThreadExecutor(Timer):
|
||||
function, kwargs = self.execution_queue.get()
|
||||
logging.debug(f"Executing {function.__name__}")
|
||||
function(**kwargs)
|
||||
|
||||
class XrUserUpdate(Timer):
|
||||
def __init__(self, timeout=.01):
|
||||
# TODO: Add user refresh rate settings
|
||||
super().__init__(timeout)
|
||||
|
||||
def execute(self):
|
||||
xr_session_state = bpy.context.window_manager.xr_session_state
|
||||
|
||||
if xr_session_state and xr_session_state.is_running:
|
||||
# Update user state
|
||||
|
||||
porcelain.update_user_metadata(
|
||||
session.repository,
|
||||
{'xr': {
|
||||
'viewer_pose_location': list(xr_session_state.viewer_pose_location),
|
||||
'viewer_pose_rotation': list(xr_session_state.viewer_pose_rotation),
|
||||
'controller_0_location': list(xr_session_state.controller_grip_location_get(bpy.context, 0)),
|
||||
'controller_0_rotation': list(xr_session_state.controller_grip_rotation_get(bpy.context, 0)),
|
||||
'controller_1_location': list(xr_session_state.controller_grip_location_get(bpy.context, 1)),
|
||||
'controller_1_rotation': list(xr_session_state.controller_grip_rotation_get(bpy.context, 1))}
|
||||
})
|
||||
else:
|
||||
logging.info("XR Session ended, stopping user update")
|
||||
self.unregister()
|
247
multi_user/ui.py
247
multi_user/ui.py
@ -62,7 +62,41 @@ def printProgressBar(iteration, total, prefix='', suffix='', decimals=1, length=
|
||||
bar = fill * filledLength + fill_empty * (length - filledLength)
|
||||
return f"{prefix} |{bar}| {iteration}/{total}{suffix}"
|
||||
|
||||
|
||||
def get_mode_icon(mode_name: str) -> str:
|
||||
""" given a mode name retrieve a built-in icon
|
||||
"""
|
||||
mode_icon = "NONE"
|
||||
if mode_name == "OBJECT" :
|
||||
mode_icon = "OBJECT_DATAMODE"
|
||||
elif mode_name == "EDIT_MESH" :
|
||||
mode_icon = "EDITMODE_HLT"
|
||||
elif mode_name == 'EDIT_CURVE':
|
||||
mode_icon = "CURVE_DATA"
|
||||
elif mode_name == 'EDIT_SURFACE':
|
||||
mode_icon = "SURFACE_DATA"
|
||||
elif mode_name == 'EDIT_TEXT':
|
||||
mode_icon = "FILE_FONT"
|
||||
elif mode_name == 'EDIT_ARMATURE':
|
||||
mode_icon = "ARMATURE_DATA"
|
||||
elif mode_name == 'EDIT_METABALL':
|
||||
mode_icon = "META_BALL"
|
||||
elif mode_name == 'EDIT_LATTICE':
|
||||
mode_icon = "LATTICE_DATA"
|
||||
elif mode_name == 'POSE':
|
||||
mode_icon = "POSE_HLT"
|
||||
elif mode_name == 'SCULPT':
|
||||
mode_icon = "SCULPTMODE_HLT"
|
||||
elif mode_name == 'PAINT_WEIGHT':
|
||||
mode_icon = "WPAINT_HLT"
|
||||
elif mode_name == 'PAINT_VERTEX':
|
||||
mode_icon = "VPAINT_HLT"
|
||||
elif mode_name == 'PAINT_TEXTURE':
|
||||
mode_icon = "TPAINT_HLT"
|
||||
elif mode_name == 'PARTICLE':
|
||||
mode_icon = "PARTICLES"
|
||||
elif mode_name == 'PAINT_GPENCIL' or mode_name =='EDIT_GPENCIL' or mode_name =='SCULPT_GPENCIL' or mode_name =='WEIGHT_GPENCIL' or mode_name =='VERTEX_GPENCIL':
|
||||
mode_icon = "GREASEPENCIL"
|
||||
return mode_icon
|
||||
class SESSION_PT_settings(bpy.types.Panel):
|
||||
"""Settings panel"""
|
||||
bl_idname = "MULTIUSER_SETTINGS_PT_panel"
|
||||
@ -149,10 +183,8 @@ class SESSION_PT_settings(bpy.types.Panel):
|
||||
col.template_list("SESSION_UL_network", "", settings, "server_preset", context.window_manager, "server_index")
|
||||
col.separator()
|
||||
connectOp = col.row()
|
||||
connectOp.operator("session.host", text="Host")
|
||||
connectopcol = connectOp.column()
|
||||
connectopcol.enabled =is_server_selected
|
||||
connectopcol.operator("session.connect", text="Connect")
|
||||
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..)
|
||||
@ -173,11 +205,17 @@ class SESSION_PT_settings(bpy.types.Panel):
|
||||
info_msg = None
|
||||
|
||||
if current_state == STATE_LOBBY:
|
||||
usr = session.online_users.get(settings.username)
|
||||
row= layout.row()
|
||||
info_msg = "Waiting for the session to start."
|
||||
|
||||
if info_msg:
|
||||
info_box = row.box()
|
||||
if usr and usr['admin']:
|
||||
info_msg = "Init the session to start."
|
||||
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')
|
||||
|
||||
# PROGRESS BAR
|
||||
@ -192,10 +230,57 @@ class SESSION_PT_settings(bpy.types.Panel):
|
||||
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):
|
||||
bl_idname = "MULTIUSER_SETTINGS_REPLICATION_PT_panel"
|
||||
bl_label = "Advanced"
|
||||
bl_label = "General Settings"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
|
||||
@ -216,30 +301,19 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
|
||||
layout = self.layout
|
||||
settings = get_preferences()
|
||||
|
||||
#ADVANCED HOST
|
||||
host_selection = layout.row().box()
|
||||
host_selection.prop(
|
||||
settings, "sidebar_advanced_hosting_expanded", text="Hosting",
|
||||
icon=get_expanded_icon(settings.sidebar_advanced_hosting_expanded),
|
||||
#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_hosting_expanded:
|
||||
host_selection_row = host_selection.row()
|
||||
host_selection_row.prop(settings, "host_port", text="Port:")
|
||||
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_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="")
|
||||
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()
|
||||
@ -335,18 +409,31 @@ class SESSION_PT_user(bpy.types.Panel):
|
||||
online_users)-1 >= selected_user else 0
|
||||
|
||||
#USER LIST
|
||||
row = layout.row()
|
||||
box = row.box()
|
||||
split = box.split(factor=0.35)
|
||||
split.label(text="user")
|
||||
split = split.split(factor=0.3)
|
||||
split.label(text="mode")
|
||||
split.label(text="frame")
|
||||
split.label(text="location")
|
||||
split.label(text="ping")
|
||||
col = layout.column(align=True)
|
||||
row = col.row(align=True)
|
||||
row = row.split(factor=0.35, align=True)
|
||||
|
||||
row = layout.row()
|
||||
layout.template_list("SESSION_UL_users", "", context.window_manager,
|
||||
box = row.box()
|
||||
brow = box.row(align=True)
|
||||
brow.label(text="user")
|
||||
|
||||
row = row.split(factor=0.25, align=True)
|
||||
|
||||
box = row.box()
|
||||
brow = box.row(align=True)
|
||||
brow.label(text="mode")
|
||||
box = row.box()
|
||||
brow = box.row(align=True)
|
||||
brow.label(text="frame")
|
||||
box = row.box()
|
||||
brow = box.row(align=True)
|
||||
brow.label(text="scene")
|
||||
box = row.box()
|
||||
brow = box.row(align=True)
|
||||
brow.label(text="ping")
|
||||
|
||||
row = col.row(align=True)
|
||||
row.template_list("SESSION_UL_users", "", context.window_manager,
|
||||
"online_users", context.window_manager, "user_index")
|
||||
|
||||
#OPERATOR ON USER
|
||||
@ -393,45 +480,32 @@ class SESSION_UL_users(bpy.types.UIList):
|
||||
frame_current = str(metadata.get('frame_current','-'))
|
||||
scene_current = metadata.get('scene_current','-')
|
||||
mode_current = metadata.get('mode_current','-')
|
||||
if mode_current == "OBJECT" :
|
||||
mode_icon = "OBJECT_DATAMODE"
|
||||
elif mode_current == "EDIT_MESH" :
|
||||
mode_icon = "EDITMODE_HLT"
|
||||
elif mode_current == 'EDIT_CURVE':
|
||||
mode_icon = "CURVE_DATA"
|
||||
elif mode_current == 'EDIT_SURFACE':
|
||||
mode_icon = "SURFACE_DATA"
|
||||
elif mode_current == 'EDIT_TEXT':
|
||||
mode_icon = "FILE_FONT"
|
||||
elif mode_current == 'EDIT_ARMATURE':
|
||||
mode_icon = "ARMATURE_DATA"
|
||||
elif mode_current == 'EDIT_METABALL':
|
||||
mode_icon = "META_BALL"
|
||||
elif mode_current == 'EDIT_LATTICE':
|
||||
mode_icon = "LATTICE_DATA"
|
||||
elif mode_current == 'POSE':
|
||||
mode_icon = "POSE_HLT"
|
||||
elif mode_current == 'SCULPT':
|
||||
mode_icon = "SCULPTMODE_HLT"
|
||||
elif mode_current == 'PAINT_WEIGHT':
|
||||
mode_icon = "WPAINT_HLT"
|
||||
elif mode_current == 'PAINT_VERTEX':
|
||||
mode_icon = "VPAINT_HLT"
|
||||
elif mode_current == 'PAINT_TEXTURE':
|
||||
mode_icon = "TPAINT_HLT"
|
||||
elif mode_current == 'PARTICLE':
|
||||
mode_icon = "PARTICLES"
|
||||
elif mode_current == 'PAINT_GPENCIL' or mode_current =='EDIT_GPENCIL' or mode_current =='SCULPT_GPENCIL' or mode_current =='WEIGHT_GPENCIL' or mode_current =='VERTEX_GPENCIL':
|
||||
mode_icon = "GREASEPENCIL"
|
||||
mode_current = metadata.get('mode_current','-')
|
||||
mode_icon = get_mode_icon(mode_current)
|
||||
user_color = metadata.get('color',[1.0,1.0,1.0,1.0])
|
||||
item.color = user_color
|
||||
if user['admin']:
|
||||
status_icon = 'FAKE_USER_ON'
|
||||
split = layout.split(factor=0.35)
|
||||
split.label(text=item.username, icon=status_icon)
|
||||
split = split.split(factor=0.3)
|
||||
split.label(icon=mode_icon)
|
||||
split.label(text=frame_current)
|
||||
split.label(text=scene_current)
|
||||
split.label(text=ping)
|
||||
row = layout.split(factor=0.35, align=True)
|
||||
entry = row.row(align=True)
|
||||
entry.scale_x = 0.05
|
||||
entry.enabled = False
|
||||
entry.prop(item, 'color', text="", event=False, full_event=False)
|
||||
entry.enabled = True
|
||||
entry.scale_x = 1.0
|
||||
entry.label(icon=status_icon, text="")
|
||||
entry.label(text=item.username)
|
||||
|
||||
row = row.split(factor=0.25, align=True)
|
||||
|
||||
entry = row.row()
|
||||
entry.label(icon=mode_icon)
|
||||
entry = row.row()
|
||||
entry.label(text=frame_current)
|
||||
entry = row.row()
|
||||
entry.label(text=scene_current)
|
||||
entry = row.row()
|
||||
entry.label(text=ping)
|
||||
|
||||
def draw_property(context, parent, property_uuid, level=0):
|
||||
settings = get_preferences()
|
||||
@ -543,8 +617,7 @@ class SESSION_PT_repository(bpy.types.Panel):
|
||||
admin = usr['admin']
|
||||
return hasattr(context.window_manager, 'session') and \
|
||||
session and \
|
||||
(session.state == STATE_ACTIVE or \
|
||||
session.state == STATE_LOBBY and admin) and \
|
||||
session.state == STATE_ACTIVE and \
|
||||
not settings.sidebar_repository_shown
|
||||
|
||||
def draw_header(self, context):
|
||||
@ -590,12 +663,6 @@ class SESSION_PT_repository(bpy.types.Panel):
|
||||
else:
|
||||
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):
|
||||
bl_space_type = 'VIEW_3D'
|
||||
@ -614,6 +681,9 @@ class VIEW3D_PT_overlay_session(bpy.types.Panel):
|
||||
pref = get_preferences()
|
||||
layout.active = settings.enable_presence
|
||||
|
||||
row = layout.row()
|
||||
row.prop(settings, "enable_presence",text="Presence Overlay")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(settings, "presence_show_selected",text="Selected Objects")
|
||||
|
||||
@ -667,6 +737,7 @@ classes = (
|
||||
SESSION_UL_users,
|
||||
SESSION_UL_network,
|
||||
SESSION_PT_settings,
|
||||
SESSION_PT_host_settings,
|
||||
SESSION_PT_advanced_settings,
|
||||
SESSION_PT_user,
|
||||
SESSION_PT_sync,
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Download base image debian jessie
|
||||
FROM python:slim
|
||||
|
||||
ARG replication_version=0.1.13
|
||||
ARG replication_version=0.9.1
|
||||
ARG version=0.1.1
|
||||
|
||||
# Infos
|
||||
@ -22,4 +22,4 @@ RUN pip install replication==$replication_version
|
||||
|
||||
# Run the server with parameters
|
||||
ENTRYPOINT ["/bin/sh", "-c"]
|
||||
CMD ["python3 -m replication.server -pwd ${password} -p ${port} -t ${timeout} -l ${log_level} -lf ${log_file}"]
|
||||
CMD ["replication.server -apwd ${password} -spwd '' -p ${port} -t ${timeout} -l ${log_level} -lf ${log_file}"]
|
@ -7,7 +7,7 @@ import bpy
|
||||
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'])
|
||||
def test_lightprobes(clear_blend, lightprobe_type):
|
||||
bpy.ops.object.lightprobe_add(type=lightprobe_type)
|
||||
|
Reference in New Issue
Block a user