Compare commits
25 Commits
236-crash-
...
242-replay
Author | SHA1 | Date | |
---|---|---|---|
48eded1a9b | |||
3fbe769928 | |||
fe998214be | |||
3a4f691b8f | |||
0e20d35e7d | |||
8d15e69b50 | |||
8000ce9931 | |||
b96f600f15 | |||
de32bd89e3 | |||
50e86aea15 | |||
c05a12343c | |||
a09193fba2 | |||
60e21f2b8e | |||
421f00879f | |||
964e6a8c63 | |||
80c81dc934 | |||
563fdb693d | |||
a64eea3cea | |||
03ad7c0066 | |||
d685573834 | |||
0681b53141 | |||
6f02b38b0e | |||
92c773dae9 | |||
f48ade6390 | |||
63c4501b88 |
@ -43,7 +43,7 @@ __all__ = [
|
|||||||
"bl_particle",
|
"bl_particle",
|
||||||
] # Order here defines execution order
|
] # Order here defines execution order
|
||||||
|
|
||||||
if bpy.app.version[1] >= 91:
|
if bpy.app.version >= (2,91,0):
|
||||||
__all__.append('bl_volume')
|
__all__.append('bl_volume')
|
||||||
|
|
||||||
from . import *
|
from . import *
|
||||||
|
@ -53,12 +53,12 @@ STROKE = [
|
|||||||
"uv_translation",
|
"uv_translation",
|
||||||
"vertex_color_fill",
|
"vertex_color_fill",
|
||||||
]
|
]
|
||||||
if bpy.app.version[1] >= 91:
|
if bpy.app.version >= (2,91,0):
|
||||||
STROKE.append('use_cyclic')
|
STROKE.append('use_cyclic')
|
||||||
else:
|
else:
|
||||||
STROKE.append('draw_cyclic')
|
STROKE.append('draw_cyclic')
|
||||||
|
|
||||||
if bpy.app.version[1] >= 83:
|
if bpy.app.version >= (2,83,0):
|
||||||
STROKE_POINT.append('vertex_color')
|
STROKE_POINT.append('vertex_color')
|
||||||
|
|
||||||
def dump_stroke(stroke):
|
def dump_stroke(stroke):
|
||||||
|
@ -37,7 +37,7 @@ class BlLightprobe(ReplicatedDatablock):
|
|||||||
def construct(data: dict) -> object:
|
def construct(data: dict) -> object:
|
||||||
type = 'CUBE' if data['type'] == 'CUBEMAP' else data['type']
|
type = 'CUBE' if data['type'] == 'CUBEMAP' else data['type']
|
||||||
# See https://developer.blender.org/D6396
|
# See https://developer.blender.org/D6396
|
||||||
if bpy.app.version[1] >= 83:
|
if bpy.app.version >= (2,83,0):
|
||||||
return bpy.data.lightprobes.new(data["name"], type)
|
return bpy.data.lightprobes.new(data["name"], type)
|
||||||
else:
|
else:
|
||||||
logging.warning("Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
|
logging.warning("Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
|
||||||
@ -49,7 +49,7 @@ class BlLightprobe(ReplicatedDatablock):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def dump(datablock: object) -> dict:
|
def dump(datablock: object) -> dict:
|
||||||
if bpy.app.version[1] < 83:
|
if bpy.app.version < (2,83,0):
|
||||||
logging.warning("Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
|
logging.warning("Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
|
||||||
|
|
||||||
dumper = Dumper()
|
dumper = Dumper()
|
||||||
|
@ -48,7 +48,7 @@ SHAPEKEY_BLOCK_ATTR = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
if bpy.app.version[1] >= 93:
|
if bpy.app.version >= (2,93,0):
|
||||||
SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str, float)
|
SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str, float)
|
||||||
else:
|
else:
|
||||||
SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str)
|
SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str)
|
||||||
@ -56,14 +56,24 @@ else:
|
|||||||
blender 2.92.")
|
blender 2.92.")
|
||||||
|
|
||||||
|
|
||||||
def get_node_group_inputs(node_group):
|
def get_node_group_properties_identifiers(node_group):
|
||||||
inputs = []
|
props_ids = []
|
||||||
|
# Inputs
|
||||||
for inpt in node_group.inputs:
|
for inpt in node_group.inputs:
|
||||||
if inpt.type in IGNORED_SOCKETS:
|
if inpt.type in IGNORED_SOCKETS:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
inputs.append(inpt)
|
props_ids.append((inpt.identifier, inpt.type))
|
||||||
return inputs
|
|
||||||
|
if inpt.type in ['INT', 'VALUE', 'BOOLEAN', 'RGBA', 'VECTOR']:
|
||||||
|
props_ids.append((f"{inpt.identifier}_attribute_name",'STR'))
|
||||||
|
props_ids.append((f"{inpt.identifier}_use_attribute", 'BOOL'))
|
||||||
|
|
||||||
|
for outpt in node_group.outputs:
|
||||||
|
if outpt.type not in IGNORED_SOCKETS and outpt.type in ['INT', 'VALUE', 'BOOLEAN', 'RGBA', 'VECTOR']:
|
||||||
|
props_ids.append((f"{outpt.identifier}_attribute_name", 'STR'))
|
||||||
|
|
||||||
|
return props_ids
|
||||||
# return [inpt.identifer for inpt in node_group.inputs if inpt.type not in IGNORED_SOCKETS]
|
# return [inpt.identifer for inpt in node_group.inputs if inpt.type not in IGNORED_SOCKETS]
|
||||||
|
|
||||||
|
|
||||||
@ -122,29 +132,35 @@ def load_physics(dumped_settings: dict, target: bpy.types.Object):
|
|||||||
bpy.ops.rigidbody.constraint_remove({"object": target})
|
bpy.ops.rigidbody.constraint_remove({"object": target})
|
||||||
|
|
||||||
|
|
||||||
def dump_modifier_geometry_node_inputs(modifier: bpy.types.Modifier) -> list:
|
def dump_modifier_geometry_node_props(modifier: bpy.types.Modifier) -> list:
|
||||||
""" Dump geometry node modifier input properties
|
""" Dump geometry node modifier input properties
|
||||||
|
|
||||||
:arg modifier: geometry node modifier to dump
|
:arg modifier: geometry node modifier to dump
|
||||||
:type modifier: bpy.type.Modifier
|
:type modifier: bpy.type.Modifier
|
||||||
"""
|
"""
|
||||||
dumped_inputs = []
|
dumped_props = []
|
||||||
for inpt in get_node_group_inputs(modifier.node_group):
|
|
||||||
input_value = modifier[inpt.identifier]
|
for prop_value, prop_type in get_node_group_properties_identifiers(modifier.node_group):
|
||||||
|
try:
|
||||||
|
prop_value = modifier[prop_value]
|
||||||
|
except KeyError as e:
|
||||||
|
logging.error(f"fail to dump geomety node modifier property : {prop_value} ({e})")
|
||||||
|
else:
|
||||||
|
dump = None
|
||||||
|
if isinstance(prop_value, bpy.types.ID):
|
||||||
|
dump = prop_value.uuid
|
||||||
|
elif isinstance(prop_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS):
|
||||||
|
dump = prop_value
|
||||||
|
elif hasattr(prop_value, 'to_list'):
|
||||||
|
dump = prop_value.to_list()
|
||||||
|
|
||||||
dumped_input = None
|
dumped_props.append((dump, prop_type))
|
||||||
if isinstance(input_value, bpy.types.ID):
|
# logging.info(prop_value)
|
||||||
dumped_input = input_value.uuid
|
|
||||||
elif isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS):
|
|
||||||
dumped_input = input_value
|
|
||||||
elif hasattr(input_value, 'to_list'):
|
|
||||||
dumped_input = input_value.to_list()
|
|
||||||
dumped_inputs.append(dumped_input)
|
|
||||||
|
|
||||||
return dumped_inputs
|
return dumped_props
|
||||||
|
|
||||||
|
|
||||||
def load_modifier_geometry_node_inputs(dumped_modifier: dict, target_modifier: bpy.types.Modifier):
|
def load_modifier_geometry_node_props(dumped_modifier: dict, target_modifier: bpy.types.Modifier):
|
||||||
""" Load geometry node modifier inputs
|
""" Load geometry node modifier inputs
|
||||||
|
|
||||||
:arg dumped_modifier: source dumped modifier to load
|
:arg dumped_modifier: source dumped modifier to load
|
||||||
@ -153,17 +169,17 @@ def load_modifier_geometry_node_inputs(dumped_modifier: dict, target_modifier: b
|
|||||||
:type target_modifier: bpy.type.Modifier
|
:type target_modifier: bpy.type.Modifier
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for input_index, inpt in enumerate(get_node_group_inputs(target_modifier.node_group)):
|
for input_index, inpt in enumerate(get_node_group_properties_identifiers(target_modifier.node_group)):
|
||||||
dumped_value = dumped_modifier['inputs'][input_index]
|
dumped_value, dumped_type = dumped_modifier['props'][input_index]
|
||||||
input_value = target_modifier[inpt.identifier]
|
input_value = target_modifier[inpt[0]]
|
||||||
if isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS):
|
if dumped_type in ['INT', 'VALUE', 'STR']:
|
||||||
target_modifier[inpt.identifier] = dumped_value
|
logging.info(f"{inpt[0]}/{dumped_value}")
|
||||||
elif hasattr(input_value, 'to_list'):
|
target_modifier[inpt[0]] = dumped_value
|
||||||
|
elif dumped_type in ['RGBA', 'VECTOR']:
|
||||||
for index in range(len(input_value)):
|
for index in range(len(input_value)):
|
||||||
input_value[index] = dumped_value[index]
|
input_value[index] = dumped_value[index]
|
||||||
elif inpt.type in ['COLLECTION', 'OBJECT']:
|
elif dumped_type in ['COLLECTION', 'OBJECT', 'IMAGE', 'TEXTURE', 'MATERIAL']:
|
||||||
target_modifier[inpt.identifier] = get_datablock_from_uuid(
|
target_modifier[inpt[0]] = get_datablock_from_uuid(dumped_value, None)
|
||||||
dumped_value, None)
|
|
||||||
|
|
||||||
|
|
||||||
def load_pose(target_bone, data):
|
def load_pose(target_bone, data):
|
||||||
@ -198,12 +214,12 @@ def find_data_from_name(name=None):
|
|||||||
instance = bpy.data.speakers[name]
|
instance = bpy.data.speakers[name]
|
||||||
elif name in bpy.data.lightprobes.keys():
|
elif name in bpy.data.lightprobes.keys():
|
||||||
# Only supported since 2.83
|
# Only supported since 2.83
|
||||||
if bpy.app.version[1] >= 83:
|
if bpy.app.version >= (2,83,0):
|
||||||
instance = bpy.data.lightprobes[name]
|
instance = bpy.data.lightprobes[name]
|
||||||
else:
|
else:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
"Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
|
"Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
|
||||||
elif bpy.app.version[1] >= 91 and name in bpy.data.volumes.keys():
|
elif bpy.app.version >= (2,91,0) and name in bpy.data.volumes.keys():
|
||||||
# Only supported since 2.91
|
# Only supported since 2.91
|
||||||
instance = bpy.data.volumes[name]
|
instance = bpy.data.volumes[name]
|
||||||
return instance
|
return instance
|
||||||
@ -250,10 +266,11 @@ def find_geometry_nodes_dependencies(modifiers: bpy.types.bpy_prop_collection) -
|
|||||||
for mod in modifiers:
|
for mod in modifiers:
|
||||||
if mod.type == 'NODES' and mod.node_group:
|
if mod.type == 'NODES' and mod.node_group:
|
||||||
dependencies.append(mod.node_group)
|
dependencies.append(mod.node_group)
|
||||||
# for inpt in get_node_group_inputs(mod.node_group):
|
for inpt, inpt_type in get_node_group_properties_identifiers(mod.node_group):
|
||||||
# parameter = mod.get(inpt.identifier)
|
inpt_value = mod.get(inpt)
|
||||||
# if parameter and isinstance(parameter, bpy.types.ID):
|
# Avoid to handle 'COLLECTION', 'OBJECT' to avoid circular dependencies
|
||||||
# dependencies.append(parameter)
|
if inpt_type in ['IMAGE', 'TEXTURE', 'MATERIAL'] and inpt_value:
|
||||||
|
dependencies.append(inpt_value)
|
||||||
|
|
||||||
return dependencies
|
return dependencies
|
||||||
|
|
||||||
@ -387,10 +404,7 @@ def dump_modifiers(modifiers: bpy.types.bpy_prop_collection)->dict:
|
|||||||
dumped_modifier = dumper.dump(modifier)
|
dumped_modifier = dumper.dump(modifier)
|
||||||
# hack to dump geometry nodes inputs
|
# hack to dump geometry nodes inputs
|
||||||
if modifier.type == 'NODES':
|
if modifier.type == 'NODES':
|
||||||
dumped_inputs = dump_modifier_geometry_node_inputs(
|
dumped_modifier['props'] = dump_modifier_geometry_node_props(modifier)
|
||||||
modifier)
|
|
||||||
dumped_modifier['inputs'] = dumped_inputs
|
|
||||||
|
|
||||||
elif modifier.type == 'PARTICLE_SYSTEM':
|
elif modifier.type == 'PARTICLE_SYSTEM':
|
||||||
dumper.exclude_filter = [
|
dumper.exclude_filter = [
|
||||||
"is_edited",
|
"is_edited",
|
||||||
@ -455,7 +469,7 @@ def load_modifiers(dumped_modifiers: list, modifiers: bpy.types.bpy_prop_collect
|
|||||||
loader.load(loaded_modifier, dumped_modifier)
|
loader.load(loaded_modifier, dumped_modifier)
|
||||||
|
|
||||||
if loaded_modifier.type == 'NODES':
|
if loaded_modifier.type == 'NODES':
|
||||||
load_modifier_geometry_node_inputs(dumped_modifier, loaded_modifier)
|
load_modifier_geometry_node_props(dumped_modifier, loaded_modifier)
|
||||||
elif loaded_modifier.type == 'PARTICLE_SYSTEM':
|
elif loaded_modifier.type == 'PARTICLE_SYSTEM':
|
||||||
default = loaded_modifier.particle_system.settings
|
default = loaded_modifier.particle_system.settings
|
||||||
dumped_particles = dumped_modifier['particle_system']
|
dumped_particles = dumped_modifier['particle_system']
|
||||||
|
@ -440,7 +440,7 @@ class BlScene(ReplicatedDatablock):
|
|||||||
if seq.name not in sequences:
|
if seq.name not in sequences:
|
||||||
vse.sequences.remove(seq)
|
vse.sequences.remove(seq)
|
||||||
# Load existing sequences
|
# Load existing sequences
|
||||||
for seq_data in sequences.value():
|
for seq_data in sequences.values():
|
||||||
load_sequence(seq_data, vse)
|
load_sequence(seq_data, vse)
|
||||||
# If the sequence is no longer used, clear it
|
# If the sequence is no longer used, clear it
|
||||||
elif datablock.sequence_editor and not sequences:
|
elif datablock.sequence_editor and not sequences:
|
||||||
|
@ -134,7 +134,7 @@ def install_modules(dependencies: list, python_path: str, install_dir: str):
|
|||||||
module_can_be_imported(package_name)
|
module_can_be_imported(package_name)
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
if bpy.app.version[1] >= 91:
|
if bpy.app.version >= (2,91,0):
|
||||||
python_binary_path = sys.executable
|
python_binary_path = sys.executable
|
||||||
else:
|
else:
|
||||||
python_binary_path = bpy.app.binary_path_python
|
python_binary_path = bpy.app.binary_path_python
|
||||||
|
@ -130,14 +130,29 @@ def load_pre_handler(dummy):
|
|||||||
if session and session.state in [STATE_ACTIVE, STATE_SYNCING]:
|
if session and session.state in [STATE_ACTIVE, STATE_SYNCING]:
|
||||||
bpy.ops.session.stop()
|
bpy.ops.session.stop()
|
||||||
|
|
||||||
|
|
||||||
@persistent
|
@persistent
|
||||||
def update_client_frame(scene):
|
def update_client_frame(scene):
|
||||||
|
setting = bpy.context.window_manager.session
|
||||||
|
if setting.replay_mode == 'TIMELINE' and \
|
||||||
|
setting.replay_files and \
|
||||||
|
scene.active_replay_file != setting.replay_frame_current :
|
||||||
|
index = bpy.context.scene.active_replay_file
|
||||||
|
bpy.ops.session.load(filepath=bpy.context.window_manager.session.replay_files[index].name,
|
||||||
|
draw_users=True,
|
||||||
|
replay=True)
|
||||||
|
setting.replay_frame_current = index
|
||||||
|
|
||||||
if session and session.state == STATE_ACTIVE:
|
if session and session.state == STATE_ACTIVE:
|
||||||
porcelain.update_user_metadata(session.repository, {
|
porcelain.update_user_metadata(session.repository, {
|
||||||
'frame_current': scene.frame_current
|
'frame_current': scene.frame_current
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@persistent
|
||||||
|
def post_frame_update(scene):
|
||||||
|
if bpy.context.window_manager.session.replay_mode == 'TIMELINE' and \
|
||||||
|
not bpy.context.scene.animation_data:
|
||||||
|
bpy.context.scene.animation_data_create()
|
||||||
|
bpy.context.scene.animation_data.action = bpy.data.actions.get('replay_action')
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
bpy.app.handlers.undo_post.append(resolve_deps_graph)
|
bpy.app.handlers.undo_post.append(resolve_deps_graph)
|
||||||
|
Submodule multi_user/libs/replication updated: d69f259046...9aa015bd69
@ -35,8 +35,10 @@ from operator import itemgetter
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
|
from numpy import interp
|
||||||
|
|
||||||
from bpy.props import FloatProperty
|
from bpy.props import FloatProperty
|
||||||
|
import bmesh
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import _pickle as pickle
|
import _pickle as pickle
|
||||||
@ -58,13 +60,164 @@ from replication.repository import Repository
|
|||||||
|
|
||||||
from . import bl_types, environment, shared_data, timers, ui, utils
|
from . import bl_types, environment, shared_data, timers, ui, utils
|
||||||
from .handlers import on_scene_update, sanitize_deps_graph
|
from .handlers import on_scene_update, sanitize_deps_graph
|
||||||
from .presence import SessionStatusWidget, renderer, view3d_find, refresh_sidebar_view
|
from .presence import SessionStatusWidget, renderer, view3d_find, refresh_sidebar_view, bbox_from_obj
|
||||||
from .timers import registry
|
from .timers import registry
|
||||||
|
|
||||||
background_execution_queue = Queue()
|
background_execution_queue = Queue()
|
||||||
deleyables = []
|
deleyables = []
|
||||||
stop_modal_executor = False
|
stop_modal_executor = False
|
||||||
|
|
||||||
|
|
||||||
|
CLEARED_DATABLOCKS = ['actions', 'armatures', 'cache_files', 'cameras',
|
||||||
|
'collections', 'curves', 'fonts',
|
||||||
|
'grease_pencils', 'images', 'lattices', 'libraries',
|
||||||
|
'lightprobes', 'lights', 'linestyles', 'masks',
|
||||||
|
'materials', 'meshes', 'metaballs', 'movieclips',
|
||||||
|
'node_groups', 'objects', 'paint_curves', 'particles',
|
||||||
|
'scenes', 'shape_keys', 'sounds', 'speakers', 'texts',
|
||||||
|
'textures', 'volumes', 'worlds']
|
||||||
|
|
||||||
|
PERSISTENT_DATABLOCKS = ['LineStyle', 'Dots Stroke', 'replay_action']
|
||||||
|
|
||||||
|
def clean_scene(ignored_datablocks: list = None):
|
||||||
|
"""
|
||||||
|
Delete all datablock of the scene except PERSISTENT_DATABLOCKS and ignored
|
||||||
|
ones in ignored_datablocks.
|
||||||
|
"""
|
||||||
|
PERSISTENT_DATABLOCKS.extend(ignored_datablocks)
|
||||||
|
# Avoid to trigger a runtime error by keeping the last scene
|
||||||
|
PERSISTENT_DATABLOCKS.append(bpy.data.scenes[0].name)
|
||||||
|
|
||||||
|
for type_name in CLEARED_DATABLOCKS:
|
||||||
|
type_collection = getattr(bpy.data, type_name)
|
||||||
|
for datablock in type_collection:
|
||||||
|
if datablock.name in PERSISTENT_DATABLOCKS:
|
||||||
|
logging.debug(f"Skipping {datablock.name}")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
logging.debug(f"Removing {datablock.name}")
|
||||||
|
type_collection.remove(datablock)
|
||||||
|
|
||||||
|
# Clear sequencer
|
||||||
|
bpy.context.scene.sequence_editor_clear()
|
||||||
|
|
||||||
|
|
||||||
|
def draw_user(username, metadata, radius=0.01, intensity=10.0):
|
||||||
|
"""
|
||||||
|
Generate a mesh representation of a given user frustum and
|
||||||
|
sight of view.
|
||||||
|
"""
|
||||||
|
view_corners = metadata.get('view_corners')
|
||||||
|
color = metadata.get('color', (1,1,1,0))
|
||||||
|
objects = metadata.get('selected_objects', None)
|
||||||
|
scene = metadata.get('scene_current', bpy.context.scene.name)
|
||||||
|
|
||||||
|
user_collection = bpy.data.collections.new(username)
|
||||||
|
# User Color
|
||||||
|
user_mat = bpy.data.materials.new(username)
|
||||||
|
user_mat.use_nodes = True
|
||||||
|
nodes = user_mat.node_tree.nodes
|
||||||
|
nodes.remove(nodes['Principled BSDF'])
|
||||||
|
emission_node = nodes.new('ShaderNodeEmission')
|
||||||
|
emission_node.inputs['Color'].default_value = color
|
||||||
|
emission_node.inputs['Strength'].default_value = intensity
|
||||||
|
|
||||||
|
output_node = nodes['Material Output']
|
||||||
|
user_mat.node_tree.links.new(
|
||||||
|
emission_node.outputs['Emission'], output_node.inputs['Surface'])
|
||||||
|
|
||||||
|
# Generate camera mesh
|
||||||
|
camera_vertices = view_corners[:4]
|
||||||
|
camera_vertices.append(view_corners[6])
|
||||||
|
camera_mesh = bpy.data.meshes.new(f"{username}_camera")
|
||||||
|
camera_obj = bpy.data.objects.new(f"{username}_camera", camera_mesh)
|
||||||
|
frustum_bm = bmesh.new()
|
||||||
|
frustum_bm.from_mesh(camera_mesh)
|
||||||
|
|
||||||
|
for p in camera_vertices:
|
||||||
|
frustum_bm.verts.new(p)
|
||||||
|
frustum_bm.verts.ensure_lookup_table()
|
||||||
|
|
||||||
|
frustum_bm.edges.new((frustum_bm.verts[0], frustum_bm.verts[2]))
|
||||||
|
frustum_bm.edges.new((frustum_bm.verts[2], frustum_bm.verts[1]))
|
||||||
|
frustum_bm.edges.new((frustum_bm.verts[1], frustum_bm.verts[3]))
|
||||||
|
frustum_bm.edges.new((frustum_bm.verts[3], frustum_bm.verts[0]))
|
||||||
|
|
||||||
|
frustum_bm.edges.new((frustum_bm.verts[0], frustum_bm.verts[4]))
|
||||||
|
frustum_bm.edges.new((frustum_bm.verts[2], frustum_bm.verts[4]))
|
||||||
|
frustum_bm.edges.new((frustum_bm.verts[1], frustum_bm.verts[4]))
|
||||||
|
frustum_bm.edges.new((frustum_bm.verts[3], frustum_bm.verts[4]))
|
||||||
|
frustum_bm.edges.ensure_lookup_table()
|
||||||
|
|
||||||
|
frustum_bm.to_mesh(camera_mesh)
|
||||||
|
frustum_bm.free() # free and prevent further access
|
||||||
|
|
||||||
|
camera_obj.modifiers.new("wireframe", "SKIN")
|
||||||
|
camera_obj.data.skin_vertices[0].data[0].use_root = True
|
||||||
|
for v in camera_obj.data.skin_vertices[0].data:
|
||||||
|
v.radius = [radius, radius]
|
||||||
|
|
||||||
|
camera_mesh.materials.append(user_mat)
|
||||||
|
user_collection.objects.link(camera_obj)
|
||||||
|
|
||||||
|
# Generate sight mesh
|
||||||
|
sight_mesh = bpy.data.meshes.new(f"{username}_sight")
|
||||||
|
sight_obj = bpy.data.objects.new(f"{username}_sight", sight_mesh)
|
||||||
|
sight_verts = view_corners[4:6]
|
||||||
|
sight_bm = bmesh.new()
|
||||||
|
sight_bm.from_mesh(sight_mesh)
|
||||||
|
|
||||||
|
for p in sight_verts:
|
||||||
|
sight_bm.verts.new(p)
|
||||||
|
sight_bm.verts.ensure_lookup_table()
|
||||||
|
|
||||||
|
sight_bm.edges.new((sight_bm.verts[0], sight_bm.verts[1]))
|
||||||
|
sight_bm.edges.ensure_lookup_table()
|
||||||
|
sight_bm.to_mesh(sight_mesh)
|
||||||
|
sight_bm.free()
|
||||||
|
|
||||||
|
sight_obj.modifiers.new("wireframe", "SKIN")
|
||||||
|
sight_obj.data.skin_vertices[0].data[0].use_root = True
|
||||||
|
for v in sight_mesh.skin_vertices[0].data:
|
||||||
|
v.radius = [radius, radius]
|
||||||
|
|
||||||
|
sight_mesh.materials.append(user_mat)
|
||||||
|
user_collection.objects.link(sight_obj)
|
||||||
|
|
||||||
|
# Draw selected objects
|
||||||
|
if objects:
|
||||||
|
for o in list(objects):
|
||||||
|
instance = bl_types.bl_datablock.get_datablock_from_uuid(o, None)
|
||||||
|
if instance:
|
||||||
|
bbox_mesh = bpy.data.meshes.new(f"{instance.name}_bbox")
|
||||||
|
bbox_obj = bpy.data.objects.new(
|
||||||
|
f"{instance.name}_bbox", bbox_mesh)
|
||||||
|
bbox_verts, bbox_ind = bbox_from_obj(instance, index=0)
|
||||||
|
bbox_bm = bmesh.new()
|
||||||
|
bbox_bm.from_mesh(bbox_mesh)
|
||||||
|
|
||||||
|
for p in bbox_verts:
|
||||||
|
bbox_bm.verts.new(p)
|
||||||
|
bbox_bm.verts.ensure_lookup_table()
|
||||||
|
|
||||||
|
for e in bbox_ind:
|
||||||
|
bbox_bm.edges.new(
|
||||||
|
(bbox_bm.verts[e[0]], bbox_bm.verts[e[1]]))
|
||||||
|
|
||||||
|
bbox_bm.to_mesh(bbox_mesh)
|
||||||
|
bbox_bm.free()
|
||||||
|
bpy.data.collections[username].objects.link(bbox_obj)
|
||||||
|
|
||||||
|
bbox_obj.modifiers.new("wireframe", "SKIN")
|
||||||
|
bbox_obj.data.skin_vertices[0].data[0].use_root = True
|
||||||
|
for v in bbox_mesh.skin_vertices[0].data:
|
||||||
|
v.radius = [radius, radius]
|
||||||
|
|
||||||
|
bbox_mesh.materials.append(user_mat)
|
||||||
|
|
||||||
|
bpy.data.scenes[scene].collection.children.link(user_collection)
|
||||||
|
|
||||||
|
|
||||||
def session_callback(name):
|
def session_callback(name):
|
||||||
""" Session callback wrapper
|
""" Session callback wrapper
|
||||||
|
|
||||||
@ -238,7 +391,7 @@ class SessionConnectOperator(bpy.types.Operator):
|
|||||||
settings.generate_supported_types()
|
settings.generate_supported_types()
|
||||||
|
|
||||||
|
|
||||||
if bpy.app.version[1] >= 91:
|
if bpy.app.version >= (2,91,0):
|
||||||
python_binary_path = sys.executable
|
python_binary_path = sys.executable
|
||||||
else:
|
else:
|
||||||
python_binary_path = bpy.app.binary_path_python
|
python_binary_path = bpy.app.binary_path_python
|
||||||
@ -249,7 +402,7 @@ class SessionConnectOperator(bpy.types.Operator):
|
|||||||
|
|
||||||
# Join a session
|
# Join a session
|
||||||
if not active_server.use_admin_password:
|
if not active_server.use_admin_password:
|
||||||
utils.clean_scene()
|
clean_scene()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
porcelain.remote_add(
|
porcelain.remote_add(
|
||||||
@ -309,7 +462,7 @@ class SessionHostOperator(bpy.types.Operator):
|
|||||||
settings.generate_supported_types()
|
settings.generate_supported_types()
|
||||||
|
|
||||||
|
|
||||||
if bpy.app.version[1] >= 91:
|
if bpy.app.version >= (2,91,0):
|
||||||
python_binary_path = sys.executable
|
python_binary_path = sys.executable
|
||||||
else:
|
else:
|
||||||
python_binary_path = bpy.app.binary_path_python
|
python_binary_path = bpy.app.binary_path_python
|
||||||
@ -320,7 +473,7 @@ class SessionHostOperator(bpy.types.Operator):
|
|||||||
|
|
||||||
# Host a session
|
# Host a session
|
||||||
if settings.init_method == 'EMPTY':
|
if settings.init_method == 'EMPTY':
|
||||||
utils.clean_scene()
|
clean_scene()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Init repository
|
# Init repository
|
||||||
@ -384,7 +537,7 @@ class SessionInitOperator(bpy.types.Operator):
|
|||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
if self.init_method == 'EMPTY':
|
if self.init_method == 'EMPTY':
|
||||||
utils.clean_scene()
|
clean_scene()
|
||||||
|
|
||||||
for scene in bpy.data.scenes:
|
for scene in bpy.data.scenes:
|
||||||
porcelain.add(session.repository, scene)
|
porcelain.add(session.repository, scene)
|
||||||
@ -863,14 +1016,89 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
|
|||||||
maxlen=255, # Max internal buffer length, longer would be clamped.
|
maxlen=255, # Max internal buffer length, longer would be clamped.
|
||||||
)
|
)
|
||||||
|
|
||||||
|
draw_users: bpy.props.BoolProperty(
|
||||||
|
name="Load users",
|
||||||
|
description="Draw users in the scene",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
replay: bpy.props.BoolProperty(
|
||||||
|
name="Replay mode",
|
||||||
|
description="Enable replay functions",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_skin_radius: bpy.props.FloatProperty(
|
||||||
|
name="Wireframe radius",
|
||||||
|
description="Wireframe radius",
|
||||||
|
default=0.01,
|
||||||
|
)
|
||||||
|
user_color_intensity: bpy.props.FloatProperty(
|
||||||
|
name="Shading intensity",
|
||||||
|
description="Shading intensity",
|
||||||
|
default=1.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
files: bpy.props.CollectionProperty(
|
||||||
|
name='File paths',
|
||||||
|
type=bpy.types.OperatorFileListElement
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
pass
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
from replication.repository import Repository
|
from replication.repository import Repository
|
||||||
|
|
||||||
|
runtime_settings = context.window_manager.session
|
||||||
|
|
||||||
# init the factory with supported types
|
# init the factory with supported types
|
||||||
bpy_protocol = bl_types.get_data_translation_protocol()
|
bpy_protocol = bl_types.get_data_translation_protocol()
|
||||||
repo = Repository(bpy_protocol)
|
repo = Repository(bpy_protocol)
|
||||||
repo.loads(self.filepath)
|
|
||||||
utils.clean_scene()
|
try:
|
||||||
|
repo.loads(self.filepath)
|
||||||
|
except TypeError:
|
||||||
|
# Load legacy snapshots
|
||||||
|
db = pickle.load(gzip.open(self.filepath, "rb"))
|
||||||
|
|
||||||
|
nodes = db.get("nodes")
|
||||||
|
|
||||||
|
logging.info(f"Loading legacy {len(nodes)} node")
|
||||||
|
repo.object_store.clear()
|
||||||
|
for node, node_data in nodes:
|
||||||
|
instance = Node(
|
||||||
|
uuid=node,
|
||||||
|
data=node_data.get('data'),
|
||||||
|
owner=node_data.get('owner'),
|
||||||
|
dependencies=node_data.get('dependencies'),
|
||||||
|
state=FETCHED)
|
||||||
|
# Patch data for compatibility
|
||||||
|
type_id = node_data.get('str_type')[2:]
|
||||||
|
if type_id == "File":
|
||||||
|
type_id = "WindowsPath"
|
||||||
|
instance.data['type_id'] = type_id
|
||||||
|
repo.do_commit(instance)
|
||||||
|
instance.state = FETCHED
|
||||||
|
|
||||||
|
# Persitstent collection
|
||||||
|
ignored_datablocks = []
|
||||||
|
|
||||||
|
persistent_collection = bpy.data.collections.get("multiuser_timelapse")
|
||||||
|
if self.replay and \
|
||||||
|
runtime_settings.replay_persistent_collection and \
|
||||||
|
persistent_collection:
|
||||||
|
collection_repo = Repository(
|
||||||
|
rdp=bpy_protocol,
|
||||||
|
username="None")
|
||||||
|
porcelain.add(collection_repo, persistent_collection)
|
||||||
|
porcelain.commit(collection_repo, persistent_collection.uuid)
|
||||||
|
for node in collection_repo.graph.values():
|
||||||
|
ignored_datablocks.append(node.data.get('name'))
|
||||||
|
|
||||||
|
clean_scene(ignored_datablocks=ignored_datablocks)
|
||||||
|
|
||||||
nodes = [repo.graph.get(n) for n in repo.index_sorted]
|
nodes = [repo.graph.get(n) for n in repo.index_sorted]
|
||||||
|
|
||||||
@ -884,14 +1112,110 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
|
|||||||
# Step 2: Load nodes
|
# Step 2: Load nodes
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
porcelain.apply(repo, node.uuid)
|
porcelain.apply(repo, node.uuid)
|
||||||
|
|
||||||
|
if len(self.files) > 1:
|
||||||
|
runtime_settings.replay_files.clear()
|
||||||
|
context.scene.active_replay_file = len(self.files)-1
|
||||||
|
directory = Path(self.filepath).parent
|
||||||
|
file_list = [f['name'] for f in self.files]
|
||||||
|
file_list.sort()
|
||||||
|
for f in file_list:
|
||||||
|
snap = runtime_settings.replay_files.add()
|
||||||
|
snap.name = str(Path(directory, f))
|
||||||
|
print(f)
|
||||||
|
|
||||||
|
if runtime_settings.replay_mode == 'TIMELINE':
|
||||||
|
replay_action = bpy.data.actions.get('replay_action', bpy.data.actions.new('replay_action'))
|
||||||
|
|
||||||
|
bpy.context.scene.animation_data_create()
|
||||||
|
bpy.context.scene.animation_data.action = replay_action
|
||||||
|
if len(replay_action.fcurves) > 0 and replay_action.fcurves[0].data_path == 'active_replay_file':
|
||||||
|
replay_fcurve = replay_action.fcurves[0]
|
||||||
|
else:
|
||||||
|
replay_fcurve = replay_action.fcurves.new('active_replay_file')
|
||||||
|
|
||||||
|
for p in reversed(replay_fcurve.keyframe_points):
|
||||||
|
replay_fcurve.keyframe_points.remove(p, fast=True)
|
||||||
|
|
||||||
|
duration = runtime_settings.replay_duration
|
||||||
|
file_count = len(self.files)-1
|
||||||
|
for index in range(0, file_count):
|
||||||
|
frame = interp(index, [0, file_count], [bpy.context.scene.frame_start, duration])
|
||||||
|
replay_fcurve.keyframe_points.insert(frame, index)
|
||||||
|
|
||||||
|
|
||||||
|
if self.draw_users:
|
||||||
|
f = gzip.open(self.filepath, "rb")
|
||||||
|
db = pickle.load(f)
|
||||||
|
|
||||||
|
users = db.get("users")
|
||||||
|
|
||||||
|
for username, user_data in users.items():
|
||||||
|
metadata = user_data['metadata']
|
||||||
|
|
||||||
|
if metadata:
|
||||||
|
draw_user(username, metadata, radius=self.user_skin_radius, intensity=self.user_color_intensity)
|
||||||
|
|
||||||
|
# Relink the persistent collection
|
||||||
|
if self.replay and persistent_collection:
|
||||||
|
logging.info(f"Relinking {persistent_collection.name}")
|
||||||
|
bpy.context.scene.collection.children.link(persistent_collection)
|
||||||
|
|
||||||
|
# Reasign scene action
|
||||||
|
if self.replay and \
|
||||||
|
runtime_settings.replay_mode == 'TIMELINE' and \
|
||||||
|
not bpy.context.scene.animation_data :
|
||||||
|
bpy.context.scene.animation_data_create()
|
||||||
|
bpy.context.scene.animation_data.action = bpy.data.actions.get('replay_action')
|
||||||
|
bpy.context.scene.frame_end = runtime_settings.replay_duration
|
||||||
|
|
||||||
|
# Reasign the scene camera
|
||||||
|
if self.replay and \
|
||||||
|
runtime_settings.replay_persistent_collection and \
|
||||||
|
runtime_settings.replay_camera:
|
||||||
|
bpy.context.scene.camera = runtime_settings.replay_camera
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
class SESSION_PT_ImportUser(bpy.types.Panel):
|
||||||
|
bl_space_type = 'FILE_BROWSER'
|
||||||
|
bl_region_type = 'TOOL_PROPS'
|
||||||
|
bl_label = "Users"
|
||||||
|
bl_parent_id = "FILE_PT_operator"
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
sfile = context.space_data
|
||||||
|
operator = sfile.active_operator
|
||||||
|
|
||||||
|
return operator.bl_idname == "SESSION_OT_load"
|
||||||
|
|
||||||
|
def draw_header(self, context):
|
||||||
|
sfile = context.space_data
|
||||||
|
operator = sfile.active_operator
|
||||||
|
|
||||||
|
self.layout.prop(operator, "draw_users", text="")
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.use_property_split = True
|
||||||
|
layout.use_property_decorate = False # No animation.
|
||||||
|
|
||||||
|
sfile = context.space_data
|
||||||
|
operator = sfile.active_operator
|
||||||
|
|
||||||
|
layout.enabled = operator.draw_users
|
||||||
|
|
||||||
|
layout.prop(operator, "user_skin_radius")
|
||||||
|
layout.prop(operator, "user_color_intensity")
|
||||||
|
|
||||||
class SessionPresetServerAdd(bpy.types.Operator):
|
class SessionPresetServerAdd(bpy.types.Operator):
|
||||||
"""Add a server to the server list preset"""
|
"""Add a server to the server list preset"""
|
||||||
bl_idname = "session.preset_server_add"
|
bl_idname = "session.preset_server_add"
|
||||||
@ -1106,6 +1430,26 @@ def menu_func_import(self, context):
|
|||||||
def menu_func_export(self, context):
|
def menu_func_export(self, context):
|
||||||
self.layout.operator(SessionSaveBackupOperator.bl_idname, text='Multi-user session snapshot (.db)')
|
self.layout.operator(SessionSaveBackupOperator.bl_idname, text='Multi-user session snapshot (.db)')
|
||||||
|
|
||||||
|
class SessionRenderReplay(bpy.types.Operator):
|
||||||
|
bl_idname = "session.render_replay"
|
||||||
|
bl_label = "Render Replay"
|
||||||
|
bl_description = "Render Replay"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.window_manager.session.replay_files
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
base_path = str(context.scene.render.filepath)
|
||||||
|
for frame in range(0,context.scene.frame_end):
|
||||||
|
logging.info(f"Rendering frame {frame} to {base_path}_{frame}.png")
|
||||||
|
context.scene.frame_current = frame
|
||||||
|
filename = Path(bpy.context.window_manager.session.replay_files[context.scene.active_replay_file].name)
|
||||||
|
context.scene.render.filepath = f"{base_path}{frame}_{filename.stem}"
|
||||||
|
bpy.ops.render.render(write_still=True)
|
||||||
|
|
||||||
|
context.scene.render.filepath = base_path
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
SessionConnectOperator,
|
SessionConnectOperator,
|
||||||
@ -1123,6 +1467,7 @@ classes = (
|
|||||||
SessionNotifyOperator,
|
SessionNotifyOperator,
|
||||||
SessionSaveBackupOperator,
|
SessionSaveBackupOperator,
|
||||||
SessionLoadSaveOperator,
|
SessionLoadSaveOperator,
|
||||||
|
SESSION_PT_ImportUser,
|
||||||
SessionStopAutoSaveOperator,
|
SessionStopAutoSaveOperator,
|
||||||
SessionPurgeOperator,
|
SessionPurgeOperator,
|
||||||
SessionPresetServerAdd,
|
SessionPresetServerAdd,
|
||||||
@ -1131,6 +1476,7 @@ classes = (
|
|||||||
RefreshServerStatus,
|
RefreshServerStatus,
|
||||||
GetDoc,
|
GetDoc,
|
||||||
FirstLaunch,
|
FirstLaunch,
|
||||||
|
SessionRenderReplay,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ import bpy
|
|||||||
import string
|
import string
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
|
from numpy import interp
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@ -99,6 +100,77 @@ def update_directory(self, context):
|
|||||||
def set_log_level(self, value):
|
def set_log_level(self, value):
|
||||||
logging.getLogger().setLevel(value)
|
logging.getLogger().setLevel(value)
|
||||||
|
|
||||||
|
def set_active_replay(self, value):
|
||||||
|
files_count = len(bpy.context.window_manager.session.replay_files)
|
||||||
|
|
||||||
|
if files_count == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
max_index = files_count-1
|
||||||
|
|
||||||
|
if value > max_index:
|
||||||
|
value = max_index
|
||||||
|
|
||||||
|
if hasattr(self, 'active_replay_file'):
|
||||||
|
self["active_replay_file"] = value
|
||||||
|
else:
|
||||||
|
self.active_replay_file = value
|
||||||
|
|
||||||
|
if bpy.context.window_manager.session.replay_mode == 'MANUAL':
|
||||||
|
bpy.ops.session.load(
|
||||||
|
filepath=bpy.context.window_manager.session.replay_files[value].name,
|
||||||
|
draw_users=True,
|
||||||
|
replay=True)
|
||||||
|
|
||||||
|
def get_active_replay(self):
|
||||||
|
return self.get('active_replay_file', 0)
|
||||||
|
|
||||||
|
|
||||||
|
def set_replay_persistent_collection(self, value):
|
||||||
|
if hasattr(self, 'replay_persistent_collection'):
|
||||||
|
self["replay_persistent_collection"] = value
|
||||||
|
else:
|
||||||
|
self.replay_persistent_collection = value
|
||||||
|
|
||||||
|
collection = bpy.data.collections.get("multiuser_timelapse", None)
|
||||||
|
|
||||||
|
if collection is None and value:
|
||||||
|
collection = bpy.data.collections.new('multiuser_timelapse')
|
||||||
|
bpy.context.scene.collection.children.link(collection)
|
||||||
|
elif collection and not value:
|
||||||
|
for o in collection.objects:
|
||||||
|
bpy.data.objects.remove(o)
|
||||||
|
bpy.data.collections.remove(collection)
|
||||||
|
|
||||||
|
def get_replay_persistent_collection(self):
|
||||||
|
return self.get('replay_persistent_collection', False)
|
||||||
|
|
||||||
|
def set_replay_duration(self, value):
|
||||||
|
if hasattr(self, 'replay_duration'):
|
||||||
|
self["replay_duration"] = value
|
||||||
|
else:
|
||||||
|
self.replay_duration = value
|
||||||
|
|
||||||
|
# Update the animation fcurve
|
||||||
|
replay_action = bpy.data.actions.get('replay_action')
|
||||||
|
replay_fcurve = None
|
||||||
|
|
||||||
|
for fcurve in replay_action.fcurves:
|
||||||
|
if fcurve.data_path == 'active_replay_file':
|
||||||
|
replay_fcurve = fcurve
|
||||||
|
|
||||||
|
if replay_fcurve:
|
||||||
|
for p in reversed(replay_fcurve.keyframe_points):
|
||||||
|
replay_fcurve.keyframe_points.remove(p, fast=True)
|
||||||
|
|
||||||
|
bpy.context.scene.frame_end = value
|
||||||
|
files_count = len(bpy.context.window_manager.session.replay_files)-1
|
||||||
|
for index in range(0, files_count):
|
||||||
|
frame = interp(index,[0, files_count],[bpy.context.scene.frame_start, value])
|
||||||
|
replay_fcurve.keyframe_points.insert(frame, index)
|
||||||
|
|
||||||
|
def get_replay_duration(self):
|
||||||
|
return self.get('replay_duration', 10)
|
||||||
|
|
||||||
def get_log_level(self):
|
def get_log_level(self):
|
||||||
return logging.getLogger().level
|
return logging.getLogger().level
|
||||||
@ -687,6 +759,37 @@ class SessionProps(bpy.types.PropertyGroup):
|
|||||||
is_host: bpy.props.BoolProperty(
|
is_host: bpy.props.BoolProperty(
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
replay_files: bpy.props.CollectionProperty(
|
||||||
|
name='File paths',
|
||||||
|
type=bpy.types.OperatorFileListElement
|
||||||
|
)
|
||||||
|
replay_persistent_collection: bpy.props.BoolProperty(
|
||||||
|
name="replay_persistent_collection",
|
||||||
|
description='Enable a collection that persist accross frames loading',
|
||||||
|
get=get_replay_persistent_collection,
|
||||||
|
set=set_replay_persistent_collection,
|
||||||
|
)
|
||||||
|
replay_mode: bpy.props.EnumProperty(
|
||||||
|
name='replay method',
|
||||||
|
description='Replay in keyframe (timeline) or manually',
|
||||||
|
items={
|
||||||
|
('TIMELINE', 'TIMELINE', 'Replay from the timeline.'),
|
||||||
|
('MANUAL', 'MANUAL', 'Replay manually, from the replay frame widget.')},
|
||||||
|
default='TIMELINE')
|
||||||
|
replay_duration: bpy.props.IntProperty(
|
||||||
|
name='replay interval',
|
||||||
|
default=250,
|
||||||
|
min=10,
|
||||||
|
set=set_replay_duration,
|
||||||
|
get=get_replay_duration,
|
||||||
|
)
|
||||||
|
replay_frame_current: bpy.props.IntProperty(
|
||||||
|
name='replay_frame_current',
|
||||||
|
)
|
||||||
|
replay_camera: bpy.props.PointerProperty(
|
||||||
|
name='Replay camera',
|
||||||
|
type=bpy.types.Object
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
@ -712,6 +815,16 @@ def register():
|
|||||||
|
|
||||||
# at launch server presets
|
# at launch server presets
|
||||||
prefs.generate_default_presets()
|
prefs.generate_default_presets()
|
||||||
|
|
||||||
|
bpy.types.Scene.active_replay_file = bpy.props.IntProperty(
|
||||||
|
name="active_replay_file",
|
||||||
|
default=0,
|
||||||
|
min=0,
|
||||||
|
description='Active snapshot',
|
||||||
|
set=set_active_replay,
|
||||||
|
get=get_active_replay,
|
||||||
|
options={'ANIMATABLE'}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -720,3 +833,5 @@ def unregister():
|
|||||||
|
|
||||||
for cls in reversed(classes):
|
for cls in reversed(classes):
|
||||||
unregister_class(cls)
|
unregister_class(cls)
|
||||||
|
|
||||||
|
del bpy.types.Scene.active_replay_file
|
||||||
|
@ -564,10 +564,42 @@ class SESSION_PT_sync(bpy.types.Panel):
|
|||||||
row.prop(settings.sync_flags, "sync_active_camera", text="",icon_only=True, icon='VIEW_CAMERA')
|
row.prop(settings.sync_flags, "sync_active_camera", text="",icon_only=True, icon='VIEW_CAMERA')
|
||||||
|
|
||||||
|
|
||||||
|
class SESSION_PT_replay(bpy.types.Panel):
|
||||||
|
bl_idname = "MULTIUSER_REPLAY_PT_panel"
|
||||||
|
bl_label = "Replay"
|
||||||
|
bl_space_type = 'VIEW_3D'
|
||||||
|
bl_region_type = 'UI'
|
||||||
|
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.window_manager.session.replay_files
|
||||||
|
|
||||||
|
def draw_header(self, context):
|
||||||
|
self.layout.label(text="", icon='RECOVER_LAST')
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
settings = context.window_manager.session
|
||||||
|
row= layout.row()
|
||||||
|
row.prop(settings,'replay_mode', toggle=True, expand=True)
|
||||||
|
row= layout.row()
|
||||||
|
if settings.replay_mode == 'MANUAL':
|
||||||
|
row.prop(bpy.context.scene, 'active_replay_file', text="Snapshot index")
|
||||||
|
else:
|
||||||
|
row.prop(settings, 'replay_duration', text="Replay Duration")
|
||||||
|
row= layout.row()
|
||||||
|
row.prop(settings, 'replay_persistent_collection', text="persistent collection", toggle=True, icon='OUTLINER_COLLECTION')
|
||||||
|
|
||||||
|
if settings.replay_persistent_collection:
|
||||||
|
row= layout.row()
|
||||||
|
row.prop(settings, 'replay_camera', text="", icon='VIEW_CAMERA')
|
||||||
|
|
||||||
class SESSION_PT_repository(bpy.types.Panel):
|
class SESSION_PT_repository(bpy.types.Panel):
|
||||||
bl_idname = "MULTIUSER_PROPERTIES_PT_panel"
|
bl_idname = "MULTIUSER_PROPERTIES_PT_panel"
|
||||||
bl_label = "Repository"
|
bl_label = "Repository"
|
||||||
bl_space_type = 'VIEW_3D'
|
bl_space_type = 'VIEW_3D'
|
||||||
bl_region_type = 'UI'
|
bl_region_type = 'UI'
|
||||||
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
|
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
|
||||||
bl_options = {'DEFAULT_CLOSED'}
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
@ -709,6 +741,7 @@ classes = (
|
|||||||
SESSION_PT_sync,
|
SESSION_PT_sync,
|
||||||
SESSION_PT_repository,
|
SESSION_PT_repository,
|
||||||
VIEW3D_PT_overlay_session,
|
VIEW3D_PT_overlay_session,
|
||||||
|
SESSION_PT_replay,
|
||||||
)
|
)
|
||||||
|
|
||||||
register, unregister = bpy.utils.register_classes_factory(classes)
|
register, unregister = bpy.utils.register_classes_factory(classes)
|
||||||
|
@ -38,15 +38,6 @@ from replication.constants import (STATE_ACTIVE, STATE_AUTH,
|
|||||||
STATE_LOBBY,
|
STATE_LOBBY,
|
||||||
CONNECTING)
|
CONNECTING)
|
||||||
|
|
||||||
CLEARED_DATABLOCKS = ['actions', 'armatures', 'cache_files', 'cameras',
|
|
||||||
'collections', 'curves', 'filepath', 'fonts',
|
|
||||||
'grease_pencils', 'images', 'lattices', 'libraries',
|
|
||||||
'lightprobes', 'lights', 'linestyles', 'masks',
|
|
||||||
'materials', 'meshes', 'metaballs', 'movieclips',
|
|
||||||
'node_groups', 'objects', 'paint_curves', 'particles',
|
|
||||||
'scenes', 'shape_keys', 'sounds', 'speakers', 'texts',
|
|
||||||
'textures', 'volumes', 'worlds']
|
|
||||||
|
|
||||||
def find_from_attr(attr_name, attr_value, list):
|
def find_from_attr(attr_name, attr_value, list):
|
||||||
for item in list:
|
for item in list:
|
||||||
if getattr(item, attr_name, None) == attr_value:
|
if getattr(item, attr_name, None) == attr_value:
|
||||||
@ -108,26 +99,6 @@ def get_state_str(state):
|
|||||||
return state_str
|
return state_str
|
||||||
|
|
||||||
|
|
||||||
def clean_scene():
|
|
||||||
for type_name in CLEARED_DATABLOCKS:
|
|
||||||
sub_collection_to_avoid = [
|
|
||||||
bpy.data.linestyles.get('LineStyle'),
|
|
||||||
bpy.data.materials.get('Dots Stroke')
|
|
||||||
]
|
|
||||||
|
|
||||||
type_collection = getattr(bpy.data, type_name)
|
|
||||||
items_to_remove = [i for i in type_collection if i not in sub_collection_to_avoid]
|
|
||||||
for item in items_to_remove:
|
|
||||||
try:
|
|
||||||
type_collection.remove(item)
|
|
||||||
logging.info(item.name)
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Clear sequencer
|
|
||||||
bpy.context.scene.sequence_editor_clear()
|
|
||||||
|
|
||||||
|
|
||||||
def get_selected_objects(scene, active_view_layer):
|
def get_selected_objects(scene, active_view_layer):
|
||||||
return [obj.uuid for obj in scene.objects if obj.select_get(view_layer=active_view_layer)]
|
return [obj.uuid for obj in scene.objects if obj.select_get(view_layer=active_view_layer)]
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import bpy
|
|||||||
from multi_user.bl_types.bl_lightprobe import BlLightprobe
|
from multi_user.bl_types.bl_lightprobe import BlLightprobe
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(bpy.app.version[1] < 83, reason="requires blender 2.83 or higher")
|
@pytest.mark.skipif(bpy.app.version < (2,83,0), reason="requires blender 2.83 or higher")
|
||||||
@pytest.mark.parametrize('lightprobe_type', ['PLANAR','GRID','CUBEMAP'])
|
@pytest.mark.parametrize('lightprobe_type', ['PLANAR','GRID','CUBEMAP'])
|
||||||
def test_lightprobes(clear_blend, lightprobe_type):
|
def test_lightprobes(clear_blend, lightprobe_type):
|
||||||
bpy.ops.object.lightprobe_add(type=lightprobe_type)
|
bpy.ops.object.lightprobe_add(type=lightprobe_type)
|
||||||
|
Reference in New Issue
Block a user