Compare commits

..

20 Commits

Author SHA1 Message Date
Swann Martinez 57fdd492ef Merge branch 'develop' into 'master'
fix: auto-updater operators registration to ensure blender 2.93 compatibility

See merge request slumber/multi-user!117
2021-04-15 13:39:47 +00:00
Swann e538752fbc Merge branch 'master' of gitlab.com:slumber/multi-user into develop 2021-04-15 15:31:59 +02:00
Swann 53eaaa2fcd fix: auto-updater operator registration for blender 2.93 compatibility 2021-04-15 15:28:59 +02:00
Swann Martinez a7e9108bff Merge branch 'develop' into 'master'
v0.3.0

See merge request slumber/multi-user!106
2021-04-14 14:32:24 +00:00
Swann 570909a7c4 fix: prevent field from being dumped if unused
fix: bl_object tests
2021-04-14 16:25:21 +02:00
Swann 736c3df7c4 feat: remove new particle systems
clean: remove logs
2021-04-14 15:50:53 +02:00
Swann 8e606068f3 fix: particle system duplication
feat: update Readme
2021-04-14 15:29:02 +02:00
Swann eb631e2d4b feat: update changelog 0.3.0 release 2021-04-14 14:36:06 +02:00
Swann 70641435cc feat: initial rigid body supports 2021-04-14 12:25:16 +02:00
Swann 552c649d34 feat: physics forcefield and collision support 2021-04-14 11:49:34 +02:00
Swann d9d5a34653 clean: remove libs 2021-04-14 09:56:07 +02:00
Swann 12acd22660 feat: ignore some attributes 2021-04-14 09:54:34 +02:00
Swann 826a59085e feat: particle texture slot support 2021-04-14 09:45:18 +02:00
Swann 5ee4988aca Merge branch '24-particle-support' into develop 2021-04-13 22:45:27 +02:00
Swann cb85a1db4c feat: dual identification for object parents 2021-04-13 14:37:43 +02:00
Swann Martinez ffe419a46e Merge branch 'develop' into 'master'
v0.2.0

See merge request slumber/multi-user!73
2020-12-17 13:34:41 +00:00
Swann Martinez bed33ca6ba Merge branch 'develop' into 'master'
v0.1.1

See merge request slumber/multi-user!54
2020-10-16 09:11:20 +00:00
Swann 56ea93508c Merge branch 'develop' into 24-particle-support 2020-04-03 18:23:29 +02:00
Swann Martinez 5f95eadc1d feat: test particle cache access 2020-03-11 18:37:43 +01:00
Swann Martinez 40ad96b0af feat: initial particle system support
Related to #24
2020-03-11 17:45:56 +01:00
12 changed files with 360 additions and 288 deletions
+30 -1
View File
@@ -157,4 +157,33 @@ All notable changes to this project will be documented in this file.
- Empty and Light object selection highlights
- Material renaming
- Default material nodes input parameters
- blender 2.91 python api compatibility
- blender 2.91 python api compatibility
## [0.3.0] - 2021-04-14
### Added
- Curve material support
- Cycle visibility settings
- Session save/load operator
- Add new scene support
- Physic initial support
- Geometry node initial support
- Blender 2.93 compatibility
### Changed
- Host documentation on Gitlab Page
- Event driven update (from the blender deps graph)
### Fixed
- Vertex group assignation
- Parent relation can't be removed
- Separate object
- Delete animation
- Sync missing holdout option for grease pencil material
- Sync missing `skin_vertices`
- Exception access violation during Undo/Redo
- Sync missing armature bone Roll
- Sync missing driver data_path
- Constraint replication
+30 -30
View File
@@ -29,35 +29,35 @@ 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.
| Name | Status | Comment |
| -------------- | :----: | :--------------------------------------------------------------------------: |
| action | ✔️ | |
| armature | ❗ | Not stable |
| camera | ✔️ | |
| collection | ✔️ | |
| curve | ❗ | Nurbs surfaces not supported |
| gpencil | ✔️ | [Airbrush not supported](https://gitlab.com/slumber/multi-user/-/issues/123) |
| image | ✔️ | |
| mesh | ✔️ | |
| material | ✔️ | |
| node_groups | ❗ | Material only |
| geometry nodes | ✔️ | |
| metaball | ✔️ | |
| object | ✔️ | |
| textures | ❗ | Supported for modifiers/materials only |
| texts | ✔️ | |
| scene | ✔️ | |
| world | ✔️ | |
| lightprobes | ✔️ | |
| compositing | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/46) |
| texts | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/81) |
| nla | ❌ | |
| volumes | ✔️ | |
| particles | ❌ | [On-going](https://gitlab.com/slumber/multi-user/-/issues/24) |
| speakers | ❗ | [Partial](https://gitlab.com/slumber/multi-user/-/issues/65) |
| vse | ❗ | Mask and Clip not supported yet |
| physics | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/45) |
| libraries | ❗ | Partial |
| Name | Status | Comment |
| -------------- | :----: | :----------------------------------------------------------: |
| action | ✔️ | |
| armature | ❗ | Not stable |
| camera | ✔️ | |
| collection | ✔️ | |
| curve | ❗ | Nurbs surfaces not supported |
| gpencil | ✔️ | |
| image | ✔️ | |
| mesh | ✔️ | |
| material | ✔️ | |
| node_groups | ❗ | Material & Geometry only |
| geometry nodes | ✔️ | |
| metaball | ✔️ | |
| object | ✔️ | |
| textures | ❗ | Supported for modifiers/materials/geo nodes only |
| texts | ✔️ | |
| scene | ✔️ | |
| world | ✔️ | |
| lightprobes | ✔️ | |
| compositing | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/46) |
| texts | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/81) |
| nla | ❌ | |
| volumes | ✔️ | |
| particles | ❗ | The cache isn't syncing. |
| speakers | ❗ | [Partial](https://gitlab.com/slumber/multi-user/-/issues/65) |
| vse | ❗ | Mask and Clip not supported yet |
| physics | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/45) |
| libraries | ❗ | Partial |
@@ -70,7 +70,7 @@ I'm working on it.
| Dependencies | Version | Needed |
| ------------ | :-----: | -----: |
| Replication | latest | yes |
| Replication | latest | yes |
+7 -7
View File
@@ -122,13 +122,13 @@ class addon_updater_install_popup(bpy.types.Operator):
# if true, run clean install - ie remove all files before adding new
# equivalent to deleting the addon and reinstalling, except the
# updater folder/backup folder remains
clean_install = bpy.props.BoolProperty(
clean_install: bpy.props.BoolProperty(
name="Clean install",
description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install",
default=False,
options={'HIDDEN'}
)
ignore_enum = bpy.props.EnumProperty(
ignore_enum: bpy.props.EnumProperty(
name="Process update",
description="Decide to install, ignore, or defer new addon update",
items=[
@@ -264,7 +264,7 @@ class addon_updater_update_now(bpy.types.Operator):
# if true, run clean install - ie remove all files before adding new
# equivalent to deleting the addon and reinstalling, except the
# updater folder/backup folder remains
clean_install = bpy.props.BoolProperty(
clean_install: bpy.props.BoolProperty(
name="Clean install",
description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install",
default=False,
@@ -332,7 +332,7 @@ class addon_updater_update_target(bpy.types.Operator):
i+=1
return ret
target = bpy.props.EnumProperty(
target: bpy.props.EnumProperty(
name="Target version to install",
description="Select the version to install",
items=target_version
@@ -341,7 +341,7 @@ class addon_updater_update_target(bpy.types.Operator):
# if true, run clean install - ie remove all files before adding new
# equivalent to deleting the addon and reinstalling, except the
# updater folder/backup folder remains
clean_install = bpy.props.BoolProperty(
clean_install: bpy.props.BoolProperty(
name="Clean install",
description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install",
default=False,
@@ -399,7 +399,7 @@ class addon_updater_install_manually(bpy.types.Operator):
bl_description = "Proceed to manually install update"
bl_options = {'REGISTER', 'INTERNAL'}
error = bpy.props.StringProperty(
error: bpy.props.StringProperty(
name="Error Occurred",
default="",
options={'HIDDEN'}
@@ -461,7 +461,7 @@ class addon_updater_updated_successful(bpy.types.Operator):
bl_description = "Update installation response"
bl_options = {'REGISTER', 'INTERNAL', 'UNDO'}
error = bpy.props.StringProperty(
error: bpy.props.StringProperty(
name="Error Occurred",
default="",
options={'HIDDEN'}
+1
View File
@@ -42,6 +42,7 @@ __all__ = [
# 'bl_sequencer',
'bl_node_group',
'bl_texture',
"bl_particle",
] # Order here defines execution order
if bpy.app.version[1] >= 91:
+1 -1
View File
@@ -69,7 +69,7 @@ class BlMesh(BlDatablock):
loader.load(target, data)
# MATERIAL SLOTS
src_materials = data.get('materials', data.get('material_list'))
src_materials = data.get('materials', None)
if src_materials:
load_materials_slots(src_materials, target.materials)
+109 -6
View File
@@ -54,6 +54,60 @@ def get_node_group_inputs(node_group):
return inputs
# return [inpt.identifer for inpt in node_group.inputs if inpt.type not in IGNORED_SOCKETS]
def dump_physics(target: bpy.types.Object)->dict:
"""
Dump all physics settings from a given object excluding modifier
related physics settings (such as softbody, cloth, dynapaint and fluid)
"""
dumper = Dumper()
dumper.depth = 1
physics_data = {}
# Collisions (collision)
if target.collision and target.collision.use:
physics_data['collision'] = dumper.dump(target.collision)
# Field (field)
if target.field and target.field.type != "NONE":
physics_data['field'] = dumper.dump(target.field)
# Rigid Body (rigid_body)
if target.rigid_body:
physics_data['rigid_body'] = dumper.dump(target.rigid_body)
# Rigid Body constraint (rigid_body_constraint)
if target.rigid_body_constraint:
physics_data['rigid_body_constraint'] = dumper.dump(target.rigid_body_constraint)
return physics_data
def load_physics(dumped_settings: dict, target: bpy.types.Object):
""" Load all physics settings from a given object excluding modifier
related physics settings (such as softbody, cloth, dynapaint and fluid)
"""
loader = Loader()
if 'collision' in dumped_settings:
loader.load(target.collision, dumped_settings['collision'])
if 'field' in dumped_settings:
loader.load(target.field, dumped_settings['field'])
if 'rigid_body' in dumped_settings:
if not target.rigid_body:
bpy.ops.rigidbody.object_add({"object": target})
loader.load(target.rigid_body, dumped_settings['rigid_body'])
elif target.rigid_body:
bpy.ops.rigidbody.object_remove({"object": target})
if 'rigid_body_constraint' in dumped_settings:
if not target.rigid_body_constraint:
bpy.ops.rigidbody.constraint_add({"object": target})
loader.load(target.rigid_body_constraint, dumped_settings['rigid_body_constraint'])
elif target.rigid_body_constraint:
bpy.ops.rigidbody.constraint_remove({"object": target})
def dump_modifier_geometry_node_inputs(modifier: bpy.types.Modifier) -> list:
""" Dump geometry node modifier input properties
@@ -186,7 +240,7 @@ def find_geometry_nodes_dependencies(modifiers: bpy.types.bpy_prop_collection) -
# parameter = mod.get(inpt.identifier)
# if parameter and isinstance(parameter, bpy.types.ID):
# dependencies.append(parameter)
logging.info(dependencies)
return dependencies
@@ -317,9 +371,9 @@ class BlObject(BlDatablock):
loader.load(target.display, data['display'])
# Parenting
parent_id = data.get('parent_id')
parent_id = data.get('parent_uid')
if parent_id:
parent = bpy.data.objects[parent_id]
parent = get_datablock_from_uuid(parent_id[0], bpy.data.objects[parent_id[1]])
# Avoid reloading
if target.parent != parent and parent is not None:
target.parent = parent
@@ -381,6 +435,30 @@ class BlObject(BlDatablock):
load_modifier_geometry_node_inputs(
data['modifiers'][modifier.name], modifier)
particles_modifiers = [
mod for mod in target.modifiers if mod.type == 'PARTICLE_SYSTEM']
for mod in particles_modifiers:
default = mod.particle_system.settings
dumped_particles = data['modifiers'][mod.name]['particle_system']
loader.load(mod.particle_system, dumped_particles)
settings = get_datablock_from_uuid(dumped_particles['settings_uuid'], None)
if settings:
mod.particle_system.settings = settings
# Hack to remove the default generated particle settings
if not default.uuid:
bpy.data.particles.remove(default)
phys_modifiers = [
mod for mod in target.modifiers if mod.type in ['SOFT_BODY', 'CLOTH']]
for mod in phys_modifiers:
loader.load(mod.settings, data['modifiers'][mod.name]['settings'])
# PHYSICS
load_physics(data, target)
transform = data.get('transforms', None)
if transform:
target.matrix_parent_inverse = mathutils.Matrix(
@@ -388,6 +466,7 @@ class BlObject(BlDatablock):
target.matrix_basis = mathutils.Matrix(transform['matrix_basis'])
target.matrix_local = mathutils.Matrix(transform['matrix_local'])
def _dump_implementation(self, data, instance=None):
assert(instance)
@@ -450,7 +529,7 @@ class BlObject(BlDatablock):
# PARENTING
if instance.parent:
data['parent_id'] = instance.parent.name
data['parent_uid'] = (instance.parent.uuid, instance.parent.name)
# MODIFIERS
if hasattr(instance, 'modifiers'):
@@ -459,13 +538,29 @@ class BlObject(BlDatablock):
if modifiers:
dumper.include_filter = None
dumper.depth = 1
dumper.exclude_filter = ['is_active']
for index, modifier in enumerate(modifiers):
data["modifiers"][modifier.name] = dumper.dump(modifier)
dumped_modifier = dumper.dump(modifier)
# hack to dump geometry nodes inputs
if modifier.type == 'NODES':
dumped_inputs = dump_modifier_geometry_node_inputs(
modifier)
data["modifiers"][modifier.name]['inputs'] = dumped_inputs
dumped_modifier['inputs'] = dumped_inputs
elif modifier.type == 'PARTICLE_SYSTEM':
dumper.exclude_filter = [
"is_edited",
"is_editable",
"is_global_hair"
]
dumped_modifier['particle_system'] = dumper.dump(modifier.particle_system)
dumped_modifier['particle_system']['settings_uuid'] = modifier.particle_system.settings.uuid
elif modifier.type in ['SOFT_BODY', 'CLOTH']:
dumped_modifier['settings'] = dumper.dump(modifier.settings)
data["modifiers"][modifier.name] = dumped_modifier
gp_modifiers = getattr(instance, 'grease_pencil_modifiers', None)
if gp_modifiers:
@@ -487,6 +582,7 @@ class BlObject(BlDatablock):
'location']
gp_mod_data['curve'] = curve_dumper.dump(modifier.curve)
# CONSTRAINTS
if hasattr(instance, 'constraints'):
dumper.include_filter = None
@@ -583,6 +679,9 @@ class BlObject(BlDatablock):
]
data['cycles_visibility'] = dumper.dump(instance.cycles_visibility)
# PHYSICS
data.update(dump_physics(instance))
return data
def _resolve_deps_implementation(self):
@@ -592,6 +691,10 @@ class BlObject(BlDatablock):
if self.instance.data:
deps.append(self.instance.data)
# Particle systems
for particle_slot in self.instance.particle_systems:
deps.append(particle_slot.settings)
if self.is_library:
deps.append(self.instance.library)
+90
View File
@@ -0,0 +1,90 @@
import bpy
import mathutils
from . import dump_anything
from .bl_datablock import BlDatablock, get_datablock_from_uuid
def dump_textures_slots(texture_slots: bpy.types.bpy_prop_collection) -> list:
""" Dump every texture slot collection as the form:
[(index, slot_texture_uuid, slot_texture_name), (), ...]
"""
dumped_slots = []
for index, slot in enumerate(texture_slots):
if slot and slot.texture:
dumped_slots.append((index, slot.texture.uuid, slot.texture.name))
return dumped_slots
def load_texture_slots(dumped_slots: list, target_slots: bpy.types.bpy_prop_collection):
"""
"""
for index, slot in enumerate(target_slots):
if slot:
target_slots.clear(index)
for index, slot_uuid, slot_name in dumped_slots:
target_slots.create(index).texture = get_datablock_from_uuid(
slot_uuid, slot_name
)
IGNORED_ATTR = [
"is_embedded_data",
"is_evaluated",
"is_fluid",
"is_library_indirect",
"users"
]
class BlParticle(BlDatablock):
bl_id = "particles"
bl_class = bpy.types.ParticleSettings
bl_icon = "PARTICLES"
bl_check_common = False
bl_reload_parent = False
def _construct(self, data):
instance = bpy.data.particles.new(data["name"])
instance.uuid = self.uuid
return instance
def _load_implementation(self, data, target):
dump_anything.load(target, data)
dump_anything.load(target.effector_weights, data["effector_weights"])
# Force field
force_field_1 = data.get("force_field_1", None)
if force_field_1:
dump_anything.load(target.force_field_1, force_field_1)
force_field_2 = data.get("force_field_2", None)
if force_field_2:
dump_anything.load(target.force_field_2, force_field_2)
# Texture slots
load_texture_slots(data["texture_slots"], target.texture_slots)
def _dump_implementation(self, data, instance=None):
assert instance
dumper = dump_anything.Dumper()
dumper.depth = 1
dumper.exclude_filter = IGNORED_ATTR
data = dumper.dump(instance)
# Particle effectors
data["effector_weights"] = dumper.dump(instance.effector_weights)
if instance.force_field_1:
data["force_field_1"] = dumper.dump(instance.force_field_1)
if instance.force_field_2:
data["force_field_2"] = dumper.dump(instance.force_field_2)
# Texture slots
data["texture_slots"] = dump_textures_slots(instance.texture_slots)
return data
def _resolve_deps_implementation(self):
return [t.texture for t in self.instance.texture_slots if t and t.texture]
+2 -4
View File
@@ -55,10 +55,6 @@ def np_load_collection(dikt: dict, collection: bpy.types.CollectionProperty, att
for attr in attributes:
attr_type = collection[0].bl_rna.properties.get(attr).type
if attr not in dikt:
logging.warning(f"No data for {attr}, skipping.")
continue
if attr_type in PRIMITIVE_TYPES:
np_load_collection_primitives(collection, attr, dikt[attr])
@@ -614,6 +610,8 @@ class Loader:
instance.write(bpy.data.fonts.get(dump))
elif isinstance(rna_property_type, T.Sound):
instance.write(bpy.data.sounds.get(dump))
# elif isinstance(rna_property_type, T.ParticleSettings):
# instance.write(bpy.data.particles.get(dump))
def _load_matrix(self, matrix, dump):
matrix.write(mathutils.Matrix(dump))
Submodule multi_user/libs/replication deleted from 9a02e16d70
+86 -233
View File
@@ -39,7 +39,6 @@ except ImportError:
import pickle
import bpy
import bmesh
import mathutils
from bpy.app.handlers import persistent
from bpy_extras.io_utils import ExportHelper, ImportHelper
@@ -57,12 +56,11 @@ background_execution_queue = Queue()
deleyables = []
stop_modal_executor = False
def session_callback(name):
""" Session callback wrapper
This allow to encapsulate session callbacks to background_execution_queue.
By doing this way callback are executed from the main thread.
By doing this way callback are executed from the main thread.
"""
def func_wrapper(func):
@session.register(name)
@@ -74,7 +72,7 @@ def session_callback(name):
@session_callback('on_connection')
def initialize_session():
"""Session connection init hander
"""Session connection init hander
"""
logging.info("Intializing the scene")
settings = utils.get_preferences()
@@ -88,7 +86,7 @@ def initialize_session():
logging.error(f"Can't construct node {node}")
elif node_ref.state == FETCHED:
node_ref.resolve()
# Step 2: Load nodes
logging.info("Loading nodes")
for node in session._graph.list_ordered():
@@ -115,7 +113,7 @@ def initialize_session():
@session_callback('on_exit')
def on_connection_end(reason="none"):
"""Session connection finished handler
"""Session connection finished handler
"""
global deleyables, stop_modal_executor
settings = utils.get_preferences()
@@ -139,8 +137,7 @@ def on_connection_end(reason="none"):
if isinstance(handler, logging.FileHandler):
logger.removeHandler(handler)
if reason != "user":
bpy.ops.session.notify(
'INVOKE_DEFAULT', message=f"Disconnected from session. Reason: {reason}. ")
bpy.ops.session.notify('INVOKE_DEFAULT', message=f"Disconnected from session. Reason: {reason}. ")
# OPERATORS
@@ -273,8 +270,7 @@ class SessionStartOperator(bpy.types.Operator):
# Background client updates service
deleyables.append(timers.ClientUpdate())
deleyables.append(timers.DynamicRightSelectTimer())
deleyables.append(timers.ApplyTimer(
timeout=settings.depsgraph_update_rate))
deleyables.append(timers.ApplyTimer(timeout=settings.depsgraph_update_rate))
# deleyables.append(timers.PushTimer(
# queue=stagging,
# timeout=settings.depsgraph_update_rate
@@ -292,6 +288,8 @@ class SessionStartOperator(bpy.types.Operator):
deleyables.append(session_update)
deleyables.append(session_user_sync)
self.report(
{'INFO'},
f"connecting to tcp://{settings.ip}:{settings.port}")
@@ -611,7 +609,7 @@ class SessionApply(bpy.types.Operator):
session.apply(parent, force=True)
except Exception as e:
self.report({'ERROR'}, repr(e))
return {"CANCELED"}
return {"CANCELED"}
return {"FINISHED"}
@@ -637,7 +635,6 @@ class SessionCommit(bpy.types.Operator):
self.report({'ERROR'}, repr(e))
return {"CANCELED"}
class ApplyArmatureOperator(bpy.types.Operator):
"""Operator which runs its self from a timer"""
bl_idname = "session.apply_armature_operator"
@@ -709,7 +706,6 @@ class SessionClearCache(bpy.types.Operator):
row = self.layout
row.label(text=f" Do you really want to remove local cache ? ")
class SessionPurgeOperator(bpy.types.Operator):
"Remove node with lost references"
bl_idname = "session.purge"
@@ -754,6 +750,7 @@ 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)
@@ -799,7 +796,6 @@ class SessionSaveBackupOperator(bpy.types.Operator, ExportHelper):
def poll(cls, context):
return session.state['STATE'] == STATE_ACTIVE
class SessionStopAutoSaveOperator(bpy.types.Operator):
bl_idname = "session.cancel_autosave"
bl_label = "Cancel auto-save"
@@ -831,222 +827,79 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
maxlen=255, # Max internal buffer length, longer would be clamped.
)
load_to_collection: bpy.props.BoolProperty(
name="Load to collection",
description="Load the snapshot into a collection",
default=False,
)
draw_users: bpy.props.BoolProperty(
name="Draw users",
description="Draw a mesh representing each user position and selected object",
default=False,
)
animate: bpy.props.BoolProperty(
name="Animate history",
description="Animate history versions",
default=False,
)
clear_datablocks: bpy.props.BoolProperty(
name="Removes existing data",
description="Remove all exisitng datablocks",
default=True,
)
files: bpy.props.CollectionProperty(
type=bpy.types.OperatorFileListElement,
options={'HIDDEN', 'SKIP_SAVE'},
)
def draw_users_as_curves(self, user_list):
users_collection = bpy.data.collections.new("users")
for username, user_data in user_list.items():
metadata = user_data.get('metadata', None)
if metadata:
logging.info(f"Loading user {username}")
# Curve creation
location = metadata.get('view_corners')
positions = [coord for coord in location]
curve = bpy.data.curves.new(username, 'CURVE')
obj = bpy.data.objects.new(username, curve)
for p in positions:
p.append(0.0)
sight = curve.splines.new('POLY')
sight.points.add(1)
sight.points[0].co = positions[4]
sight.points[1].co = positions[5]
cadre = curve.splines.new('POLY')
cadre.points.add(4)
cadre.points[0].co = positions[0]
cadre.points[1].co = positions[2]
cadre.points[2].co = positions[1]
cadre.points[3].co = positions[3]
cadre.points[4].co = positions[0]
frust = curve.splines.new('POLY')
frust.points.add(2)
frust.points[0].co = positions[0]
frust.points[1].co = positions[6]
frust.points[2].co = positions[1]
frust2 = curve.splines.new('POLY')
frust2.points.add(2)
frust2.points[0].co = positions[2]
frust2.points[1].co = positions[6]
frust2.points[2].co = positions[3]
curve.bevel_depth = 0.02
# Material creation
color = metadata.get('color')
material = bpy.data.materials.new(username)
material.use_nodes = True
material.node_tree.nodes[0].inputs['Emission'].default_value = color
curve.materials.append(material)
users_collection.objects.link(obj)
return users_collection
def draw_users_meshes(self, user_list):
for username, user_data in user_list.items():
metadata = user_data.get('metadata', None)
if metadata:
logging.info(f"Loading user {username}")
location = metadata.get('view_corners')
color = metadata.get('color')
positions = [tuple(coord) for coord in location]
edges = ((1, 3), (2, 1), (3, 0),
(2, 0), (4, 5), (1, 6),
(2, 6), (3, 6), (0, 6))
mesh = bpy.data.meshes.new("toto")
obj = bpy.data.objects.new("toto", mesh)
bm = bmesh.new()
for p in positions:
bm.verts.new(p)
bm.verts.ensure_lookup_table()
for v1, v2 in edges:
bm.edges.new((bm.verts[v1], bm.verts[v2]))
bm.to_mesh(mesh)
bpy.context.scene.collection.objects.link(obj)
def execute(self, context):
from replication.graph import ReplicationGraph
# Initialisation
# init the factory with supported types
bpy_factory=ReplicatedDataFactory()
for type in bl_types.types_to_register():
type_module=getattr(bl_types, type)
name=[e.capitalize() for e in type.split('_')[1:]]
type_impl_name='Bl'+''.join(name)
type_module_class=getattr(type_module, type_impl_name)
# TODO: add filechecks
try:
f = gzip.open(self.filepath, "rb")
db = pickle.load(f)
except OSError as e:
f = open(self.filepath, "rb")
db = pickle.load(f)
if db:
logging.info(f"Reading {self.filepath}")
nodes = db.get("nodes")
logging.info(f"{len(nodes)} Nodes to load")
# init the factory with supported types
bpy_factory = ReplicatedDataFactory()
for type in bl_types.types_to_register():
type_module = getattr(bl_types, type)
name = [e.capitalize() for e in type.split('_')[1:]]
type_impl_name = 'Bl'+''.join(name)
type_module_class = getattr(type_module, type_impl_name)
bpy_factory.register_type(
type_module_class.bl_class,
type_module_class)
# Optionnaly clear the scene
if self.clear_datablocks:
bpy_factory.register_type(
type_module_class.bl_class,
type_module_class)
graph = ReplicationGraph()
for node, node_data in nodes:
node_type = node_data.get('str_type')
impl = bpy_factory.get_implementation_from_net(node_type)
if impl:
logging.info(f"Loading {node}")
instance = impl(owner=node_data['owner'],
uuid=node,
dependencies=node_data['dependencies'],
data=node_data['data'])
instance.store(graph)
instance.state = FETCHED
logging.info("Graph succefully loaded")
utils.clean_scene()
dir_path=Path(self.filepath).parent
# Step 1: Construct nodes
for node in graph.list_ordered():
graph[node].resolve()
for db in self.files:
filepath=os.path.join(dir_path, db.name)
try:
f=gzip.open(filepath, "rb")
db=pickle.load(f)
except OSError as e:
f=open(filepath, "rb")
db=pickle.load(f)
if db:
created=os.path.getctime(filepath)
logging.info(f"Reading {filepath}")
nodes=db.get("nodes")
users=db.get("users")
users_collection = self.draw_users_as_curves(users)
logging.info(f"{len(nodes)} Nodes to load")
graph=ReplicationGraph()
for node, node_data in nodes:
node_type=node_data.get('str_type')
impl=bpy_factory.get_implementation_from_net(node_type)
if impl:
logging.info(f"Loading {node}")
instance=impl(owner=node_data['owner'],
uuid=node,
dependencies=node_data['dependencies'],
data=node_data['data'])
instance.store(graph)
instance.state=FETCHED
logging.info("Graph succefully loaded")
# Find scene
scenes=[n for n in graph.values() if isinstance(
n, bl_types.bl_scene.BlScene)]
scene=scenes[0]
# collection_data = {
# 'instance_offset': [0.0, 0.0, 0.0],
# 'name': str(created),
# 'objects': scene.data['collection']['objects'],
# 'children': scene.data['collection']['children']}
# collection_node = bl_types.bl_collection.BlCollection()
# collection_node.dependencies = scene.dependencies
# collection_node.data = collection_data
# graph[collection_node.uuid] = collection_node
# del graph[scene.uuid]
scene.data['name']=str(created)
# Step 1: Construct nodes
for node in graph.list_ordered():
node_inst=graph[node]
try:
node_inst.instance=node_inst._construct(node_inst.data)
node_inst.instance.uuid=node_inst.uuid
except Exception as e:
continue
# Step 2: Load nodes
for node in graph.list_ordered():
graph[node].state=FETCHED
graph[node].apply()
scene.instance.collection.children.link(users_collection)
# bpy.context.scene.collection.children.link(collection_node.instance)
# Step 2: Load nodes
for node in graph.list_ordered():
graph[node].apply()
return {'FINISHED'}
@ classmethod
@classmethod
def poll(cls, context):
return True
def menu_func_import(self, context):
self.layout.operator(SessionLoadSaveOperator.bl_idname,
text='Multi-user session snapshot (.db)')
self.layout.operator(SessionLoadSaveOperator.bl_idname, text='Multi-user session snapshot (.db)')
classes=(
classes = (
SessionStartOperator,
SessionStopOperator,
SessionPropertyRemoveOperator,
@@ -1067,22 +920,22 @@ classes=(
)
def update_external_dependencies():
nodes_ids=session.list(filter=bl_types.bl_file.BlFile)
nodes_ids = session.list(filter=bl_types.bl_file.BlFile)
for node_id in nodes_ids:
node=session.get(node_id)
node = session.get(node_id)
if node and node.owner in [session.id, RP_COMMON] \
and node.has_changed():
session.commit(node_id)
session.push(node_id, check_data=False)
def sanitize_deps_graph(remove_nodes: bool=False):
def sanitize_deps_graph(remove_nodes: bool = False):
""" Cleanup the replication graph
"""
if session and session.state['STATE'] == STATE_ACTIVE:
start=utils.current_milli_time()
rm_cpt=0
start = utils.current_milli_time()
rm_cpt = 0
for node_key in session.list():
node=session.get(node_key)
node = session.get(node_key)
if node is None \
or (node.state == UP and not node.resolve(construct=False)):
if remove_nodes:
@@ -1095,7 +948,7 @@ def sanitize_deps_graph(remove_nodes: bool=False):
logging.info(f"Sanitize took { utils.current_milli_time()-start} ms")
@ persistent
@persistent
def resolve_deps_graph(dummy):
"""Resolve deps graph
@@ -1106,13 +959,13 @@ def resolve_deps_graph(dummy):
if session and session.state['STATE'] == STATE_ACTIVE:
sanitize_deps_graph(remove_nodes=True)
@ persistent
@persistent
def load_pre_handler(dummy):
if session and session.state['STATE'] in [STATE_ACTIVE, STATE_SYNCING]:
bpy.ops.session.stop()
@ persistent
@persistent
def update_client_frame(scene):
if session and session.state['STATE'] == STATE_ACTIVE:
session.update_user_metadata({
@@ -1120,13 +973,13 @@ def update_client_frame(scene):
})
@ persistent
@persistent
def depsgraph_evaluation(scene):
if session and session.state['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()
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()
@@ -1135,8 +988,8 @@ def depsgraph_evaluation(scene):
# Is the object tracked ?
if update.id.uuid:
# Retrieve local version
node=session.get(uuid=update.id.uuid)
node = session.get(uuid=update.id.uuid)
# Check our right on this update:
# - if its ours or ( under common and diff), launch the
# update process
@@ -1152,24 +1005,24 @@ def depsgraph_evaluation(scene):
if not node.is_valid():
session.remove(node.uuid)
except ContextError as e:
logging.debug(e)
logging.debug(e)
except Exception as e:
logging.error(e)
else:
continue
# A new scene is created
# A new scene is created
elif isinstance(update.id, bpy.types.Scene):
ref=session.get(reference=update.id)
ref = session.get(reference=update.id)
if ref:
ref.resolve()
else:
scn_uuid=session.add(update.id)
scn_uuid = session.add(update.id)
session.commit(scn_uuid)
session.push(scn_uuid, check_data=False)
def register():
from bpy.utils import register_class
for cls in classes:
for cls in classes:
register_class(cls)
+1 -1
View File
@@ -13,7 +13,7 @@ def main():
if len(sys.argv) > 2:
blender_rev = sys.argv[2]
else:
blender_rev = "2.91.0"
blender_rev = "2.92.0"
try:
exit_val = BAT.test_blender_addon(addon_path=addon, blender_revision=blender_rev)
+3 -4
View File
@@ -7,7 +7,7 @@ import bpy
import random
from multi_user.bl_types.bl_object import BlObject
# Removed 'BUILD' modifier because the seed doesn't seems to be
# Removed 'BUILD', 'SOFT_BODY' modifier because the seed doesn't seems to be
# correctly initialized (#TODO: report the bug)
MOFIFIERS_TYPES = [
'DATA_TRANSFER', 'MESH_CACHE', 'MESH_SEQUENCE_CACHE',
@@ -22,8 +22,7 @@ MOFIFIERS_TYPES = [
'MESH_DEFORM', 'SHRINKWRAP', 'SIMPLE_DEFORM', 'SMOOTH',
'CORRECTIVE_SMOOTH', 'LAPLACIANSMOOTH', 'SURFACE_DEFORM',
'WARP', 'WAVE', 'CLOTH', 'COLLISION', 'DYNAMIC_PAINT',
'EXPLODE', 'FLUID', 'OCEAN', 'PARTICLE_INSTANCE',
'SOFT_BODY', 'SURFACE']
'EXPLODE', 'FLUID', 'OCEAN', 'PARTICLE_INSTANCE', 'SURFACE']
GP_MODIFIERS_TYPE = [
'GP_ARRAY', 'GP_BUILD', 'GP_MIRROR', 'GP_MULTIPLY',
@@ -72,5 +71,5 @@ def test_object(clear_blend):
test = implementation._construct(expected)
implementation._load(expected, test)
result = implementation._dump(test)
print(DeepDiff(expected, result))
assert not DeepDiff(expected, result)