diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..23dc814 --- /dev/null +++ b/constants.py @@ -0,0 +1,4 @@ + +from pathlib import Path + +RESOURCES_DIR = Path(__file__).parent /'resources' \ No newline at end of file diff --git a/interpolate_strokes/__init__.py b/interpolate_strokes/__init__.py index 66d65bd..d58dc0f 100644 --- a/interpolate_strokes/__init__.py +++ b/interpolate_strokes/__init__.py @@ -1,6 +1,7 @@ from gp_interpolate.interpolate_strokes import (properties, operators, operators_triangle, + operators_velocity, debug, bind_points, ) @@ -9,6 +10,7 @@ modules = ( properties, operators, operators_triangle, + operators_velocity, debug, bind_points, ) diff --git a/interpolate_strokes/operators.py b/interpolate_strokes/operators.py index 72b56ff..3af6bae 100644 --- a/interpolate_strokes/operators.py +++ b/interpolate_strokes/operators.py @@ -88,20 +88,21 @@ class GP_OT_interpolate_stroke_base(bpy.types.Operator): return {'FINISHED'} return {'CANCELLED'} - def get_stroke_to_interpolate(self, context): - ## Get strokes to interpolate - tgt_strokes = [s for s in self.gp.data.layers.active.active_frame.strokes if s.select] + # def get_stroke_to_interpolate(self, context): + # ## Get strokes to interpolate + # #tgt_strokes = [s for s in self.gp.data.layers.active.active_frame.strokes if s.select] + # tgt_strokes = [s for l in self.layers for s in l.active_frame.strokes if s.select] - ## If nothing selected in sculpt/paint, Select all before triggering - if not tgt_strokes and context.mode in ('SCULPT_GPENCIL', 'PAINT_GPENCIL'): - for s in self.gp.data.layers.active.active_frame.strokes: - s.select = True - tgt_strokes = self.gp.data.layers.active.active_frame.strokes + # ## If nothing selected in sculpt/paint, Select all before triggering + # if not tgt_strokes and context.mode in ('SCULPT_GPENCIL', 'PAINT_GPENCIL'): + # for s in self.gp.data.layers.active.active_frame.strokes: + # s.select = True + # tgt_strokes = self.gp.data.layers.active.active_frame.strokes - if tgt_strokes: - return tgt_strokes + # if tgt_strokes: + # return tgt_strokes - return self.exit(context, status='ERROR', text='No stroke selected!') + # return self.exit(context, status='ERROR', text='No stroke selected!') ## Added to operators owns invoke with uper().invoke(context, event) @@ -128,19 +129,28 @@ class GP_OT_interpolate_stroke_base(bpy.types.Operator): if interp_col := bpy.data.collections.get('interpolation_tool'): bpy.data.collections.remove(interp_col) + if context.mode != 'EDIT_GPENCIL': + self.report({"ERROR"}, "Mode need to be Edit Grease Pencil") + return {"CANCELLED"} + ## Change active layer if strokes are selected only on this layer - layers = [l for l in self.gp.data.layers - if (not l.lock and l.active_frame) + self.layers = [l for l in self.gp.data.layers + if (not l.lock and l.active_frame and not l.hide) and next((s for s in l.active_frame.strokes if s.select), None)] - if not layers: - return self.exit(context, status='ERROR', text='No stroke selected!') + self.strokes = [s for l in self.layers for s in l.active_frame.strokes if s.select] + if not self.strokes: + self.report({"ERROR"}, "No strokes selected") + return {"CANCELLED"} + + #if not self.layers: + # return self.exit(context, status='ERROR', text='No stroke selected!') - elif len(layers) > 1: - return self.exit(context, status='ERROR', text='Strokes selected accross multiple layers!') + #elif len(layers) > 1: + # return self.exit(context, status='ERROR', text='Strokes selected accross multiple layers!') ## Set active layer - self.gp.data.layers.active = layers[0] + #self.gp.data.layers.active = layers[0] if self.interactive: self.frames_to_jump = following_keys(forward=True, animation=True) @@ -269,9 +279,10 @@ class GP_OT_interpolate_stroke(GP_OT_interpolate_stroke_base): origin = scn.camera.matrix_world.to_translation() - tgt_strokes = self.get_stroke_to_interpolate(context) - if isinstance(tgt_strokes, set): - return tgt_strokes + strokes = [s for l in self.layers for s in l.active_frame.strokes if s.select] + if not strokes: + self.report({"ERROR"}, "No strokes selected") + return {"CANCELLED"} col = self.settings.target_collection if not col: @@ -350,7 +361,7 @@ class GP_OT_interpolate_stroke(GP_OT_interpolate_stroke_base): dg = bpy.context.evaluated_depsgraph_get() self.strokes_data = [] - for stroke_index, stroke in enumerate(tgt_strokes): + for stroke_index, stroke in enumerate(strokes): stroke_data = [] for point_index, point in enumerate(stroke.points): point_co_world = self.gp.matrix_world @ point.co @@ -393,7 +404,7 @@ class GP_OT_interpolate_stroke(GP_OT_interpolate_stroke_base): origin = scn.camera.matrix_world.to_translation() plane_co, plane_no = get_gp_draw_plane(self.gp) bpy.ops.gpencil.select_all(action='DESELECT') - bpy.ops.gpencil.paste() + bpy.ops.gpencil.paste(type='LAYER') if self.settings.method == 'BONE': ## Set plane on the bone @@ -405,10 +416,13 @@ class GP_OT_interpolate_stroke(GP_OT_interpolate_stroke_base): dg = bpy.context.evaluated_depsgraph_get() ## Get pasted stroke - new_strokes = [s for s in self.gp.data.layers.active.active_frame.strokes if s.select] + #new_strokes = [s for s in self.gp.data.layers.active.active_frame.strokes if s.select] + new_strokes = [s for l in self.layers for s in l.active_frame.strokes if s.select] ## Keep reference to all accessible other strokes (in all accessible layer) other_strokes = [s for l in self.gp.data.layers if l.active_frame and not l.lock for s in l.active_frame.strokes if not s.select] + smooth_level = self.settings.smooth_level + occluded_points = [] for new_stroke, stroke_data in zip(list(new_strokes), list(self.strokes_data)): world_co_3d = [] @@ -416,9 +430,25 @@ class GP_OT_interpolate_stroke(GP_OT_interpolate_stroke_base): eval_ob = object_hit.evaluated_get(dg) tri_b = [eval_ob.matrix_world @ eval_ob.data.vertices[i].co for i in tri_indices] - new_loc = barycentric_transform(hit_location, *tri_a, *tri_b) + new_loc = barycentric_transform(hit_location, *tri_a, *tri_b) world_co_3d.append(new_loc) + # Smooth points + if smooth_level: + old_co_3d = [s[1] for s in stroke_data] + points_velocity = [b-a for a, b in zip(old_co_3d, world_co_3d)] + + # Average of points + for i in range(smooth_level + 1): + points_velocity = [ + (points_velocity[i] + points_velocity[i + 1]) / 2 if i == 0 else + (points_velocity[i] + points_velocity[i - 1]) / 2 if i == len(points_velocity) - 1 else + (points_velocity[i - 1] + points_velocity[i] + points_velocity[i + 1]) / 3 + for i in range(len(points_velocity)) + ] + + world_co_3d = [a+b for a, b in zip(old_co_3d, points_velocity)] + ## Reproject on plane new_world_co_3d = [intersect_line_plane(origin, p, plane_co, plane_no) for p in world_co_3d] new_local_co_3d = [co for coord in new_world_co_3d for co in self.gp.matrix_world.inverted() @ coord] diff --git a/interpolate_strokes/operators_single.py b/interpolate_strokes/operators_single.py index a9251a1..958bf43 100644 --- a/interpolate_strokes/operators_single.py +++ b/interpolate_strokes/operators_single.py @@ -78,7 +78,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): # print('----') - tgt_strokes = [s for s in gp.data.layers.active.active_frame.strokes if s.select] + tgt_strokes = [s for l in self.layers for s in l.active_frame.strokes if s.select] ## If nothing selected in sculpt/paint, Select all before triggering if not tgt_strokes and context.mode in ('SCULPT_GPENCIL', 'PAINT_GPENCIL'): @@ -247,7 +247,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): origin = scn.camera.matrix_world.to_translation() # origin = np.array(scn.camera.matrix_world.to_translation(), 'float64') plan_co, plane_no = get_gp_draw_plane(gp) - bpy.ops.gpencil.paste() + bpy.ops.gpencil.paste(type="LAYER") if settings.method == 'BONE': bone_plane = plane_on_bone(settings.target_rig.pose.bones.get(settings.target_bone), @@ -257,10 +257,10 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): dg = bpy.context.evaluated_depsgraph_get() matrix_inv = np.array(gp.matrix_world.inverted(), dtype='float64')#.inverted() - new_strokes = gp.data.layers.active.active_frame.strokes[-len(strokes_data):] + new_strokes = [(l, s) for l in self.layers for s in l.active_frame.strokes if s.select] # for new_stroke, stroke_data in zip(new_strokes, strokes_data): - for new_stroke, stroke_data in zip(reversed(new_strokes), reversed(strokes_data)): + for (layer, new_stroke), stroke_data in zip(reversed(new_strokes), reversed(strokes_data)): world_co_3d = [] for stroke, point_co, object_hit, hit_location, tri_a, tri_indices in stroke_data: eval_ob = object_hit.evaluated_get(dg) @@ -316,7 +316,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): if len(sublist) == 1: continue - ns = gp.data.layers.active.active_frame.strokes.new() + ns = layer.active_frame.strokes.new() for elem in ('hardness', 'material_index', 'line_width'): setattr(ns, elem, getattr(new_stroke, elem)) @@ -326,7 +326,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): setattr(ns.points[i], elem, getattr(new_stroke.points[point_index], elem)) ## Delete original stroke - gp.data.layers.active.active_frame.strokes.remove(new_stroke) + layer.active_frame.strokes.remove(new_stroke) wm.progress_end() # Pgs diff --git a/interpolate_strokes/operators_triangle.py b/interpolate_strokes/operators_triangle.py index a8fea88..cdc7b44 100644 --- a/interpolate_strokes/operators_triangle.py +++ b/interpolate_strokes/operators_triangle.py @@ -27,10 +27,6 @@ class GP_OT_interpolate_stroke_tri(GP_OT_interpolate_stroke_base): origin = scn.camera.matrix_world.to_translation() - tgt_strokes = self.get_stroke_to_interpolate(context) - if isinstance(tgt_strokes, set): - return tgt_strokes - ## Prepare context manager attrs = [ # (context.view_layer.objects, 'active', self.gp), @@ -55,7 +51,7 @@ class GP_OT_interpolate_stroke_tri(GP_OT_interpolate_stroke_base): self.strokes_data = [] - for stroke in tgt_strokes: + for stroke in self.strokes: stroke_data = [] for point in stroke.points: point_co_world = self.gp.matrix_world @ point.co @@ -91,12 +87,13 @@ class GP_OT_interpolate_stroke_tri(GP_OT_interpolate_stroke_base): scn = context.scene origin = scn.camera.matrix_world.to_translation() plane_co, plane_no = get_gp_draw_plane(self.gp) - bpy.ops.gpencil.paste() + bpy.ops.gpencil.paste(type='LAYER') dg = bpy.context.evaluated_depsgraph_get() ## List of newly pasted strokes (using range) - new_strokes = self.gp.data.layers.active.active_frame.strokes[-len(self.strokes_data):] + new_strokes = [s for l in self.layers for s in l.active_frame.strokes if s.select] + #new_strokes = self.gp.data.layers.active.active_frame.strokes[-len(self.strokes_data):] ## Get user triangle position at current frame tri_b = [] diff --git a/interpolate_strokes/operators_velocity.py b/interpolate_strokes/operators_velocity.py new file mode 100644 index 0000000..e7c4001 --- /dev/null +++ b/interpolate_strokes/operators_velocity.py @@ -0,0 +1,275 @@ +import bpy +from time import time + +import bpy +#from bpy_extras.object_utils import world_to_camera_view +from mathutils import Vector +from mathutils.kdtree import KDTree +from math import tan + + +from mathutils.geometry import (barycentric_transform, + intersect_line_plane) + +from ..utils import (triangle_normal, + get_gp_draw_plane, load_datablock) + +from ..constants import RESOURCES_DIR +from .operators import GP_OT_interpolate_stroke_base + + +def world_to_camera_view(scene, obj, coord): + """ + Returns the camera space coords for a 3d point. + (also known as: normalized device coordinates - NDC). + + Where (0, 0) is the bottom left and (1, 1) + is the top right of the camera frame. + values outside 0-1 are also supported. + A negative 'z' value means the point is behind the camera. + + Takes shift-x/y, lens angle and sensor size into account + as well as perspective/ortho projections. + + :arg scene: Scene to use for frame size. + :type scene: :class:`bpy.types.Scene` + :arg obj: Camera object. + :type obj: :class:`bpy.types.Object` + :arg coord: World space location. + :type coord: :class:`mathutils.Vector` + :return: a vector where X and Y map to the view plane and + Z is the depth on the view axis. + :rtype: :class:`mathutils.Vector` + """ + from mathutils import Vector + + co_local = obj.matrix_world.normalized().inverted() @ coord + z = -co_local.z + + camera = obj.data + frame = [v for v in camera.view_frame(scene=scene)[:3]] + if camera.type != 'ORTHO': + if z == 0.0: + return Vector((0.5, 0.5, 0.0)) + else: + frame = [-(v / (v.z / z)) for v in frame] + + min_x, max_x = frame[2].x, frame[1].x + min_y, max_y = frame[1].y, frame[0].y + + x = (co_local.x - min_x) / (max_x - min_x) + y = (co_local.y - min_y) / (max_y - min_y) + + return Vector((x, y, z)) + +def camera_view_to_world(scene, obj, coord): + """Reverse function of world_to_camera_view""" + + frame = [obj.matrix_world @ co for co in obj.data.view_frame(scene=scene)] + + x, y, z = coord + + right_interp = frame[1] + y * (frame[0] - frame[1]) + # Interpolate along x-axis (left side) + left_interp = frame[2] + y * (frame[3] - frame[2]) + + # Interpolate along y-axis + return Vector(left_interp + x * (right_interp - left_interp)) + + +class GP_OT_interpolate_stroke_velocity(GP_OT_interpolate_stroke_base): + bl_idname = "gp.interpolate_stroke_velocity" + bl_label = "Interpolate Stroke" + bl_description = 'Interpolate Stroke based on velocity' + bl_options = {'REGISTER', 'UNDO'} + + def invoke(self, context, event): + if state := super().invoke(context, event): + return state + + scn = bpy.context.scene + settings = context.scene.gp_interpo_settings + col = settings.target_collection + + ## Prepare context manager + attrs = [ + # (context.view_layer.objects, 'active', self.gp), + (context.tool_settings, 'use_keyframe_insert_auto', True), + # (bpy.context.scene.render, 'simplify_subdivision', 0), + ] + self.apply_and_store(attrs) + + velocity_mesh = bpy.data.meshes.new('interpolate_velocity') + velocity_ob = bpy.data.objects.new('interpolate_velocity', velocity_mesh) + + self.velocity_node_group = load_datablock(RESOURCES_DIR/'nodes.blend', 'Velocity Grid', type='node_groups', link=False) + + instance_col_mod = velocity_ob.modifiers.new('IngestCollection', 'NODES') + ingest_node_group = load_datablock(RESOURCES_DIR/'nodes.blend', 'Ingest Collection', type='node_groups', link=False) + instance_col_mod.node_group = ingest_node_group + instance_col_mod["Socket_2"] = col + + scn.collection.objects.link(velocity_ob) + + # Apply instance collection modifier + dg = bpy.context.evaluated_depsgraph_get() + eval_ob = velocity_ob.evaluated_get(dg) + eval_data = eval_ob.data.copy() + + velocity_ob.modifiers.remove(instance_col_mod) + velocity_ob.data = eval_data + + bpy.data.node_groups.remove(ingest_node_group) + + self.velocity_ob = velocity_ob + + if self.debug: + self.scan_time = time()-self.start + print(f'Scan time {self.scan_time:.4f}s') + + # Baking Camera + self.camera = scn.camera.copy() + self.camera.data = self.camera.data.copy() + self.camera.animation_data_clear() + self.camera.data.animation_data_clear() + + cam_mat = self.camera.matrix_world.copy() + self.camera.animation_data_clear() + self.camera.parent = None + self.camera.matrix_world = cam_mat + + # Store curent gp matrix + self.gp_matrix = self.gp.matrix_world.copy() + + # Ensure whole stroke are selected before copy + + bpy.ops.gpencil.select_linked() + # Copy stroke selection + bpy.ops.gpencil.copy() + + # Jump frame and paste + # if self.report_progress: + # context.window_manager.progress_begin(self.frames_to_jump[0], self.frames_to_jump[-1]) # Pgs + # context.area.header_text_set('Starting interpolation | Esc: Cancel') + + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + + def interpolate_frame(self, context): + scn = context.scene + cam = scn.camera + #dg = bpy.context.evaluated_depsgraph_get() + col = self.settings.target_collection + smooth_level = self.settings.smooth_level + + origin = scn.camera.matrix_world.to_translation() + plane_co, plane_no = get_gp_draw_plane(self.gp) + + #print("interpolate_frame") + + print(self.gp_matrix) + print(self.gp.matrix_world) + + velocity_ob = self.velocity_ob + + velocity_ob.hide_set(True) + + + grid_velocity_mod = velocity_ob.modifiers.new('VelocityGrid', 'NODES') + grid_velocity_mod.node_group = self.velocity_node_group + grid_velocity_mod["Socket_2"] = col + grid_velocity_mod["Socket_4"] = self.camera + grid_velocity_mod["Socket_5"] = self.camera.data.angle + grid_velocity_mod["Socket_6"] = self.camera.data.shift_x + grid_velocity_mod["Socket_7"] = self.camera.data.shift_y + + #raise Exception() + + # Apply velocity grid modifier + dg = bpy.context.evaluated_depsgraph_get() + eval_ob = velocity_ob.evaluated_get(dg) + #eval_data = eval_ob.data.copy() + grid_ob = bpy.data.objects.new('Velocity Grid Object', eval_ob.data.copy()) + + #copy_ob = velocity_ob.copy() + #scn.collection.objects.link(copy_ob) + + velocity_ob.modifiers.remove(grid_velocity_mod) + #velocity_ob.data = eval_data + + + #Create kd tree for finding nearest points + kd = KDTree(len(grid_ob.data.vertices)) + + points = [0, 0, 0] * len(grid_ob.data.vertices) + grid_ob.data.vertices.foreach_get('co', points) + + for i in range(0, len(points), 3): + kd.insert(points[i:i+3], int(i/3)) + + kd.balance() + + #nb_strokes = len(self.gp.data.layers.active.active_frame.strokes) + bpy.ops.gpencil.paste(type='LAYER') + + ## List of newly pasted strokes (using range) + new_strokes = [s for l in self.layers for s in l.active_frame.strokes if s.select] + #new_strokes = self.gp.data.layers.active.active_frame.strokes[-nb_strokes:] + + velocity_attr = grid_ob.data.attributes["velocity"].data + + for stroke in new_strokes: + points = [0, 0, 0] * len(stroke.points) + stroke.points.foreach_get('co', points) + + points_2d = [world_to_camera_view(scn, self.camera, self.gp_matrix @ Vector(points[i:i+3])) for i in range(0, len(points), 3)] + points_2d = [Vector((p.x, p.y, 0)) for p in points_2d] # Remove Z component + + points_velocity = [velocity_attr[kd.find(p)[1]].vector for p in points_2d] + + if smooth_level: + # Average of points + for i in range(smooth_level + 1): + points_velocity = [ + (points_velocity[i] + points_velocity[i + 1]) / 2 if i == 0 else + (points_velocity[i] + points_velocity[i - 1]) / 2 if i == len(points_velocity) - 1 else + (points_velocity[i - 1] + points_velocity[i] + points_velocity[i + 1]) / 3 + for i in range(len(points_velocity)) + ] + + new_points_3d = [camera_view_to_world(scn, cam, p+vel) for p, vel in zip(points_2d, points_velocity)] + + ## Reproject on plane + new_points_3d = [intersect_line_plane(origin, p, plane_co, plane_no) for p in new_points_3d] + + stroke.points.foreach_set('co', [v for p in new_points_3d for v in self.gp.matrix_world.inverted() @p]) + + #stroke.points.foreach_set('co', [v for p in points_2d for v in self.gp.matrix_world.inverted() @p]) + stroke.points.update() + + #velocity_ob.modifiers.remove(grid_velocity_mod) + + bpy.data.meshes.remove(grid_ob.data) + + def exit(self, context, status='INFO', text=None, cancelled=False): + out = super().exit(context, status='INFO', text=None, cancelled=False) + + bpy.data.node_groups.remove(self.velocity_node_group) + bpy.data.meshes.remove(self.velocity_ob.data) + bpy.data.cameras.remove(self.camera.data) + + return out + + +classes = ( + GP_OT_interpolate_stroke_velocity, +) + +def register(): + for c in classes: + bpy.utils.register_class(c) + + +def unregister(): + for c in reversed(classes): + bpy.utils.unregister_class(c) diff --git a/interpolate_strokes/properties.py b/interpolate_strokes/properties.py index 77171cd..0ab7538 100644 --- a/interpolate_strokes/properties.py +++ b/interpolate_strokes/properties.py @@ -21,6 +21,7 @@ class GP_PG_interpolate_settings(PropertyGroup): ('OBJECT', 'Object Geometry', 'Same as Geometry mode, but target only a specific object, even if occluded (ignore all the others)', 1), ('BONE', 'Bone', 'Pick an armature bone and follow it', 2), ('TRI', 'Triangle', 'Interpolate based on triangle traced manually over geometry', 3), + ('VELOCITY', 'Velocity', 'Interpolate based on velocity, works well for point outside geometry', 4) ), default='GEOMETRY', description='Select method for interpolating strokes' @@ -83,6 +84,11 @@ class GP_PG_interpolate_settings(PropertyGroup): default=True, description='Apply rotation of the bone') # Bone + #selection: EnumProperty(default='SELECTED', items=[("SELECTED", "Selected", ""), ("ALL", "All", "")], + # description="Stroke to interpolate") + + smooth_level: IntProperty(default=2, min=0, max=10, name='Smooth Level') + classes = ( GP_PG_interpolate_settings, ) diff --git a/resources/nodes.blend b/resources/nodes.blend new file mode 100644 index 0000000..bb4b30d Binary files /dev/null and b/resources/nodes.blend differ diff --git a/ui.py b/ui.py index beb2ad6..72e2cc3 100755 --- a/ui.py +++ b/ui.py @@ -36,7 +36,13 @@ class GP_PT_interpolate(bpy.types.Panel): row.scale_y = 1.2 direction_button_row = row.row(align=True) direction_button_row.scale_x = 3 - ops_id = "gp.interpolate_stroke_tri" if settings.method == 'TRI' else "gp.interpolate_stroke" + ops_id = "gp.interpolate_stroke" + if settings.method == 'TRI': + ops_id = "gp.interpolate_stroke_tri" + elif settings.method == 'VELOCITY': + ops_id = "gp.interpolate_stroke_velocity" + + direction_button_row.operator(ops_id, text=prev_text, icon=prev_icon).next = False direction_button_row.operator(ops_id, text=next_text, icon=next_icon).next = True @@ -59,10 +65,17 @@ class GP_PT_interpolate(bpy.types.Panel): elif settings.method == 'GEOMETRY': col.prop(settings, 'search_range') col.prop(settings, 'remove_occluded') + col.prop(settings, 'smooth_level', text='Smooth') elif settings.method == 'OBJECT': col.prop(settings, 'search_range') col.prop(settings, 'target_object', text='Object') + col.prop(settings, 'smooth_level', text='Smooth') + + elif settings.method == 'VELOCITY': + col.prop(settings, 'target_collection', text='Collection') + col.prop(settings, 'target_object', text='Object') + col.prop(settings, 'smooth_level', text='Smooth') col.separator() col = layout.column(align=True) diff --git a/utils.py b/utils.py index aa88723..8094f0f 100644 --- a/utils.py +++ b/utils.py @@ -2,6 +2,9 @@ import bpy import math import numpy as np +import fnmatch +import os +from pathlib import Path from math import tan from mathutils import Vector, Matrix @@ -35,6 +38,63 @@ class attr_set(): # --- Vector +def load_datablock(filepath, *names, type='objects', link=True, expr=None, assets_only=False, + relative_to=None): + """link or append elements from another blender scene + + Args: + filepath (str): filepath of the scene to import objects from + names (list[str]): names of datablocks to import. + type (str, optional): type of data to import. + Defaults to 'objects'. + link (bool, optional): true if we want to import as link, else append. + Defaults to True. + expr (str, optional): pattern of names to import. + Defaults to None. + assets_only (bool, optional): If true, import only data-blocks marked as assets. + Defaults to False. + relative_to (str|Path|bool, optionnal): If str or Path and link make path relative to it + if False make path absolute, if None use preferences + Defaults to None. + + Returns: + list|bpy.types.Object: datablocks imported + """ + + # convert names from tuple to list to get the correct datablock type (blender tricks) + names = list(names) + + if isinstance(expr, str): + pattern = expr + expr = lambda x: fnmatch(x, pattern) + + with bpy.data.libraries.load(str(filepath), link=link, assets_only=assets_only) as (data_from, data_to): + datablocks = getattr(data_from, type) + if expr: + names += [i for i in datablocks if expr(i)] + elif not names: + names = datablocks + + setattr(data_to, type, names) + + datablocks = getattr(data_to, type) + + if link and datablocks: + lib = datablocks[0].library + lib_path = os.path.abspath(bpy.path.abspath(lib.filepath)) + + if relative_to is False: + lib.filepath = lib_path + elif isinstance(relative_to, (str, Path)): + lib.filepath = bpy.path.relpath(lib_path, start=str(relative_to)) + + if len(names) > 1: + return datablocks + + if datablocks: + return datablocks[0] + + def triangle_normal(p1, p2, p3): """ Calculate the normal of a triangle given its three vertices.