Compare commits

..

38 Commits

Author SHA1 Message Date
060b7507b6 feat: initial server request implementation along with a demo operator (SessionGetInfo)
Related to #222
2021-07-21 15:43:21 +02:00
a4f9f6e051 fix: replication dependencies conflicts 2021-07-20 16:19:53 +02:00
10de88cdc9 fix: old replication installation conflicts 2021-07-20 16:06:24 +02:00
e4fa34c984 fix: addon version number 2021-07-20 15:37:11 +02:00
0dd685d009 doc: add missing presence flags 2021-07-20 15:11:38 +02:00
3e8c30c0ab fix: supported datablocks in readme 2021-07-20 14:59:30 +02:00
21cc3cd917 fix: update readme to reflect changes 2021-07-20 14:57:52 +02:00
81e620ee3d fix: documentations capture for 0.4.0 2021-07-20 14:50:33 +02:00
fb9bd108bd feat: update changelog to reflect v0.4.0 version 2021-07-20 14:19:33 +02:00
cab6625399 Merge branch '219-lock-annotation-doesn-t-sync' into 'develop'
Resolve "Lock annotation doesn't sync"

See merge request slumber/multi-user!143
2021-07-14 10:41:32 +00:00
1b81251a11 fix: annotation lock 2021-07-14 12:38:30 +02:00
77bf269fb5 Merge branch '221-optimize-user-selection-draw-code' into 'develop'
Resolve "Optimize user selection draw code"

See merge request slumber/multi-user!142
2021-07-13 14:37:09 +00:00
1e675132d4 fix: collection instances index offset 2021-07-13 16:33:46 +02:00
781287c390 refactor: use one drawcall for all selection bbox 2021-07-13 15:45:08 +02:00
d4476baa1b Merge branch '220-batch-right-selection-update' into 'develop'
Resolve "Batch right selection update"

See merge request slumber/multi-user!141
2021-07-12 10:20:23 +00:00
467e98906e feat: Batch right selection update
Related to https://gitlab.com/slumber/multi-user/-/issues/220
2021-07-12 12:06:45 +02:00
64a25f94a3 fix: gpencil material loading error
Now loading gpencil materials from uuid
2021-07-09 16:59:59 +02:00
e6996316be Merge branch '215-annotations-doesn-t-sync-correctly' into 'develop'
Resolve "Annotations doesn't sync correctly"

See merge request slumber/multi-user!138
2021-07-07 08:18:49 +00:00
cf4cd94096 refactor: remove gpencil dump stroke legacy
Related to #166 and #215
2021-07-07 10:15:23 +02:00
e9ab633aac fix: annotations updates
Related to #215
2021-07-06 16:06:14 +02:00
297639e80f fix: crash on changing workspace change 2021-07-06 15:39:19 +02:00
f0cc63b6f0 Merge branch '214-animated-object-transform-not-correctly-sync' into 'develop'
Resolve "Animated object transform not correctly sync"

See merge request slumber/multi-user!137
2021-07-06 12:32:39 +00:00
d433e8f241 fix: transform offset for object animated with a curve constraint
Related to #214
2021-07-06 14:29:20 +02:00
963a551a1e Merge branch '206-draw-active-mode-in-the-object-presence-overlay-2' into 'develop'
Draw active mode in the object presence overlay

See merge request slumber/multi-user!131
2021-07-01 12:57:01 +00:00
8926ab44e1 Merge branch '201-improved-image-support' into 'develop'
Resolve "Improved image support"

See merge request slumber/multi-user!136
2021-07-01 09:55:47 +00:00
a207c51973 fix: image renamin support
fix: sync Color Space Settings

related to #201
2021-06-29 15:59:26 +02:00
e706c8e0bf Merge branch '209-adding-a-scene-create-node-duplicates' into 'develop'
Resolve "Adding a scene create node duplicates"

See merge request slumber/multi-user!135
2021-06-28 08:30:22 +00:00
e590e896da fix: scene duplicates by using data instead of the update id
Related to #209
2021-06-28 10:27:04 +02:00
4140b62a8e Merge branch '119-add-timeline-marker-sync' into 'develop'
Resolve "Add timeline marker sync"

See merge request slumber/multi-user!133
2021-06-24 15:52:12 +00:00
6d9c9c4532 fix: timeline marker selection
feat: basic test
2021-06-24 17:45:34 +02:00
e9e1911840 Merge branch '208-late-update-logging-error' into 'develop'
Resolve "Late update logging error"

See merge request slumber/multi-user!134
2021-06-24 15:28:56 +00:00
ab350ca7bc fix: late update logging error
Related to #208
2021-06-24 17:24:08 +02:00
2238a15c11 feat: initial markers support 2021-06-24 15:51:01 +02:00
de73f022e6 merge 2021-06-24 14:52:07 +02:00
f517205647 fix: doc authors 2021-06-24 14:51:00 +02:00
f33c3d8481 fix: doc version 2021-06-24 14:50:12 +02:00
71c69000ec Merge branch '207-repository-panel-filtering-is-boken' into 'develop'
Resolve "Repository panel filtering is boken"

See merge request slumber/multi-user!132
2021-06-24 12:49:06 +00:00
de1e684b3c fix: name filtering 2021-06-24 14:35:59 +02:00
24 changed files with 281 additions and 230 deletions

View File

@ -187,3 +187,33 @@ All notable changes to this project will be documented in this file.
- Sync missing armature bone Roll - Sync missing armature bone Roll
- Sync missing driver data_path - Sync missing driver data_path
- Constraint replication - Constraint replication
## [0.4.0] - 2021-07-20
### Added
- Connection preset system (@Kysios)
- Display connected users active mode (users pannel and viewport) (@Kysios)
- Delta-based replication
- Sync timeline marker
- Sync images settings (@Kysios)
- Sync parent relation type (@Kysios)
- Sync uv project modifier
- Sync FCurves modifiers
### Changed
- User selection optimizations (draw and sync) (@Kysios)
- Improved shapekey syncing performances
- Improved gpencil syncing performances
- Integrate replication as a submodule
- The dependencies are now installed in a folder(blender addon folder) that no longer requires administrative rights
- Presence overlay UI optimization (@Kysios)
### Fixed
- User selection bounding box glitches for non-mesh objects (@Kysios)
- Transforms replication for animated objects
- GPencil fill stroke
- Sculpt and GPencil brushes deleted when joining a session (@Kysios)
- Auto-updater doesn't work for master and develop builds

View File

@ -11,9 +11,8 @@ This tool aims to allow multiple users to work on the same scene over the networ
## Quick installation ## Quick installation
1. Download latest release [multi_user.zip](https://gitlab.com/slumber/multi-user/-/jobs/artifacts/master/download?job=build). 1. Download [latest build](https://gitlab.com/slumber/multi-user/-/jobs/artifacts/develop/download?job=build) or [stable build](https://gitlab.com/slumber/multi-user/-/jobs/artifacts/master/download?job=build).
2. Run blender as administrator (dependencies installation). 2. Install last_version.zip from your addon preferences.
3. Install last_version.zip from your addon preferences.
[Dependencies](#dependencies) will be automatically added to your blender python during installation. [Dependencies](#dependencies) will be automatically added to your blender python during installation.
@ -30,7 +29,7 @@ See the [troubleshooting guide](https://slumber.gitlab.io/multi-user/getting_sta
Currently, not all data-block are supported for replication over the wire. The following list summarizes the status for each ones. Currently, not all data-block are supported for replication over the wire. The following list summarizes the status for each ones.
| Name | Status | Comment | | Name | Status | Comment |
| -------------- | :----: | :----------------------------------------------------------: | | -------------- | :----: | :---------------------------------------------------------------------: |
| action | ✔️ | | | action | ✔️ | |
| camera | ✔️ | | | camera | ✔️ | |
| collection | ✔️ | | | collection | ✔️ | |
@ -48,16 +47,16 @@ Currently, not all data-block are supported for replication over the wire. The f
| volumes | ✔️ | | | volumes | ✔️ | |
| lightprobes | ✔️ | | | lightprobes | ✔️ | |
| physics | ✔️ | | | physics | ✔️ | |
| textures | ✔️ | |
| curve | ❗ | Nurbs surfaces not supported | | curve | ❗ | Nurbs surfaces not supported |
| textures | ❗ | Supported for modifiers/materials/geo nodes only | | armature | ❗ | Only for Mesh. [Planned for GPencil](https://gitlab.com/slumber/multi-user/-/issues/161). Not stable yet |
| armature | ❗ | Not stable |
| particles | ❗ | The cache isn't syncing. | | particles | ❗ | The cache isn't syncing. |
| speakers | ❗ | [Partial](https://gitlab.com/slumber/multi-user/-/issues/65) | | speakers | ❗ | [Partial](https://gitlab.com/slumber/multi-user/-/issues/65) |
| vse | ❗ | Mask and Clip not supported yet | | vse | ❗ | Mask and Clip not supported yet |
| libraries | | Partial | | libraries | | |
| nla | ❌ | | | nla | ❌ | |
| texts | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/81) | | texts | ❌ | [Planned for v0.5.0](https://gitlab.com/slumber/multi-user/-/issues/81) |
| compositing | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/46) | | compositing | ❌ | [Planned for v0.5.0](https://gitlab.com/slumber/multi-user/-/issues/46) |

View File

@ -19,10 +19,10 @@ import sys
project = 'multi-user' project = 'multi-user'
copyright = '2020, Swann Martinez' copyright = '2020, Swann Martinez'
author = 'Swann Martinez, with contributions from Poochy' author = 'Swann Martinez, Poochy, Fabian'
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
release = '0.2.0' release = '0.5.0-develop'
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -215,8 +215,10 @@ One of the most vital tools is the **Online user panel**. It lists all connected
users' information including your own: users' information including your own:
* **Role** : if a user is an admin or a regular user. * **Role** : if a user is an admin or a regular user.
* **Location**: Where the user is actually working. * **Username** : Name of the user.
* **Mode** : User's active editing mode (edit_mesh, paint,etc.).
* **Frame**: When (on which frame) the user is working. * **Frame**: When (on which frame) the user is working.
* **Location**: Where the user is actually working.
* **Ping**: user's connection delay in milliseconds * **Ping**: user's connection delay in milliseconds
.. figure:: img/quickstart_users.png .. figure:: img/quickstart_users.png
@ -273,6 +275,7 @@ it draw users' related information in your viewport such as:
* Username * Username
* User point of view * User point of view
* User active mode
* User selection * User selection
.. figure:: img/quickstart_presence.png .. figure:: img/quickstart_presence.png

View File

@ -19,7 +19,7 @@
bl_info = { bl_info = {
"name": "Multi-User", "name": "Multi-User",
"author": "Swann Martinez", "author": "Swann Martinez",
"version": (0, 5, 0), "version": (0, 4, 0),
"description": "Enable real-time collaborative workflow inside blender", "description": "Enable real-time collaborative workflow inside blender",
"blender": (2, 82, 0), "blender": (2, 82, 0),
"location": "3D View > Sidebar > Multi-User tab", "location": "3D View > Sidebar > Multi-User tab",

View File

@ -56,7 +56,7 @@ class BlCamera(ReplicatedDatablock):
background_images = data.get('background_images') background_images = data.get('background_images')
datablock.background_images.clear() datablock.background_images.clear()
# TODO: Use image uuid
if background_images: if background_images:
for img_name, img_data in background_images.items(): for img_name, img_data in background_images.items():
img_id = img_data.get('image') img_id = img_data.get('image')

View File

@ -28,7 +28,8 @@ from replication.protocol import ReplicatedDatablock
from .bl_datablock import resolve_datablock_from_uuid from .bl_datablock import resolve_datablock_from_uuid
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
from ..utils import get_preferences from ..utils import get_preferences
from ..timers import is_annotating
from .bl_material import load_materials_slots, dump_materials_slots
STROKE_POINT = [ STROKE_POINT = [
'co', 'co',
@ -65,36 +66,9 @@ def dump_stroke(stroke):
:param stroke: target grease pencil stroke :param stroke: target grease pencil stroke
:type stroke: bpy.types.GPencilStroke :type stroke: bpy.types.GPencilStroke
:return: dict :return: (p_count, p_data)
""" """
return (len(stroke.points), np_dump_collection(stroke.points, STROKE_POINT))
assert(stroke)
dumper = Dumper()
dumper.include_filter = [
"aspect",
"display_mode",
"draw_cyclic",
"end_cap_mode",
"hardeness",
"line_width",
"material_index",
"start_cap_mode",
"uv_rotation",
"uv_scale",
"uv_translation",
"vertex_color_fill",
]
dumped_stroke = dumper.dump(stroke)
# Stoke points
p_count = len(stroke.points)
dumped_stroke['p_count'] = p_count
dumped_stroke['points'] = np_dump_collection(stroke.points, STROKE_POINT)
# TODO: uv_factor, uv_rotation
return dumped_stroke
def load_stroke(stroke_data, stroke): def load_stroke(stroke_data, stroke):
@ -107,12 +81,12 @@ def load_stroke(stroke_data, stroke):
""" """
assert(stroke and stroke_data) assert(stroke and stroke_data)
stroke.points.add(stroke_data["p_count"]) stroke.points.add(stroke_data[0])
np_load_collection(stroke_data['points'], stroke.points, STROKE_POINT) np_load_collection(stroke_data[1], stroke.points, STROKE_POINT)
# HACK: Temporary fix to trigger a BKE_gpencil_stroke_geometry_update to # HACK: Temporary fix to trigger a BKE_gpencil_stroke_geometry_update to
# fix fill issues # fix fill issues
stroke.uv_scale = stroke_data["uv_scale"] stroke.uv_scale = 1.0
def dump_frame(frame): def dump_frame(frame):
@ -147,10 +121,12 @@ def load_frame(frame_data, frame):
assert(frame and frame_data) assert(frame and frame_data)
# Load stroke points
for stroke_data in frame_data['strokes_points']: for stroke_data in frame_data['strokes_points']:
target_stroke = frame.strokes.new() target_stroke = frame.strokes.new()
load_stroke(stroke_data, target_stroke) load_stroke(stroke_data, target_stroke)
# Load stroke metadata
np_load_collection(frame_data['strokes'], frame.strokes, STROKE) np_load_collection(frame_data['strokes'], frame.strokes, STROKE)
@ -170,7 +146,6 @@ def dump_layer(layer):
'opacity', 'opacity',
'channel_color', 'channel_color',
'color', 'color',
# 'thickness', #TODO: enabling only for annotation
'tint_color', 'tint_color',
'tint_factor', 'tint_factor',
'vertex_paint_opacity', 'vertex_paint_opacity',
@ -187,7 +162,7 @@ def dump_layer(layer):
'hide', 'hide',
'annotation_hide', 'annotation_hide',
'lock', 'lock',
# 'lock_frame', 'lock_frame',
# 'lock_material', # 'lock_material',
# 'use_mask_layer', # 'use_mask_layer',
'use_lights', 'use_lights',
@ -195,12 +170,13 @@ def dump_layer(layer):
'select', 'select',
'show_points', 'show_points',
'show_in_front', 'show_in_front',
# 'thickness'
# 'parent', # 'parent',
# 'parent_type', # 'parent_type',
# 'parent_bone', # 'parent_bone',
# 'matrix_inverse', # 'matrix_inverse',
] ]
if layer.id_data.is_annotation: if layer.thickness != 0:
dumper.include_filter.append('thickness') dumper.include_filter.append('thickness')
dumped_layer = dumper.dump(layer) dumped_layer = dumper.dump(layer)
@ -255,10 +231,10 @@ class BlGpencil(ReplicatedDatablock):
@staticmethod @staticmethod
def load(data: dict, datablock: object): def load(data: dict, datablock: object):
datablock.materials.clear() # MATERIAL SLOTS
if "materials" in data.keys(): src_materials = data.get('materials', None)
for mat in data['materials']: if src_materials:
datablock.materials.append(bpy.data.materials[mat]) load_materials_slots(src_materials, datablock.materials)
loader = Loader() loader = Loader()
loader.load(datablock, data) loader.load(datablock, data)
@ -286,7 +262,6 @@ class BlGpencil(ReplicatedDatablock):
dumper = Dumper() dumper = Dumper()
dumper.depth = 2 dumper.depth = 2
dumper.include_filter = [ dumper.include_filter = [
'materials',
'name', 'name',
'zdepth_offset', 'zdepth_offset',
'stroke_thickness_space', 'stroke_thickness_space',
@ -294,7 +269,7 @@ class BlGpencil(ReplicatedDatablock):
'stroke_depth_order' 'stroke_depth_order'
] ]
data = dumper.dump(datablock) data = dumper.dump(datablock)
data['materials'] = dump_materials_slots(datablock.materials)
data['layers'] = {} data['layers'] = {}
for layer in datablock.layers: for layer in datablock.layers:
@ -323,7 +298,8 @@ class BlGpencil(ReplicatedDatablock):
return bpy.context.mode == 'OBJECT' \ return bpy.context.mode == 'OBJECT' \
or layer_changed(datablock, data) \ or layer_changed(datablock, data) \
or frame_changed(data) \ or frame_changed(data) \
or get_preferences().sync_flags.sync_during_editmode or get_preferences().sync_flags.sync_during_editmode \
or is_annotating(bpy.context)
_type = bpy.types.GreasePencil _type = bpy.types.GreasePencil
_class = BlGpencil _class = BlGpencil

View File

@ -69,11 +69,12 @@ class BlImage(ReplicatedDatablock):
@staticmethod @staticmethod
def load(data: dict, datablock: object): def load(data: dict, datablock: object):
loader = Loader() loader = Loader()
loader.load(data, datablock) loader.load(datablock, data)
# datablock.name = data.get('name')
datablock.source = 'FILE' datablock.source = 'FILE'
datablock.filepath_raw = get_filepath(data['filename']) datablock.filepath_raw = get_filepath(data['filename'])
color_space_name = data["colorspace_settings"]["name"] color_space_name = data.get("colorspace")
if color_space_name: if color_space_name:
datablock.colorspace_settings.name = color_space_name datablock.colorspace_settings.name = color_space_name
@ -92,12 +93,10 @@ class BlImage(ReplicatedDatablock):
"name", "name",
# 'source', # 'source',
'size', 'size',
'height', 'alpha_mode']
'alpha',
'float_buffer',
'alpha_mode',
'colorspace_settings']
data.update(dumper.dump(datablock)) data.update(dumper.dump(datablock))
data['colorspace'] = datablock.colorspace_settings.name
return data return data
@staticmethod @staticmethod
@ -132,10 +131,7 @@ class BlImage(ReplicatedDatablock):
if datablock.is_dirty: if datablock.is_dirty:
datablock.save() datablock.save()
if not data or (datablock and (datablock.name != data.get('name'))):
return True return True
else:
return False
_type = bpy.types.Image _type = bpy.types.Image
_class = BlImage _class = BlImage

View File

@ -124,8 +124,7 @@ def dump_node(node: bpy.types.ShaderNode) -> dict:
"show_preview", "show_preview",
"show_texture", "show_texture",
"outputs", "outputs",
"width_hidden", "width_hidden"
"image"
] ]
dumped_node = node_dumper.dump(node) dumped_node = node_dumper.dump(node)
@ -388,11 +387,10 @@ def load_materials_slots(src_materials: list, dst_materials: bpy.types.bpy_prop_
for mat_uuid, mat_name in src_materials: for mat_uuid, mat_name in src_materials:
mat_ref = None mat_ref = None
if mat_uuid is not None: if mat_uuid:
mat_ref = get_datablock_from_uuid(mat_uuid, None) mat_ref = get_datablock_from_uuid(mat_uuid, None)
else: else:
mat_ref = bpy.data.materials[mat_name] mat_ref = bpy.data.materials[mat_name]
dst_materials.append(mat_ref) dst_materials.append(mat_ref)

View File

@ -620,10 +620,8 @@ class BlObject(ReplicatedDatablock):
transform = data.get('transforms', None) transform = data.get('transforms', None)
if transform: if transform:
datablock.matrix_parent_inverse = mathutils.Matrix( datablock.matrix_parent_inverse = mathutils.Matrix(transform['matrix_parent_inverse'])
transform['matrix_parent_inverse'])
datablock.matrix_basis = mathutils.Matrix(transform['matrix_basis']) datablock.matrix_basis = mathutils.Matrix(transform['matrix_basis'])
datablock.matrix_local = mathutils.Matrix(transform['matrix_local'])
@staticmethod @staticmethod

View File

@ -403,8 +403,9 @@ class BlScene(ReplicatedDatablock):
datablock.world = bpy.data.worlds[data['world']] datablock.world = bpy.data.worlds[data['world']]
# Annotation # Annotation
if 'grease_pencil' in data.keys(): gpencil_uid = data.get('grease_pencil')
datablock.grease_pencil = bpy.data.grease_pencils[data['grease_pencil']] if gpencil_uid:
datablock.grease_pencil = resolve_datablock_from_uuid(gpencil_uid, bpy.data.grease_pencils)
if get_preferences().sync_flags.sync_render_settings: if get_preferences().sync_flags.sync_render_settings:
if 'eevee' in data.keys(): if 'eevee' in data.keys():
@ -445,6 +446,15 @@ class BlScene(ReplicatedDatablock):
elif datablock.sequence_editor and not sequences: elif datablock.sequence_editor and not sequences:
datablock.sequence_editor_clear() 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 # FIXME: Find a better way after the replication big refacotoring
# Keep other user from deleting collection object by flushing their history # Keep other user from deleting collection object by flushing their history
flush_history() flush_history()
@ -461,7 +471,6 @@ class BlScene(ReplicatedDatablock):
'name', 'name',
'world', 'world',
'id', 'id',
'grease_pencil',
'frame_start', 'frame_start',
'frame_end', 'frame_end',
'frame_step', 'frame_step',
@ -517,6 +526,13 @@ class BlScene(ReplicatedDatablock):
dumped_sequences[seq.name] = dump_sequence(seq) dumped_sequences[seq.name] = dump_sequence(seq)
data['sequences'] = dumped_sequences 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]
if datablock.grease_pencil:
data['grease_pencil'] = datablock.grease_pencil.uuid
return data return data
@staticmethod @staticmethod

View File

@ -52,7 +52,8 @@ def sanitize_deps_graph(remove_nodes: bool = False):
def update_external_dependencies(): def update_external_dependencies():
"""Force external dependencies(files such as images) evaluation """Force external dependencies(files such as images) evaluation
""" """
nodes_ids = [n.uuid for n in session.repository.graph.values() if n.data['type_id'] in ['WindowsPath', 'PosixPath']] 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: for node_id in nodes_ids:
node = session.repository.graph.get(node_id) node = session.repository.graph.get(node_id)
if node and node.owner in [session.repository.username, RP_COMMON]: if node and node.owner in [session.repository.username, RP_COMMON]:
@ -103,7 +104,8 @@ def on_scene_update(scene):
else: else:
continue continue
elif isinstance(update.id, bpy.types.Scene): elif isinstance(update.id, bpy.types.Scene):
scn_uuid = porcelain.add(session.repository, update.id) scene = bpy.data.scenes.get(update.id.name)
scn_uuid = porcelain.add(session.repository, scene)
porcelain.commit(session.repository, scn_uuid) porcelain.commit(session.repository, scn_uuid)
porcelain.push(session.repository, 'origin', scn_uuid) porcelain.push(session.repository, 'origin', scn_uuid)

View File

@ -273,8 +273,7 @@ class SessionStartOperator(bpy.types.Operator):
session_update = timers.SessionStatusUpdate() session_update = timers.SessionStatusUpdate()
session_user_sync = timers.SessionUserSync() session_user_sync = timers.SessionUserSync()
session_background_executor = timers.MainThreadExecutor( session_background_executor = timers.MainThreadExecutor(execution_queue=background_execution_queue)
execution_queue=background_execution_queue)
session_listen = timers.SessionListenTimer(timeout=0.001) session_listen = timers.SessionListenTimer(timeout=0.001)
session_listen.register() session_listen.register()
@ -286,6 +285,7 @@ class SessionStartOperator(bpy.types.Operator):
deleyables.append(session_update) deleyables.append(session_update)
deleyables.append(session_user_sync) deleyables.append(session_user_sync)
deleyables.append(session_listen) deleyables.append(session_listen)
deleyables.append(timers.AnnotationUpdates())
return {"FINISHED"} return {"FINISHED"}
@ -784,6 +784,22 @@ class SessionStopAutoSaveOperator(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
class SessionGetInfo(bpy.types.Operator):
bl_idname = "session.get_info"
bl_label = "Get session info"
bl_description = "Get session info"
target_server: bpy.props.StringProperty(default="127.0.0.1:5555")
@classmethod
def poll(cls, context):
return (session.state != STATE_ACTIVE)
def execute(self, context):
infos = porcelain.request_session_info(self.target_server, timeout=100)
logging.info(f"Session info: {infos}")
return {'FINISHED'}
class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper): class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
bl_idname = "session.load" bl_idname = "session.load"
@ -922,6 +938,7 @@ classes = (
SessionPurgeOperator, SessionPurgeOperator,
SessionPresetServerAdd, SessionPresetServerAdd,
SessionPresetServerRemove, SessionPresetServerRemove,
SessionGetInfo,
) )

View File

@ -94,18 +94,21 @@ def project_to_viewport(region: bpy.types.Region, rv3d: bpy.types.RegionView3D,
return [target.x, target.y, target.z] return [target.x, target.y, target.z]
def bbox_from_obj(obj: bpy.types.Object) -> list: def bbox_from_obj(obj: bpy.types.Object, index: int = 1) -> list:
""" Generate a bounding box for a given object by using its world matrix """ Generate a bounding box for a given object by using its world matrix
:param obj: target object :param obj: target object
:type obj: bpy.types.Object :type obj: bpy.types.Object
:param index: indice offset
:type index: int
:return: list of 8 points [(x,y,z),...], list of 12 link between these points [(1,2),...] :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 radius = 1.0 # Radius of the bounding box
index = 8*index
vertex_indices = ( vertex_indices = (
(0, 1), (0, 2), (1, 3), (2, 3), (0+index, 1+index), (0+index, 2+index), (1+index, 3+index), (2+index, 3+index),
(4, 5), (4, 6), (5, 7), (6, 7), (4+index, 5+index), (4+index, 6+index), (5+index, 7+index), (6+index, 7+index),
(0, 4), (1, 5), (2, 6), (3, 7)) (0+index, 4+index), (1+index, 5+index), (2+index, 6+index), (3+index, 7+index))
if obj.type == 'EMPTY': if obj.type == 'EMPTY':
radius = obj.empty_display_size radius = obj.empty_display_size
@ -117,9 +120,12 @@ def bbox_from_obj(obj: bpy.types.Object) -> list:
radius = obj.data.display_size radius = obj.data.display_size
elif hasattr(obj, 'bound_box'): elif hasattr(obj, 'bound_box'):
vertex_indices = ( vertex_indices = (
(0, 1), (1, 2), (2, 3), (0, 3), (0+index, 1+index), (1+index, 2+index),
(4, 5), (5, 6), (6, 7), (4, 7), (2+index, 3+index), (0+index, 3+index),
(0, 4), (1, 5), (2, 6), (3, 7)) (4+index, 5+index), (5+index, 6+index),
(6+index, 7+index), (4+index, 7+index),
(0+index, 4+index), (1+index, 5+index),
(2+index, 6+index), (3+index, 7+index))
vertex_pos = get_bb_coords_from_obj(obj) vertex_pos = get_bb_coords_from_obj(obj)
return vertex_pos, vertex_indices return vertex_pos, vertex_indices
@ -136,26 +142,21 @@ def bbox_from_obj(obj: bpy.types.Object) -> list:
return vertex_pos, vertex_indices return vertex_pos, vertex_indices
def bbox_from_instance_collection(ic: bpy.types.Object) -> list: def bbox_from_instance_collection(ic: bpy.types.Object, index: int = 0) -> list:
""" Generate a bounding box for a given instance collection by using its objects """ Generate a bounding box for a given instance collection by using its objects
:param ic: target instance collection :param ic: target instance collection
:type ic: bpy.types.Object :type ic: bpy.types.Object
:param radius: bounding box radius :param index: indice offset
:type radius: float :type index: int
:return: list of 8*objs points [(x,y,z),...], tuple of 12*objs link between these points [(1,2),...] :return: list of 8*objs points [(x,y,z),...], tuple of 12*objs link between these points [(1,2),...]
""" """
vertex_pos = [] vertex_pos = []
vertex_indices = () vertex_indices = ()
for obj_index, obj in enumerate(ic.instance_collection.objects): for obj_index, obj in enumerate(ic.instance_collection.objects):
vertex_pos_temp, vertex_indices_temp = bbox_from_obj(obj) vertex_pos_temp, vertex_indices_temp = bbox_from_obj(obj, index=index+obj_index)
vertex_pos += vertex_pos_temp 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 vertex_indices += vertex_indices_temp
bbox_corners = [ic.matrix_world @ mathutils.Vector(vertex) for vertex in vertex_pos] bbox_corners = [ic.matrix_world @ mathutils.Vector(vertex) for vertex in vertex_pos]
@ -322,6 +323,8 @@ class UserSelectionWidget(Widget):
username): username):
self.username = username self.username = username
self.settings = bpy.context.window_manager.session self.settings = bpy.context.window_manager.session
self.current_selection_ids = []
self.current_selected_objects = []
@property @property
def data(self): def data(self):
@ -331,6 +334,15 @@ class UserSelectionWidget(Widget):
else: else:
return None return None
@property
def selected_objects(self):
user_selection = self.data.get('selected_objects')
if self.current_selection_ids != user_selection:
self.current_selected_objects = [find_from_attr("uuid", uid, bpy.data.objects) for uid in user_selection]
self.current_selection_ids = user_selection
return self.current_selected_objects
def poll(self): def poll(self):
if self.data is None: if self.data is None:
return False return False
@ -345,22 +357,27 @@ class UserSelectionWidget(Widget):
self.settings.enable_presence self.settings.enable_presence
def draw(self): def draw(self):
user_selection = self.data.get('selected_objects') vertex_pos = []
for select_obj in user_selection: vertex_ind = []
obj = find_from_attr("uuid", select_obj, bpy.data.objects) collection_offset = 0
if not obj: for obj_index, obj in enumerate(self.selected_objects):
return if obj is None:
if obj.instance_collection: continue
vertex_pos, vertex_indices = bbox_from_instance_collection(obj) obj_index+=collection_offset
if hasattr(obj, 'instance_collection') and obj.instance_collection:
bbox_pos, bbox_ind = bbox_from_instance_collection(obj, index=obj_index)
collection_offset+=len(obj.instance_collection.objects)-1
else : else :
vertex_pos, vertex_indices = bbox_from_obj(obj) bbox_pos, bbox_ind = bbox_from_obj(obj, index=obj_index)
vertex_pos += bbox_pos
vertex_ind += bbox_ind
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR') shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
batch = batch_for_shader( batch = batch_for_shader(
shader, shader,
'LINES', 'LINES',
{"pos": vertex_pos}, {"pos": vertex_pos},
indices=vertex_indices) indices=vertex_ind)
shader.bind() shader.bind()
shader.uniform_float("color", self.data.get('color')) shader.uniform_float("color", self.data.get('color'))

View File

@ -41,7 +41,8 @@ this.registry = dict()
def is_annotating(context: bpy.types.Context): def is_annotating(context: bpy.types.Context):
""" Check if the annotate mode is enabled """ Check if the annotate mode is enabled
""" """
return bpy.context.workspace.tools.from_space_view3d_mode('OBJECT', create=False).idname == 'builtin.annotate' active_tool = bpy.context.workspace.tools.from_space_view3d_mode('OBJECT', create=False)
return (active_tool and active_tool.idname == 'builtin.annotate')
class Timer(object): class Timer(object):
@ -136,22 +137,15 @@ class ApplyTimer(Timer):
force=True) force=True)
class DynamicRightSelectTimer(Timer): class AnnotationUpdates(Timer):
def __init__(self, timeout=.1): def __init__(self, timeout=1):
super().__init__(timeout)
self._last_selection = []
self._user = None
self._annotating = False self._annotating = False
self._settings = utils.get_preferences()
super().__init__(timeout)
def execute(self): def execute(self):
settings = utils.get_preferences()
if session and session.state == STATE_ACTIVE: if session and session.state == STATE_ACTIVE:
# Find user
if self._user is None:
self._user = session.online_users.get(settings.username)
if self._user:
ctx = bpy.context ctx = bpy.context
annotation_gp = ctx.scene.grease_pencil annotation_gp = ctx.scene.grease_pencil
@ -168,67 +162,76 @@ class DynamicRightSelectTimer(Timer):
logging.debug( logging.debug(
"Getting the right on the annotation GP") "Getting the right on the annotation GP")
porcelain.lock(session.repository, porcelain.lock(session.repository,
registered_gp.uuid, [registered_gp.uuid],
ignore_warnings=True, ignore_warnings=True,
affect_dependencies=False) affect_dependencies=False)
if registered_gp.owner == settings.username: if registered_gp.owner == self._settings.username:
gp_node = session.repository.graph.get(annotation_gp.uuid) porcelain.commit(session.repository, annotation_gp.uuid)
porcelain.commit(session.repository, gp_node.uuid) porcelain.push(session.repository, 'origin', annotation_gp.uuid)
porcelain.push(session.repository, 'origin', gp_node.uuid)
elif self._annotating: elif self._annotating:
porcelain.unlock(session.repository, porcelain.unlock(session.repository,
registered_gp.uuid, [registered_gp.uuid],
ignore_warnings=True, ignore_warnings=True,
affect_dependencies=False) affect_dependencies=False)
self._annotating = False
current_selection = utils.get_selected_objects( class DynamicRightSelectTimer(Timer):
def __init__(self, timeout=.1):
super().__init__(timeout)
self._last_selection = set()
self._user = None
def execute(self):
settings = utils.get_preferences()
if session and session.state == STATE_ACTIVE:
# Find user
if self._user is None:
self._user = session.online_users.get(settings.username)
if self._user:
current_selection = set(utils.get_selected_objects(
bpy.context.scene, bpy.context.scene,
bpy.data.window_managers['WinMan'].windows[0].view_layer bpy.data.window_managers['WinMan'].windows[0].view_layer
) ))
if current_selection != self._last_selection: if current_selection != self._last_selection:
obj_common = [ to_lock = list(current_selection.difference(self._last_selection))
o for o in self._last_selection if o not in current_selection] to_release = list(self._last_selection.difference(current_selection))
obj_ours = [ instances_to_lock = list()
o for o in current_selection if o not in self._last_selection]
# change old selection right to common
for obj in obj_common:
node = session.repository.graph.get(obj)
if node and (node.owner == settings.username or node.owner == RP_COMMON):
recursive = True
if node.data and 'instance_type' in node.data.keys():
recursive = node.data['instance_type'] != 'COLLECTION'
try:
porcelain.unlock(session.repository,
node.uuid,
ignore_warnings=True,
affect_dependencies=recursive)
except NonAuthorizedOperationError:
logging.warning(
f"Not authorized to change {node} owner")
# change new selection to our
for obj in obj_ours:
node = session.repository.graph.get(obj)
if node and node.owner == RP_COMMON:
recursive = True
if node.data and 'instance_type' in node.data.keys():
recursive = node.data['instance_type'] != 'COLLECTION'
for node_id in to_lock:
node = session.repository.graph.get(node_id)
instance_mode = node.data.get('instance_type')
if instance_mode and instance_mode == 'COLLECTION':
to_lock.remove(node_id)
instances_to_lock.append(node_id)
if instances_to_lock:
try: try:
porcelain.lock(session.repository, porcelain.lock(session.repository,
node.uuid, instances_to_lock,
ignore_warnings=True, ignore_warnings=True,
affect_dependencies=recursive) affect_dependencies=False)
except NonAuthorizedOperationError: except NonAuthorizedOperationError as e:
logging.warning( logging.warning(e)
f"Not authorized to change {node} owner")
else: if to_release:
return try:
porcelain.unlock(session.repository,
to_release,
ignore_warnings=True,
affect_dependencies=True)
except NonAuthorizedOperationError as e:
logging.warning(e)
if to_lock:
try:
porcelain.lock(session.repository,
to_lock,
ignore_warnings=True,
affect_dependencies=True)
except NonAuthorizedOperationError as e:
logging.warning(e)
self._last_selection = current_selection self._last_selection = current_selection
@ -242,17 +245,16 @@ class DynamicRightSelectTimer(Timer):
# Fix deselection until right managment refactoring (with Roles concepts) # Fix deselection until right managment refactoring (with Roles concepts)
if len(current_selection) == 0 : if len(current_selection) == 0 :
owned_keys = [k for k, v in session.repository.graph.items() if v.owner==settings.username] owned_keys = [k for k, v in session.repository.graph.items() if v.owner==settings.username]
for key in owned_keys: if owned_keys:
node = session.repository.graph.get(key)
try: try:
porcelain.unlock(session.repository, porcelain.unlock(session.repository,
key, owned_keys,
ignore_warnings=True, ignore_warnings=True,
affect_dependencies=True) affect_dependencies=True)
except NonAuthorizedOperationError: except NonAuthorizedOperationError as e:
logging.warning( logging.warning(e)
f"Not authorized to change {key} owner")
# Objects selectability
for obj in bpy.data.objects: for obj in bpy.data.objects:
object_uuid = getattr(obj, 'uuid', None) object_uuid = getattr(obj, 'uuid', None)
if object_uuid: if object_uuid:

View File

@ -599,20 +599,15 @@ class SESSION_PT_repository(bpy.types.Panel):
# Properties # Properties
owned_nodes = [k for k, v in session.repository.graph.items() if v.owner==settings.username] 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: if runtime_settings.filter_name:
for node_id in filtered_node: filtered_node = [n for n in filtered_node if runtime_settings.filter_name.lower() in session.repository.graph.get(n).data.get('name').lower()]
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)
if filtered_node: if filtered_node:
col = layout.column(align=True) col = layout.column(align=True)
for key in filtered_node: for key in filtered_node:
draw_property(context, col, key) draw_property(context, col, key)
else: else:
layout.row().label(text="Empty") layout.row().label(text="Empty")

View File

@ -12,6 +12,8 @@ def test_scene(clear_blend):
get_preferences().sync_flags.sync_render_settings = True get_preferences().sync_flags.sync_render_settings = True
datablock = bpy.data.scenes.new("toto") 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 datablock.view_settings.use_curve_mapping = True
# Test # Test
implementation = BlScene() implementation = BlScene()