Compare commits
39 Commits
194-smooth
...
214-animat
Author | SHA1 | Date | |
---|---|---|---|
d433e8f241 | |||
963a551a1e | |||
d01a434fb7 | |||
3a5a5fc633 | |||
8926ab44e1 | |||
a8f96581c5 | |||
440a4cc1cd | |||
a207c51973 | |||
e706c8e0bf | |||
e590e896da | |||
4140b62a8e | |||
6d9c9c4532 | |||
e9e1911840 | |||
ab350ca7bc | |||
0a8f0b5f88 | |||
2238a15c11 | |||
de73f022e6 | |||
f517205647 | |||
f33c3d8481 | |||
71c69000ec | |||
de1e684b3c | |||
d87730cffb | |||
3f005b86ab | |||
5098e5135d | |||
37cfed489c | |||
9003abcd18 | |||
a199e0df00 | |||
3774419b7e | |||
3e552cb406 | |||
9f381b44c8 | |||
ad795caed5 | |||
504dd77405 | |||
82022c9e4d | |||
d81b4dc014 | |||
63affa079f | |||
fcf5a12dd0 | |||
b0529e4444 | |||
bdfd89c085 | |||
ff1630f9cc |
@ -19,10 +19,10 @@ import sys
|
||||
|
||||
project = 'multi-user'
|
||||
copyright = '2020, Swann Martinez'
|
||||
author = 'Swann Martinez, with contributions from Poochy'
|
||||
author = 'Swann Martinez, Poochy, Fabian'
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = '0.2.0'
|
||||
release = '0.5.0-develop'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
@ -59,6 +59,7 @@ def register():
|
||||
|
||||
from . import presence
|
||||
from . import operators
|
||||
from . import handlers
|
||||
from . import ui
|
||||
from . import preferences
|
||||
from . import addon_updater_ops
|
||||
@ -67,6 +68,7 @@ def register():
|
||||
addon_updater_ops.register(bl_info)
|
||||
presence.register()
|
||||
operators.register()
|
||||
handlers.register()
|
||||
ui.register()
|
||||
except ModuleNotFoundError as e:
|
||||
raise Exception(module_error_msg)
|
||||
@ -87,6 +89,7 @@ def register():
|
||||
def unregister():
|
||||
from . import presence
|
||||
from . import operators
|
||||
from . import handlers
|
||||
from . import ui
|
||||
from . import preferences
|
||||
from . import addon_updater_ops
|
||||
@ -96,6 +99,7 @@ def unregister():
|
||||
presence.unregister()
|
||||
addon_updater_ops.unregister()
|
||||
ui.unregister()
|
||||
handlers.unregister()
|
||||
operators.unregister()
|
||||
preferences.unregister()
|
||||
|
||||
|
@ -219,7 +219,7 @@ def load_fcurve(fcurve_data, fcurve):
|
||||
def dump_animation_data(datablock):
|
||||
animation_data = {}
|
||||
if has_action(datablock):
|
||||
animation_data['action'] = datablock.animation_data.action.name
|
||||
animation_data['action'] = datablock.animation_data.action.uuid
|
||||
if has_driver(datablock):
|
||||
animation_data['drivers'] = []
|
||||
for driver in datablock.animation_data.drivers:
|
||||
@ -241,8 +241,10 @@ def load_animation_data(animation_data, datablock):
|
||||
for driver in animation_data['drivers']:
|
||||
load_driver(datablock, driver)
|
||||
|
||||
if 'action' in animation_data:
|
||||
datablock.animation_data.action = bpy.data.actions[animation_data['action']]
|
||||
action = animation_data.get('action')
|
||||
if action:
|
||||
action = resolve_datablock_from_uuid(action, bpy.data.actions)
|
||||
datablock.animation_data.action = action
|
||||
elif datablock.animation_data.action:
|
||||
datablock.animation_data.action = None
|
||||
|
||||
@ -259,6 +261,8 @@ def resolve_animation_dependencies(datablock):
|
||||
|
||||
|
||||
class BlAction(ReplicatedDatablock):
|
||||
use_delta = True
|
||||
|
||||
bl_id = "actions"
|
||||
bl_class = bpy.types.Action
|
||||
bl_check_common = False
|
||||
|
@ -37,6 +37,8 @@ def get_roll(bone: bpy.types.Bone) -> float:
|
||||
|
||||
|
||||
class BlArmature(ReplicatedDatablock):
|
||||
use_delta = True
|
||||
|
||||
bl_id = "armatures"
|
||||
bl_class = bpy.types.Armature
|
||||
bl_check_common = False
|
||||
|
@ -26,6 +26,8 @@ from .bl_action import dump_animation_data, load_animation_data, resolve_animati
|
||||
|
||||
|
||||
class BlCamera(ReplicatedDatablock):
|
||||
use_delta = True
|
||||
|
||||
bl_id = "cameras"
|
||||
bl_class = bpy.types.Camera
|
||||
bl_check_common = False
|
||||
@ -54,7 +56,7 @@ class BlCamera(ReplicatedDatablock):
|
||||
background_images = data.get('background_images')
|
||||
|
||||
datablock.background_images.clear()
|
||||
|
||||
# TODO: Use image uuid
|
||||
if background_images:
|
||||
for img_name, img_data in background_images.items():
|
||||
img_id = img_data.get('image')
|
||||
|
@ -137,6 +137,8 @@ SPLINE_METADATA = [
|
||||
|
||||
|
||||
class BlCurve(ReplicatedDatablock):
|
||||
use_delta = True
|
||||
|
||||
bl_id = "curves"
|
||||
bl_class = bpy.types.Curve
|
||||
bl_check_common = False
|
||||
|
@ -69,11 +69,12 @@ class BlImage(ReplicatedDatablock):
|
||||
@staticmethod
|
||||
def load(data: dict, datablock: object):
|
||||
loader = Loader()
|
||||
loader.load(data, datablock)
|
||||
loader.load(datablock, data)
|
||||
|
||||
# datablock.name = data.get('name')
|
||||
datablock.source = 'FILE'
|
||||
datablock.filepath_raw = get_filepath(data['filename'])
|
||||
color_space_name = data["colorspace_settings"]["name"]
|
||||
color_space_name = data.get("colorspace")
|
||||
|
||||
if color_space_name:
|
||||
datablock.colorspace_settings.name = color_space_name
|
||||
@ -92,12 +93,10 @@ class BlImage(ReplicatedDatablock):
|
||||
"name",
|
||||
# 'source',
|
||||
'size',
|
||||
'height',
|
||||
'alpha',
|
||||
'float_buffer',
|
||||
'alpha_mode',
|
||||
'colorspace_settings']
|
||||
'alpha_mode']
|
||||
data.update(dumper.dump(datablock))
|
||||
data['colorspace'] = datablock.colorspace_settings.name
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
@ -132,10 +131,7 @@ class BlImage(ReplicatedDatablock):
|
||||
if datablock.is_dirty:
|
||||
datablock.save()
|
||||
|
||||
if not data or (datablock and (datablock.name != data.get('name'))):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
_type = bpy.types.Image
|
||||
_class = BlImage
|
||||
|
@ -29,6 +29,8 @@ POINT = ['co', 'weight_softbody', 'co_deform']
|
||||
|
||||
|
||||
class BlLattice(ReplicatedDatablock):
|
||||
use_delta = True
|
||||
|
||||
bl_id = "lattices"
|
||||
bl_class = bpy.types.Lattice
|
||||
bl_check_common = False
|
||||
|
@ -26,6 +26,8 @@ from .bl_action import dump_animation_data, load_animation_data, resolve_animati
|
||||
|
||||
|
||||
class BlLight(ReplicatedDatablock):
|
||||
use_delta = True
|
||||
|
||||
bl_id = "lights"
|
||||
bl_class = bpy.types.Light
|
||||
bl_check_common = False
|
||||
|
@ -25,6 +25,8 @@ from replication.protocol import ReplicatedDatablock
|
||||
from .bl_datablock import resolve_datablock_from_uuid
|
||||
|
||||
class BlLightprobe(ReplicatedDatablock):
|
||||
use_delta = True
|
||||
|
||||
bl_id = "lightprobes"
|
||||
bl_class = bpy.types.LightProbe
|
||||
bl_check_common = False
|
||||
|
@ -124,8 +124,7 @@ def dump_node(node: bpy.types.ShaderNode) -> dict:
|
||||
"show_preview",
|
||||
"show_texture",
|
||||
"outputs",
|
||||
"width_hidden",
|
||||
"image"
|
||||
"width_hidden"
|
||||
]
|
||||
|
||||
dumped_node = node_dumper.dump(node)
|
||||
@ -397,11 +396,14 @@ def load_materials_slots(src_materials: list, dst_materials: bpy.types.bpy_prop_
|
||||
|
||||
|
||||
class BlMaterial(ReplicatedDatablock):
|
||||
use_delta = True
|
||||
|
||||
bl_id = "materials"
|
||||
bl_class = bpy.types.Material
|
||||
bl_check_common = False
|
||||
bl_icon = 'MATERIAL_DATA'
|
||||
bl_reload_parent = False
|
||||
bl_reload_child = True
|
||||
|
||||
@staticmethod
|
||||
def construct(data: dict) -> object:
|
||||
@ -409,8 +411,6 @@ class BlMaterial(ReplicatedDatablock):
|
||||
|
||||
@staticmethod
|
||||
def load(data: dict, datablock: object):
|
||||
load_animation_data(data.get('animation_data'), datablock)
|
||||
|
||||
loader = Loader()
|
||||
|
||||
is_grease_pencil = data.get('is_grease_pencil')
|
||||
@ -427,6 +427,8 @@ class BlMaterial(ReplicatedDatablock):
|
||||
datablock.use_nodes = True
|
||||
|
||||
load_node_tree(data['node_tree'], datablock.node_tree)
|
||||
load_animation_data(data.get('nodes_animation_data'), datablock.node_tree)
|
||||
load_animation_data(data.get('animation_data'), datablock)
|
||||
|
||||
@staticmethod
|
||||
def dump(datablock: object) -> dict:
|
||||
@ -494,8 +496,10 @@ class BlMaterial(ReplicatedDatablock):
|
||||
data['grease_pencil'] = gp_mat_dumper.dump(datablock.grease_pencil)
|
||||
elif datablock.use_nodes:
|
||||
data['node_tree'] = dump_node_tree(datablock.node_tree)
|
||||
data['nodes_animation_data'] = dump_animation_data(datablock.node_tree)
|
||||
|
||||
data['animation_data'] = dump_animation_data(datablock)
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
@ -509,7 +513,7 @@ class BlMaterial(ReplicatedDatablock):
|
||||
|
||||
if datablock.use_nodes:
|
||||
deps.extend(get_node_tree_dependencies(datablock.node_tree))
|
||||
|
||||
deps.extend(resolve_animation_dependencies(datablock.node_tree))
|
||||
deps.extend(resolve_animation_dependencies(datablock))
|
||||
|
||||
return deps
|
||||
|
@ -55,6 +55,8 @@ POLYGON = [
|
||||
]
|
||||
|
||||
class BlMesh(ReplicatedDatablock):
|
||||
use_delta = True
|
||||
|
||||
bl_id = "meshes"
|
||||
bl_class = bpy.types.Mesh
|
||||
bl_check_common = False
|
||||
|
@ -65,6 +65,8 @@ def load_metaball_elements(elements_data, elements):
|
||||
|
||||
|
||||
class BlMetaball(ReplicatedDatablock):
|
||||
use_delta = True
|
||||
|
||||
bl_id = "metaballs"
|
||||
bl_class = bpy.types.MetaBall
|
||||
bl_check_common = False
|
||||
|
@ -28,6 +28,8 @@ from .bl_datablock import resolve_datablock_from_uuid
|
||||
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
||||
|
||||
class BlNodeGroup(ReplicatedDatablock):
|
||||
use_delta = True
|
||||
|
||||
bl_id = "node_groups"
|
||||
bl_class = bpy.types.NodeTree
|
||||
bl_check_common = False
|
||||
|
@ -493,6 +493,8 @@ def load_modifiers_custom_data(dumped_modifiers: dict, modifiers: bpy.types.bpy_
|
||||
|
||||
|
||||
class BlObject(ReplicatedDatablock):
|
||||
use_delta = True
|
||||
|
||||
bl_id = "objects"
|
||||
bl_class = bpy.types.Object
|
||||
bl_check_common = False
|
||||
@ -618,10 +620,8 @@ class BlObject(ReplicatedDatablock):
|
||||
|
||||
transform = data.get('transforms', None)
|
||||
if transform:
|
||||
datablock.matrix_parent_inverse = mathutils.Matrix(
|
||||
transform['matrix_parent_inverse'])
|
||||
datablock.matrix_parent_inverse = mathutils.Matrix(transform['matrix_parent_inverse'])
|
||||
datablock.matrix_basis = mathutils.Matrix(transform['matrix_basis'])
|
||||
datablock.matrix_local = mathutils.Matrix(transform['matrix_local'])
|
||||
|
||||
|
||||
@staticmethod
|
||||
|
@ -41,6 +41,8 @@ IGNORED_ATTR = [
|
||||
]
|
||||
|
||||
class BlParticle(ReplicatedDatablock):
|
||||
use_delta = True
|
||||
|
||||
bl_id = "particles"
|
||||
bl_class = bpy.types.ParticleSettings
|
||||
bl_icon = "PARTICLES"
|
||||
|
@ -445,6 +445,15 @@ class BlScene(ReplicatedDatablock):
|
||||
elif datablock.sequence_editor and not sequences:
|
||||
datablock.sequence_editor_clear()
|
||||
|
||||
# Timeline markers
|
||||
markers = data.get('timeline_markers')
|
||||
if markers:
|
||||
datablock.timeline_markers.clear()
|
||||
for name, frame, camera in markers:
|
||||
marker = datablock.timeline_markers.new(name, frame=frame)
|
||||
if camera:
|
||||
marker.camera = resolve_datablock_from_uuid(camera, bpy.data.objects)
|
||||
marker.select = False
|
||||
# FIXME: Find a better way after the replication big refacotoring
|
||||
# Keep other user from deleting collection object by flushing their history
|
||||
flush_history()
|
||||
@ -517,6 +526,10 @@ class BlScene(ReplicatedDatablock):
|
||||
dumped_sequences[seq.name] = dump_sequence(seq)
|
||||
data['sequences'] = dumped_sequences
|
||||
|
||||
# Timeline markers
|
||||
if datablock.timeline_markers:
|
||||
data['timeline_markers'] = [(m.name, m.frame, getattr(m.camera, 'uuid', None)) for m in datablock.timeline_markers]
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
|
@ -25,6 +25,8 @@ from .bl_datablock import resolve_datablock_from_uuid
|
||||
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
||||
|
||||
class BlSpeaker(ReplicatedDatablock):
|
||||
use_delta = True
|
||||
|
||||
bl_id = "speakers"
|
||||
bl_class = bpy.types.Speaker
|
||||
bl_check_common = False
|
||||
|
@ -26,6 +26,8 @@ from .bl_action import dump_animation_data, load_animation_data, resolve_animati
|
||||
import bpy.types as T
|
||||
|
||||
class BlTexture(ReplicatedDatablock):
|
||||
use_delta = True
|
||||
|
||||
bl_id = "textures"
|
||||
bl_class = bpy.types.Texture
|
||||
bl_check_common = False
|
||||
|
@ -27,6 +27,8 @@ from .bl_material import dump_materials_slots, load_materials_slots
|
||||
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
||||
|
||||
class BlVolume(ReplicatedDatablock):
|
||||
use_delta = True
|
||||
|
||||
bl_id = "volumes"
|
||||
bl_class = bpy.types.Volume
|
||||
bl_check_common = False
|
||||
|
@ -30,6 +30,8 @@ from .bl_action import dump_animation_data, load_animation_data, resolve_animati
|
||||
|
||||
|
||||
class BlWorld(ReplicatedDatablock):
|
||||
use_delta = True
|
||||
|
||||
bl_id = "worlds"
|
||||
bl_class = bpy.types.World
|
||||
bl_check_common = True
|
||||
|
152
multi_user/handlers.py
Normal file
152
multi_user/handlers.py
Normal file
@ -0,0 +1,152 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
import logging
|
||||
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
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 . import shared_data, utils
|
||||
|
||||
|
||||
def sanitize_deps_graph(remove_nodes: bool = False):
|
||||
""" Cleanup the replication graph
|
||||
"""
|
||||
if session and session.state == STATE_ACTIVE:
|
||||
start = utils.current_milli_time()
|
||||
rm_cpt = 0
|
||||
for node in session.repository.graph.values():
|
||||
node.instance = session.repository.rdp.resolve(node.data)
|
||||
if node is None \
|
||||
or (node.state == UP and not node.instance):
|
||||
if remove_nodes:
|
||||
try:
|
||||
porcelain.rm(session.repository,
|
||||
node.uuid,
|
||||
remove_dependencies=False)
|
||||
logging.info(f"Removing {node.uuid}")
|
||||
rm_cpt += 1
|
||||
except NonAuthorizedOperationError:
|
||||
continue
|
||||
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]
|
||||
for node_id in nodes_ids:
|
||||
node = session.repository.graph.get(node_id)
|
||||
if node and node.owner in [session.repository.username, RP_COMMON]:
|
||||
porcelain.commit(session.repository, node_id)
|
||||
porcelain.push(session.repository, 'origin', node_id)
|
||||
|
||||
|
||||
@persistent
|
||||
def on_scene_update(scene):
|
||||
"""Forward blender depsgraph update to replication
|
||||
"""
|
||||
if session and session.state == STATE_ACTIVE:
|
||||
context = bpy.context
|
||||
blender_depsgraph = bpy.context.view_layer.depsgraph
|
||||
dependency_updates = [u for u in blender_depsgraph.updates]
|
||||
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]
|
||||
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}")
|
||||
return
|
||||
|
||||
update_external_dependencies()
|
||||
|
||||
# NOTE: maybe we don't need to check each update but only the first
|
||||
for update in reversed(dependency_updates):
|
||||
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
|
||||
|
||||
if node and (node.owner == session.repository.username or check_common):
|
||||
logging.debug(f"Evaluate {update.id.name}")
|
||||
if node.state == UP:
|
||||
try:
|
||||
porcelain.commit(session.repository, node.uuid)
|
||||
porcelain.push(session.repository,
|
||||
'origin', node.uuid)
|
||||
except ReferenceError:
|
||||
logging.debug(f"Reference error {node.uuid}")
|
||||
except ContextError as e:
|
||||
logging.debug(e)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
else:
|
||||
continue
|
||||
elif isinstance(update.id, bpy.types.Scene):
|
||||
scene = bpy.data.scenes.get(update.id.name)
|
||||
scn_uuid = porcelain.add(session.repository, scene)
|
||||
porcelain.commit(session.repository, scn_uuid)
|
||||
porcelain.push(session.repository, 'origin', scn_uuid)
|
||||
|
||||
|
||||
@persistent
|
||||
def resolve_deps_graph(dummy):
|
||||
"""Resolve deps graph
|
||||
|
||||
Temporary solution to resolve each node pointers after a Undo.
|
||||
A future solution should be to avoid storing dataclock reference...
|
||||
|
||||
"""
|
||||
if session and session.state == STATE_ACTIVE:
|
||||
sanitize_deps_graph(remove_nodes=True)
|
||||
|
||||
|
||||
@persistent
|
||||
def load_pre_handler(dummy):
|
||||
if session and session.state in [STATE_ACTIVE, STATE_SYNCING]:
|
||||
bpy.ops.session.stop()
|
||||
|
||||
|
||||
@persistent
|
||||
def update_client_frame(scene):
|
||||
if session and session.state == STATE_ACTIVE:
|
||||
porcelain.update_user_metadata(session.repository, {
|
||||
'frame_current': scene.frame_current
|
||||
})
|
||||
|
||||
|
||||
def register():
|
||||
bpy.app.handlers.undo_post.append(resolve_deps_graph)
|
||||
bpy.app.handlers.redo_post.append(resolve_deps_graph)
|
||||
|
||||
bpy.app.handlers.load_pre.append(load_pre_handler)
|
||||
bpy.app.handlers.frame_change_pre.append(update_client_frame)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.app.handlers.undo_post.remove(resolve_deps_graph)
|
||||
bpy.app.handlers.redo_post.remove(resolve_deps_graph)
|
||||
|
||||
bpy.app.handlers.load_pre.remove(load_pre_handler)
|
||||
bpy.app.handlers.frame_change_pre.remove(update_client_frame)
|
Submodule multi_user/libs/replication updated: cb4cdd0444...ff88725991
@ -27,12 +27,12 @@ import shutil
|
||||
import string
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from operator import itemgetter
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from time import gmtime, strftime
|
||||
import traceback
|
||||
|
||||
from bpy.props import FloatProperty
|
||||
|
||||
@ -45,16 +45,17 @@ import bpy
|
||||
import mathutils
|
||||
from bpy.app.handlers import persistent
|
||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
from replication import porcelain
|
||||
from replication.constants import (COMMITED, FETCHED, RP_COMMON, STATE_ACTIVE,
|
||||
STATE_INITIAL, STATE_SYNCING, UP)
|
||||
from replication.protocol import DataTranslationProtocol
|
||||
from replication.exception import ContextError, NonAuthorizedOperationError
|
||||
from replication.interface import session
|
||||
from replication import porcelain
|
||||
from replication.repository import Repository
|
||||
from replication.objects import Node
|
||||
from replication.protocol import DataTranslationProtocol
|
||||
from replication.repository import Repository
|
||||
|
||||
from . import bl_types, environment, timers, ui, utils
|
||||
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
|
||||
from .timers import registry
|
||||
|
||||
@ -99,7 +100,7 @@ def initialize_session():
|
||||
|
||||
# Step 2: Load nodes
|
||||
logging.info("Applying nodes")
|
||||
for node in session.repository.index_sorted:
|
||||
for node in session.repository.heads:
|
||||
porcelain.apply(session.repository, node)
|
||||
|
||||
logging.info("Registering timers")
|
||||
@ -112,7 +113,7 @@ def initialize_session():
|
||||
utils.flush_history()
|
||||
|
||||
# Step 6: Launch deps graph update handling
|
||||
bpy.app.handlers.depsgraph_update_post.append(depsgraph_evaluation)
|
||||
bpy.app.handlers.depsgraph_update_post.append(on_scene_update)
|
||||
|
||||
|
||||
@session_callback('on_exit')
|
||||
@ -132,8 +133,8 @@ def on_connection_end(reason="none"):
|
||||
|
||||
stop_modal_executor = True
|
||||
|
||||
if depsgraph_evaluation in bpy.app.handlers.depsgraph_update_post:
|
||||
bpy.app.handlers.depsgraph_update_post.remove(depsgraph_evaluation)
|
||||
if on_scene_update in bpy.app.handlers.depsgraph_update_post:
|
||||
bpy.app.handlers.depsgraph_update_post.remove(on_scene_update)
|
||||
|
||||
# Step 3: remove file handled
|
||||
logger = logging.getLogger()
|
||||
@ -272,8 +273,7 @@ class SessionStartOperator(bpy.types.Operator):
|
||||
|
||||
session_update = timers.SessionStatusUpdate()
|
||||
session_user_sync = timers.SessionUserSync()
|
||||
session_background_executor = timers.MainThreadExecutor(
|
||||
execution_queue=background_execution_queue)
|
||||
session_background_executor = timers.MainThreadExecutor(execution_queue=background_execution_queue)
|
||||
session_listen = timers.SessionListenTimer(timeout=0.001)
|
||||
|
||||
session_listen.register()
|
||||
@ -603,9 +603,9 @@ class SessionApply(bpy.types.Operator):
|
||||
node_ref = session.repository.graph.get(self.target)
|
||||
porcelain.apply(session.repository,
|
||||
self.target,
|
||||
force=True,
|
||||
force_dependencies=self.reset_dependencies)
|
||||
force=True)
|
||||
impl = session.repository.rdp.get_implementation(node_ref.instance)
|
||||
# NOTE: find another way to handle child and parent automatic reloading
|
||||
if impl.bl_reload_parent:
|
||||
for parent in session.repository.graph.get_parents(self.target):
|
||||
logging.debug(f"Refresh parent {parent}")
|
||||
@ -613,6 +613,11 @@ class SessionApply(bpy.types.Operator):
|
||||
porcelain.apply(session.repository,
|
||||
parent.uuid,
|
||||
force=True)
|
||||
if hasattr(impl, 'bl_reload_child') and impl.bl_reload_child:
|
||||
for dep in node_ref.dependencies:
|
||||
porcelain.apply(session.repository,
|
||||
dep,
|
||||
force=True)
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, repr(e))
|
||||
traceback.print_exc()
|
||||
@ -636,7 +641,7 @@ class SessionCommit(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
try:
|
||||
porcelain.commit(session.repository, self.target)
|
||||
porcelain.push(session.repository, 'origin', self.target)
|
||||
porcelain.push(session.repository, 'origin', self.target, force=True)
|
||||
return {"FINISHED"}
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, repr(e))
|
||||
@ -684,6 +689,7 @@ class SessionPurgeOperator(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
try:
|
||||
sanitize_deps_graph(remove_nodes=True)
|
||||
porcelain.purge_orphan_nodes(session.repository)
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, repr(e))
|
||||
|
||||
@ -716,7 +722,6 @@ class SessionNotifyOperator(bpy.types.Operator):
|
||||
layout = self.layout
|
||||
layout.row().label(text=self.message)
|
||||
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
@ -919,110 +924,6 @@ classes = (
|
||||
)
|
||||
|
||||
|
||||
def update_external_dependencies():
|
||||
nodes_ids = [n.uuid for n in session.repository.graph.values() if n.data['type_id'] in ['WindowsPath', 'PosixPath']]
|
||||
for node_id in nodes_ids:
|
||||
node = session.repository.graph.get(node_id)
|
||||
if node and node.owner in [session.repository.username, RP_COMMON]:
|
||||
porcelain.commit(session.repository, node_id)
|
||||
porcelain.push(session.repository,'origin', node_id)
|
||||
|
||||
|
||||
def sanitize_deps_graph(remove_nodes: bool = False):
|
||||
""" Cleanup the replication graph
|
||||
"""
|
||||
if session and session.state == STATE_ACTIVE:
|
||||
start = utils.current_milli_time()
|
||||
rm_cpt = 0
|
||||
for node in session.repository.graph.values():
|
||||
node.instance = session.repository.rdp.resolve(node.data)
|
||||
if node is None \
|
||||
or (node.state == UP and not node.instance):
|
||||
if remove_nodes:
|
||||
try:
|
||||
porcelain.rm(session.repository,
|
||||
node.uuid,
|
||||
remove_dependencies=False)
|
||||
logging.info(f"Removing {node.uuid}")
|
||||
rm_cpt += 1
|
||||
except NonAuthorizedOperationError:
|
||||
continue
|
||||
logging.info(f"Sanitize took { utils.current_milli_time()-start} ms, removed {rm_cpt} nodes")
|
||||
|
||||
|
||||
@persistent
|
||||
def resolve_deps_graph(dummy):
|
||||
"""Resolve deps graph
|
||||
|
||||
Temporary solution to resolve each node pointers after a Undo.
|
||||
A future solution should be to avoid storing dataclock reference...
|
||||
|
||||
"""
|
||||
if session and session.state == STATE_ACTIVE:
|
||||
sanitize_deps_graph(remove_nodes=True)
|
||||
|
||||
|
||||
@persistent
|
||||
def load_pre_handler(dummy):
|
||||
if session and session.state in [STATE_ACTIVE, STATE_SYNCING]:
|
||||
bpy.ops.session.stop()
|
||||
|
||||
|
||||
@persistent
|
||||
def update_client_frame(scene):
|
||||
if session and session.state == STATE_ACTIVE:
|
||||
porcelain.update_user_metadata(session.repository, {
|
||||
'frame_current': scene.frame_current
|
||||
})
|
||||
|
||||
|
||||
@persistent
|
||||
def depsgraph_evaluation(scene):
|
||||
if session and session.state == STATE_ACTIVE:
|
||||
context = bpy.context
|
||||
blender_depsgraph = bpy.context.view_layer.depsgraph
|
||||
dependency_updates = [u for u in blender_depsgraph.updates]
|
||||
settings = utils.get_preferences()
|
||||
|
||||
update_external_dependencies()
|
||||
|
||||
is_internal = [u for u in dependency_updates if u.is_updated_geometry or u.is_updated_shading or u.is_updated_transform]
|
||||
|
||||
# NOTE: maybe we don't need to check each update but only the first
|
||||
if not is_internal:
|
||||
return
|
||||
for update in reversed(dependency_updates):
|
||||
# Is the object tracked ?
|
||||
if update.id.uuid:
|
||||
# Retrieve local version
|
||||
node = session.repository.graph.get(update.id.uuid)
|
||||
check_common = session.repository.rdp.get_implementation(update.id).bl_check_common
|
||||
# Check our right on this update:
|
||||
# - if its ours or ( under common and diff), launch the
|
||||
# update process
|
||||
# - if its to someone else, ignore the update
|
||||
if node and (node.owner == session.repository.username or check_common):
|
||||
if node.state == UP:
|
||||
try:
|
||||
porcelain.commit(session.repository, node.uuid)
|
||||
porcelain.push(session.repository, 'origin', node.uuid)
|
||||
except ReferenceError:
|
||||
logging.debug(f"Reference error {node.uuid}")
|
||||
except ContextError as e:
|
||||
logging.debug(e)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
else:
|
||||
continue
|
||||
# A new scene is created
|
||||
elif isinstance(update.id, bpy.types.Scene):
|
||||
ref = session.repository.get_node_by_datablock(update.id)
|
||||
if ref:
|
||||
pass
|
||||
else:
|
||||
scn_uuid = porcelain.add(session.repository, update.id)
|
||||
porcelain.commit(session.node_id, scn_uuid)
|
||||
porcelain.push(session.repository,'origin', scn_uuid)
|
||||
def register():
|
||||
from bpy.utils import register_class
|
||||
|
||||
@ -1030,13 +931,6 @@ def register():
|
||||
register_class(cls)
|
||||
|
||||
|
||||
bpy.app.handlers.undo_post.append(resolve_deps_graph)
|
||||
bpy.app.handlers.redo_post.append(resolve_deps_graph)
|
||||
|
||||
bpy.app.handlers.load_pre.append(load_pre_handler)
|
||||
bpy.app.handlers.frame_change_pre.append(update_client_frame)
|
||||
|
||||
|
||||
def unregister():
|
||||
if session and session.state == STATE_ACTIVE:
|
||||
session.disconnect()
|
||||
@ -1044,9 +938,3 @@ def unregister():
|
||||
from bpy.utils import unregister_class
|
||||
for cls in reversed(classes):
|
||||
unregister_class(cls)
|
||||
|
||||
bpy.app.handlers.undo_post.remove(resolve_deps_graph)
|
||||
bpy.app.handlers.redo_post.remove(resolve_deps_graph)
|
||||
|
||||
bpy.app.handlers.load_pre.remove(load_pre_handler)
|
||||
bpy.app.handlers.frame_change_pre.remove(update_client_frame)
|
||||
|
@ -273,6 +273,13 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
||||
step=1,
|
||||
subtype='PERCENTAGE',
|
||||
)
|
||||
presence_mode_distance: bpy.props.FloatProperty(
|
||||
name="Distance mode visibilty",
|
||||
description="Adjust the distance visibilty of user's mode",
|
||||
min=0.1,
|
||||
max=1000,
|
||||
default=100,
|
||||
)
|
||||
conf_session_identity_expanded: bpy.props.BoolProperty(
|
||||
name="Identity",
|
||||
description="Identity",
|
||||
@ -446,10 +453,11 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
||||
col = box.column(align=True)
|
||||
col.prop(self, "presence_hud_scale", expand=True)
|
||||
|
||||
|
||||
col.prop(self, "presence_hud_hpos", expand=True)
|
||||
col.prop(self, "presence_hud_vpos", expand=True)
|
||||
|
||||
col.prop(self, "presence_mode_distance", expand=True)
|
||||
|
||||
if self.category == 'UPDATE':
|
||||
from . import addon_updater_ops
|
||||
addon_updater_ops.update_settings_ui(self, context)
|
||||
@ -538,6 +546,11 @@ class SessionProps(bpy.types.PropertyGroup):
|
||||
description='Enable user overlay ',
|
||||
default=True,
|
||||
)
|
||||
presence_show_mode: bpy.props.BoolProperty(
|
||||
name="Show users current mode",
|
||||
description='Enable user mode overlay ',
|
||||
default=False,
|
||||
)
|
||||
presence_show_far_user: bpy.props.BoolProperty(
|
||||
name="Show users on different scenes",
|
||||
description="Show user on different scenes",
|
||||
|
@ -94,15 +94,35 @@ def project_to_viewport(region: bpy.types.Region, rv3d: bpy.types.RegionView3D,
|
||||
return [target.x, target.y, target.z]
|
||||
|
||||
|
||||
def bbox_from_obj(obj: bpy.types.Object, radius: float) -> list:
|
||||
def bbox_from_obj(obj: bpy.types.Object) -> list:
|
||||
""" Generate a bounding box for a given object by using its world matrix
|
||||
|
||||
:param obj: target object
|
||||
:type obj: bpy.types.Object
|
||||
:param radius: bounding box radius
|
||||
:type radius: float
|
||||
:return: list of 8 points [(x,y,z),...]
|
||||
:return: list of 8 points [(x,y,z),...], list of 12 link between these points [(1,2),...]
|
||||
"""
|
||||
radius = 1.0 # Radius of the bounding box
|
||||
vertex_indices = (
|
||||
(0, 1), (0, 2), (1, 3), (2, 3),
|
||||
(4, 5), (4, 6), (5, 7), (6, 7),
|
||||
(0, 4), (1, 5), (2, 6), (3, 7))
|
||||
|
||||
if obj.type == 'EMPTY':
|
||||
radius = obj.empty_display_size
|
||||
elif obj.type == 'LIGHT':
|
||||
radius = obj.data.shadow_soft_size
|
||||
elif obj.type == 'LIGHT_PROBE':
|
||||
radius = obj.data.influence_distance
|
||||
elif obj.type == 'CAMERA':
|
||||
radius = obj.data.display_size
|
||||
elif hasattr(obj, 'bound_box'):
|
||||
vertex_indices = (
|
||||
(0, 1), (1, 2), (2, 3), (0, 3),
|
||||
(4, 5), (5, 6), (6, 7), (4, 7),
|
||||
(0, 4), (1, 5), (2, 6), (3, 7))
|
||||
vertex_pos = get_bb_coords_from_obj(obj)
|
||||
return vertex_pos, vertex_indices
|
||||
|
||||
coords = [
|
||||
(-radius, -radius, -radius), (+radius, -radius, -radius),
|
||||
(-radius, +radius, -radius), (+radius, +radius, -radius),
|
||||
@ -112,9 +132,37 @@ def bbox_from_obj(obj: bpy.types.Object, radius: float) -> list:
|
||||
base = obj.matrix_world
|
||||
bbox_corners = [base @ mathutils.Vector(corner) for corner in coords]
|
||||
|
||||
return [(point.x, point.y, point.z)
|
||||
for point in bbox_corners]
|
||||
vertex_pos = [(point.x, point.y, point.z) for point in bbox_corners]
|
||||
|
||||
return vertex_pos, vertex_indices
|
||||
|
||||
def bbox_from_instance_collection(ic: bpy.types.Object) -> list:
|
||||
""" Generate a bounding box for a given instance collection by using its objects
|
||||
|
||||
:param ic: target instance collection
|
||||
:type ic: bpy.types.Object
|
||||
:param radius: bounding box radius
|
||||
:type radius: float
|
||||
:return: list of 8*objs points [(x,y,z),...], tuple of 12*objs link between these points [(1,2),...]
|
||||
"""
|
||||
vertex_pos = []
|
||||
vertex_indices = ()
|
||||
|
||||
for obj_index, obj in enumerate(ic.instance_collection.objects):
|
||||
vertex_pos_temp, vertex_indices_temp = bbox_from_obj(obj)
|
||||
vertex_pos += vertex_pos_temp
|
||||
vertex_indices_list_temp = list(list(indice) for indice in vertex_indices_temp)
|
||||
for indice in vertex_indices_list_temp:
|
||||
indice[0] += 8*obj_index
|
||||
indice[1] += 8*obj_index
|
||||
vertex_indices_temp = tuple(tuple(indice) for indice in vertex_indices_list_temp)
|
||||
vertex_indices += vertex_indices_temp
|
||||
|
||||
bbox_corners = [ic.matrix_world @ mathutils.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
|
||||
@ -203,6 +251,13 @@ class Widget(object):
|
||||
"""
|
||||
return True
|
||||
|
||||
def configure_bgl(self):
|
||||
bgl.glLineWidth(2.)
|
||||
bgl.glEnable(bgl.GL_DEPTH_TEST)
|
||||
bgl.glEnable(bgl.GL_BLEND)
|
||||
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
||||
|
||||
|
||||
def draw(self):
|
||||
"""How to draw the widget
|
||||
"""
|
||||
@ -256,11 +311,6 @@ class UserFrustumWidget(Widget):
|
||||
{"pos": positions},
|
||||
indices=self.indices)
|
||||
|
||||
bgl.glLineWidth(2.)
|
||||
bgl.glEnable(bgl.GL_DEPTH_TEST)
|
||||
bgl.glEnable(bgl.GL_BLEND)
|
||||
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
||||
|
||||
shader.bind()
|
||||
shader.uniform_float("color", self.data.get('color'))
|
||||
batch.draw(shader)
|
||||
@ -294,38 +344,16 @@ class UserSelectionWidget(Widget):
|
||||
self.settings.presence_show_selected and \
|
||||
self.settings.enable_presence
|
||||
|
||||
def draw(self):
|
||||
def draw(self):
|
||||
user_selection = self.data.get('selected_objects')
|
||||
for select_ob in user_selection:
|
||||
ob = find_from_attr("uuid", select_ob, bpy.data.objects)
|
||||
if not ob:
|
||||
for select_obj in user_selection:
|
||||
obj = find_from_attr("uuid", select_obj, bpy.data.objects)
|
||||
if not obj:
|
||||
return
|
||||
|
||||
vertex_pos = bbox_from_obj(ob, 1.0)
|
||||
vertex_indices = (
|
||||
(0, 1), (1, 2), (2, 3), (0, 3),
|
||||
(4, 5), (5, 6), (6, 7), (4, 7),
|
||||
(0, 4), (1, 5), (2, 6), (3, 7))
|
||||
|
||||
if ob.instance_collection:
|
||||
for obj in ob.instance_collection.objects:
|
||||
if obj.type == 'MESH' and hasattr(obj, 'bound_box'):
|
||||
vertex_pos = get_bb_coords_from_obj(obj, instance=ob)
|
||||
break
|
||||
elif ob.type == 'EMPTY':
|
||||
vertex_pos = bbox_from_obj(ob, ob.empty_display_size)
|
||||
elif ob.type == 'LIGHT':
|
||||
vertex_pos = bbox_from_obj(ob, ob.data.shadow_soft_size)
|
||||
elif ob.type == 'LIGHT_PROBE':
|
||||
vertex_pos = bbox_from_obj(ob, ob.data.influence_distance)
|
||||
elif ob.type == 'CAMERA':
|
||||
vertex_pos = bbox_from_obj(ob, ob.data.display_size)
|
||||
elif hasattr(ob, 'bound_box'):
|
||||
vertex_indices = (
|
||||
(0, 1), (1, 2), (2, 3), (0, 3),
|
||||
(4, 5), (5, 6), (6, 7), (4, 7),
|
||||
(0, 4), (1, 5), (2, 6), (3, 7))
|
||||
vertex_pos = get_bb_coords_from_obj(ob)
|
||||
if obj.instance_collection:
|
||||
vertex_pos, vertex_indices = bbox_from_instance_collection(obj)
|
||||
else :
|
||||
vertex_pos, vertex_indices = bbox_from_obj(obj)
|
||||
|
||||
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
|
||||
batch = batch_for_shader(
|
||||
@ -338,7 +366,6 @@ class UserSelectionWidget(Widget):
|
||||
shader.uniform_float("color", self.data.get('color'))
|
||||
batch.draw(shader)
|
||||
|
||||
|
||||
class UserNameWidget(Widget):
|
||||
draw_type = 'POST_PIXEL'
|
||||
|
||||
@ -381,6 +408,62 @@ class UserNameWidget(Widget):
|
||||
blf.color(0, color[0], color[1], color[2], color[3])
|
||||
blf.draw(0, self.username)
|
||||
|
||||
class UserModeWidget(Widget):
|
||||
draw_type = 'POST_PIXEL'
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
username):
|
||||
self.username = username
|
||||
self.settings = bpy.context.window_manager.session
|
||||
self.preferences = get_preferences()
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
user = session.online_users.get(self.username)
|
||||
if user:
|
||||
return user.get('metadata')
|
||||
else:
|
||||
return None
|
||||
|
||||
def poll(self):
|
||||
if self.data is None:
|
||||
return False
|
||||
|
||||
scene_current = self.data.get('scene_current')
|
||||
mode_current = self.data.get('mode_current')
|
||||
user_selection = self.data.get('selected_objects')
|
||||
|
||||
return (scene_current == bpy.context.scene.name or
|
||||
mode_current == bpy.context.mode or
|
||||
self.settings.presence_show_far_user) and \
|
||||
user_selection and \
|
||||
self.settings.presence_show_mode and \
|
||||
self.settings.enable_presence
|
||||
|
||||
def draw(self):
|
||||
user_selection = self.data.get('selected_objects')
|
||||
area, region, rv3d = view3d_find()
|
||||
viewport_coord = project_to_viewport(region, rv3d, (0, 0))
|
||||
|
||||
obj = find_from_attr("uuid", user_selection[0], bpy.data.objects)
|
||||
if not obj:
|
||||
return
|
||||
mode_current = self.data.get('mode_current')
|
||||
color = self.data.get('color')
|
||||
origin_coord = project_to_screen(obj.location)
|
||||
|
||||
distance_viewport_object = math.sqrt((viewport_coord[0]-obj.location[0])**2+(viewport_coord[1]-obj.location[1])**2+(viewport_coord[2]-obj.location[2])**2)
|
||||
|
||||
if distance_viewport_object > self.preferences.presence_mode_distance :
|
||||
return
|
||||
|
||||
if origin_coord :
|
||||
blf.position(0, origin_coord[0]+8, origin_coord[1]-15, 0)
|
||||
blf.size(0, 16, 72)
|
||||
blf.color(0, color[0], color[1], color[2], color[3])
|
||||
blf.draw(0, mode_current)
|
||||
|
||||
|
||||
class SessionStatusWidget(Widget):
|
||||
draw_type = 'POST_PIXEL'
|
||||
@ -463,6 +546,7 @@ class DrawFactory(object):
|
||||
try:
|
||||
for widget in self.widgets.values():
|
||||
if widget.draw_type == 'POST_VIEW' and widget.poll():
|
||||
widget.configure_bgl()
|
||||
widget.draw()
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
@ -472,6 +556,7 @@ class DrawFactory(object):
|
||||
try:
|
||||
for widget in self.widgets.values():
|
||||
if widget.draw_type == 'POST_PIXEL' and widget.poll():
|
||||
widget.configure_bgl()
|
||||
widget.draw()
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
@ -484,6 +569,7 @@ this.renderer = DrawFactory()
|
||||
|
||||
def register():
|
||||
this.renderer.register_handlers()
|
||||
|
||||
|
||||
this.renderer.add_widget("session_status", SessionStatusWidget())
|
||||
|
||||
|
48
multi_user/shared_data.py
Normal file
48
multi_user/shared_data.py
Normal file
@ -0,0 +1,48 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
from replication.constants import STATE_INITIAL
|
||||
|
||||
|
||||
class SessionData():
|
||||
""" A structure to share easily the current session data across the addon
|
||||
modules.
|
||||
This object will completely replace the Singleton lying in replication
|
||||
interface module.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.repository = None # The current repository
|
||||
self.remote = None # The active remote
|
||||
self.server = None
|
||||
self.applied_updates = []
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
if self.remote is None:
|
||||
return STATE_INITIAL
|
||||
else:
|
||||
return self.remote.connection_status
|
||||
|
||||
def clear(self):
|
||||
self.remote = None
|
||||
self.repository = None
|
||||
self.server = None
|
||||
self.applied_updates = []
|
||||
|
||||
|
||||
session = SessionData()
|
@ -27,10 +27,12 @@ from replication.interface import session
|
||||
from replication import porcelain
|
||||
|
||||
from . import operators, utils
|
||||
from .presence import (UserFrustumWidget, UserNameWidget, UserSelectionWidget,
|
||||
from .presence import (UserFrustumWidget, UserNameWidget, UserModeWidget, UserSelectionWidget,
|
||||
generate_user_camera, get_view_matrix, refresh_3d_view,
|
||||
refresh_sidebar_view, renderer)
|
||||
|
||||
from . import shared_data
|
||||
|
||||
this = sys.modules[__name__]
|
||||
|
||||
# Registered timers
|
||||
@ -89,7 +91,7 @@ class Timer(object):
|
||||
if bpy.app.timers.is_registered(self.main):
|
||||
logging.info(f"Unregistering {self.id}")
|
||||
bpy.app.timers.unregister(self.main)
|
||||
|
||||
|
||||
del this.registry[self.id]
|
||||
self.is_running = False
|
||||
|
||||
@ -114,6 +116,7 @@ class ApplyTimer(Timer):
|
||||
|
||||
if node_ref.state == FETCHED:
|
||||
try:
|
||||
shared_data.session.applied_updates.append(node)
|
||||
porcelain.apply(session.repository, node)
|
||||
except Exception as e:
|
||||
logging.error(f"Fail to apply {node_ref.uuid}")
|
||||
@ -126,6 +129,11 @@ class ApplyTimer(Timer):
|
||||
porcelain.apply(session.repository,
|
||||
parent.uuid,
|
||||
force=True)
|
||||
if hasattr(impl, 'bl_reload_child') and impl.bl_reload_child:
|
||||
for dep in node_ref.dependencies:
|
||||
porcelain.apply(session.repository,
|
||||
dep,
|
||||
force=True)
|
||||
|
||||
|
||||
class DynamicRightSelectTimer(Timer):
|
||||
@ -251,6 +259,7 @@ class DynamicRightSelectTimer(Timer):
|
||||
is_selectable = not session.repository.is_node_readonly(object_uuid)
|
||||
if obj.hide_select != is_selectable:
|
||||
obj.hide_select = is_selectable
|
||||
shared_data.session.applied_updates.append(object_uuid)
|
||||
|
||||
|
||||
class ClientUpdate(Timer):
|
||||
@ -300,7 +309,8 @@ class ClientUpdate(Timer):
|
||||
settings.client_color.b,
|
||||
1),
|
||||
'frame_current': bpy.context.scene.frame_current,
|
||||
'scene_current': scene_current
|
||||
'scene_current': scene_current,
|
||||
'mode_current': bpy.context.mode
|
||||
}
|
||||
porcelain.update_user_metadata(session.repository, metadata)
|
||||
|
||||
@ -314,6 +324,9 @@ class ClientUpdate(Timer):
|
||||
local_user_metadata['view_matrix'] = get_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
|
||||
porcelain.update_user_metadata(session.repository, local_user_metadata)
|
||||
|
||||
|
||||
class SessionStatusUpdate(Timer):
|
||||
@ -341,6 +354,7 @@ class SessionUserSync(Timer):
|
||||
renderer.remove_widget(f"{user.username}_cam")
|
||||
renderer.remove_widget(f"{user.username}_select")
|
||||
renderer.remove_widget(f"{user.username}_name")
|
||||
renderer.remove_widget(f"{user.username}_mode")
|
||||
ui_users.remove(index)
|
||||
break
|
||||
|
||||
@ -356,6 +370,8 @@ class SessionUserSync(Timer):
|
||||
f"{user}_select", UserSelectionWidget(user))
|
||||
renderer.add_widget(
|
||||
f"{user}_name", UserNameWidget(user))
|
||||
renderer.add_widget(
|
||||
f"{user}_mode", UserModeWidget(user))
|
||||
|
||||
|
||||
class MainThreadExecutor(Timer):
|
||||
|
122
multi_user/ui.py
122
multi_user/ui.py
@ -107,7 +107,7 @@ class SESSION_PT_settings(bpy.types.Panel):
|
||||
row = row.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=True)
|
||||
row.prop(settings.sync_flags, "sync_render_settings",text="",icon_only=True, icon='SCENE')
|
||||
row.prop(settings.sync_flags, "sync_during_editmode", text="",icon_only=True, icon='EDITMODE_HLT')
|
||||
row.prop(settings.sync_flags, "sync_active_camera", text="",icon_only=True, icon='OBJECT_DATAMODE')
|
||||
row.prop(settings.sync_flags, "sync_active_camera", text="",icon_only=True, icon='VIEW_CAMERA')
|
||||
|
||||
row= layout.row()
|
||||
|
||||
@ -343,9 +343,10 @@ class SESSION_PT_user(bpy.types.Panel):
|
||||
box = row.box()
|
||||
split = box.split(factor=0.35)
|
||||
split.label(text="user")
|
||||
split = split.split(factor=0.5)
|
||||
split.label(text="location")
|
||||
split = split.split(factor=0.3)
|
||||
split.label(text="mode")
|
||||
split.label(text="frame")
|
||||
split.label(text="location")
|
||||
split.label(text="ping")
|
||||
|
||||
row = layout.row()
|
||||
@ -383,6 +384,8 @@ class SESSION_UL_users(bpy.types.UIList):
|
||||
ping = '-'
|
||||
frame_current = '-'
|
||||
scene_current = '-'
|
||||
mode_current = '-'
|
||||
mode_icon = 'BLANK1'
|
||||
status_icon = 'BLANK1'
|
||||
if session:
|
||||
user = session.online_users.get(item.username)
|
||||
@ -392,13 +395,45 @@ class SESSION_UL_users(bpy.types.UIList):
|
||||
if metadata and 'frame_current' in metadata:
|
||||
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"
|
||||
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.5)
|
||||
split.label(text=scene_current)
|
||||
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)
|
||||
|
||||
|
||||
@ -425,20 +460,29 @@ class SESSION_PT_presence(bpy.types.Panel):
|
||||
settings = context.window_manager.session
|
||||
pref = get_preferences()
|
||||
layout.active = settings.enable_presence
|
||||
|
||||
row = layout.row()
|
||||
row = row.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=True)
|
||||
row.prop(settings, "presence_show_selected",text="",icon_only=True, icon='CUBE')
|
||||
row.prop(settings, "presence_show_user", text="",icon_only=True, icon='CAMERA_DATA')
|
||||
row.prop(settings, "presence_show_mode", text="",icon_only=True, icon='OBJECT_DATAMODE')
|
||||
row.prop(settings, "presence_show_far_user", text="",icon_only=True, icon='SCENE_DATA')
|
||||
|
||||
col = layout.column()
|
||||
if settings.presence_show_mode :
|
||||
row = col.column()
|
||||
row.prop(pref, "presence_mode_distance", expand=True)
|
||||
|
||||
col.prop(settings, "presence_show_session_status")
|
||||
row = col.column()
|
||||
row.active = settings.presence_show_session_status
|
||||
row.prop(pref, "presence_hud_scale", expand=True)
|
||||
row = col.column(align=True)
|
||||
row.active = settings.presence_show_session_status
|
||||
row.prop(pref, "presence_hud_hpos", expand=True)
|
||||
row.prop(pref, "presence_hud_vpos", expand=True)
|
||||
col.prop(settings, "presence_show_selected")
|
||||
col.prop(settings, "presence_show_user")
|
||||
row = layout.column()
|
||||
row.active = settings.presence_show_user
|
||||
row.prop(settings, "presence_show_far_user")
|
||||
if settings.presence_show_session_status :
|
||||
row = col.column()
|
||||
row.active = settings.presence_show_session_status
|
||||
row.prop(pref, "presence_hud_scale", expand=True)
|
||||
row = col.column(align=True)
|
||||
row.active = settings.presence_show_session_status
|
||||
row.prop(pref, "presence_hud_hpos", expand=True)
|
||||
row.prop(pref, "presence_hud_vpos", expand=True)
|
||||
|
||||
|
||||
def draw_property(context, parent, property_uuid, level=0):
|
||||
settings = get_preferences()
|
||||
@ -555,20 +599,15 @@ class SESSION_PT_repository(bpy.types.Panel):
|
||||
# Properties
|
||||
owned_nodes = [k for k, v in session.repository.graph.items() if v.owner==settings.username]
|
||||
|
||||
filtered_node = owned_nodes if runtime_settings.filter_owned else session.repository.graph.keys()
|
||||
filtered_node = owned_nodes if runtime_settings.filter_owned else list(session.repository.graph.keys())
|
||||
|
||||
if runtime_settings.filter_name:
|
||||
for node_id in filtered_node:
|
||||
node_instance = session.repository.graph.get(node_id)
|
||||
name = node_instance.data.get('name')
|
||||
if runtime_settings.filter_name not in name:
|
||||
filtered_node.remove(node_id)
|
||||
filtered_node = [n for n in filtered_node if runtime_settings.filter_name.lower() in session.repository.graph.get(n).data.get('name').lower()]
|
||||
|
||||
if filtered_node:
|
||||
col = layout.column(align=True)
|
||||
for key in filtered_node:
|
||||
draw_property(context, col, key)
|
||||
|
||||
else:
|
||||
layout.row().label(text="Empty")
|
||||
|
||||
@ -590,23 +629,32 @@ class VIEW3D_PT_overlay_session(bpy.types.Panel):
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
view = context.space_data
|
||||
overlay = view.overlay
|
||||
display_all = overlay.show_overlays
|
||||
|
||||
col = layout.column()
|
||||
|
||||
row = col.row(align=True)
|
||||
settings = context.window_manager.session
|
||||
pref = get_preferences()
|
||||
layout.active = settings.enable_presence
|
||||
|
||||
row = layout.row()
|
||||
row = row.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=True)
|
||||
row.prop(settings, "presence_show_selected",text="",icon_only=True, icon='CUBE')
|
||||
row.prop(settings, "presence_show_user", text="",icon_only=True, icon='CAMERA_DATA')
|
||||
row.prop(settings, "presence_show_mode", text="",icon_only=True, icon='OBJECT_DATAMODE')
|
||||
row.prop(settings, "presence_show_far_user", text="",icon_only=True, icon='SCENE_DATA')
|
||||
|
||||
col = layout.column()
|
||||
if settings.presence_show_mode :
|
||||
row = col.column()
|
||||
row.prop(pref, "presence_mode_distance", expand=True)
|
||||
|
||||
col.prop(settings, "presence_show_session_status")
|
||||
col.prop(settings, "presence_show_selected")
|
||||
col.prop(settings, "presence_show_user")
|
||||
|
||||
row = layout.column()
|
||||
row.active = settings.presence_show_user
|
||||
row.prop(settings, "presence_show_far_user")
|
||||
if settings.presence_show_session_status :
|
||||
row = col.column()
|
||||
row.active = settings.presence_show_session_status
|
||||
row.prop(pref, "presence_hud_scale", expand=True)
|
||||
row = col.column(align=True)
|
||||
row.active = settings.presence_show_session_status
|
||||
row.prop(pref, "presence_hud_hpos", expand=True)
|
||||
row.prop(pref, "presence_hud_vpos", expand=True)
|
||||
|
||||
|
||||
classes = (
|
||||
SESSION_UL_users,
|
||||
|
@ -38,6 +38,14 @@ from replication.constants import (STATE_ACTIVE, STATE_AUTH,
|
||||
STATE_LOBBY,
|
||||
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):
|
||||
for item in list:
|
||||
@ -101,23 +109,25 @@ def get_state_str(state):
|
||||
|
||||
|
||||
def clean_scene():
|
||||
to_delete = [f for f in dir(bpy.data) if f not in ['brushes', 'palettes']]
|
||||
for type_name in to_delete:
|
||||
try:
|
||||
sub_collection_to_avoid = [bpy.data.linestyles['LineStyle'], bpy.data.materials['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)
|
||||
except:
|
||||
continue
|
||||
except:
|
||||
continue
|
||||
|
||||
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):
|
||||
return [obj.uuid for obj in scene.objects if obj.select_get(view_layer=active_view_layer)]
|
||||
|
||||
|
@ -12,6 +12,8 @@ def test_scene(clear_blend):
|
||||
get_preferences().sync_flags.sync_render_settings = True
|
||||
|
||||
datablock = bpy.data.scenes.new("toto")
|
||||
datablock.timeline_markers.new('toto', frame=10)
|
||||
datablock.timeline_markers.new('tata', frame=1)
|
||||
datablock.view_settings.use_curve_mapping = True
|
||||
# Test
|
||||
implementation = BlScene()
|
||||
|
Reference in New Issue
Block a user