- Added interaction helpers for cutout region validation and shape menu interaction. - Created main operator for converting image regions into cutout shapes, including AI artifact generation. - Introduced object creation logic for cutout shapes, handling depth and normal textures. - Defined cutout shape identifiers and node groups for various shape types. - Implemented depth texture writing and prediction artifact generation for MoGe-2. - Developed server jobs for generating cutout artifacts, depth planes, and refining image regions. - Added region refinement logic to enhance gesture masks using BEN2.
533 lines
20 KiB
Python
533 lines
20 KiB
Python
from pathlib import Path
|
|
|
|
import bpy
|
|
|
|
from .image_data import (
|
|
configure_image_user,
|
|
image_base_name,
|
|
image_pixels,
|
|
is_animated_image,
|
|
)
|
|
|
|
|
|
NODE_ASSET_NAME = "O_AnyImage.blend"
|
|
|
|
|
|
def node_asset_path():
|
|
return Path(__file__).resolve().parents[2] / "assets" / NODE_ASSET_NAME
|
|
|
|
|
|
def load_asset_node_group(group_name):
|
|
existing = bpy.data.node_groups.get(group_name)
|
|
if existing is not None:
|
|
return existing
|
|
|
|
asset_path = node_asset_path()
|
|
if not asset_path.is_file():
|
|
raise RuntimeError(f"Missing node asset: {asset_path}")
|
|
if bpy.app.version < (5, 0, 0):
|
|
directory = str(asset_path) + "/NodeTree/"
|
|
result = bpy.ops.wm.append(
|
|
filepath=directory + group_name,
|
|
directory=directory,
|
|
filename=group_name,
|
|
link=False,
|
|
do_reuse_local_id=True,
|
|
)
|
|
if "FINISHED" not in result:
|
|
raise RuntimeError("Unable to append the node asset")
|
|
node_group = bpy.data.node_groups.get(group_name)
|
|
if node_group is None:
|
|
raise RuntimeError("Appended node asset is unavailable")
|
|
node_group.use_fake_user = True
|
|
return node_group
|
|
|
|
with bpy.data.libraries.load(str(asset_path), link=True) as (source, target):
|
|
if group_name not in source.node_groups:
|
|
raise RuntimeError(f"Node asset is missing {group_name}")
|
|
target.node_groups = [group_name]
|
|
linked_group = target.node_groups[0]
|
|
node_group = bpy.data.pack_linked_ids_hierarchy(root_id=linked_group)
|
|
if linked_group != node_group and linked_group.users == 0:
|
|
bpy.data.node_groups.remove(linked_group)
|
|
node_group.use_fake_user = True
|
|
return node_group
|
|
|
|
|
|
def modifier_input_identifier(node_group, name):
|
|
for item in node_group.interface.items_tree:
|
|
if item.item_type != "SOCKET" or item.in_out != "INPUT":
|
|
continue
|
|
if item.name == name:
|
|
return item.identifier
|
|
raise RuntimeError(f"Geometry Nodes input is missing: {name}")
|
|
|
|
|
|
def _modifier_input_slots(modifier):
|
|
"""Return the runtime input collection, or None for the legacy API."""
|
|
properties = getattr(modifier, "properties", None)
|
|
if properties is None:
|
|
return None
|
|
return getattr(properties, "inputs", None)
|
|
|
|
|
|
def set_modifier_input(modifier, identifier, value):
|
|
"""Assign a value to a Geometry Nodes modifier input socket."""
|
|
inputs = _modifier_input_slots(modifier)
|
|
if inputs is not None:
|
|
# Blender 5.2+ exposes inputs as runtime RNA keyed by socket identifier.
|
|
getattr(inputs, identifier).value = value
|
|
else:
|
|
# Blender 4.x exposes inputs as modifier ID properties.
|
|
modifier[identifier] = value
|
|
|
|
|
|
DISPLACEMENT_STRENGTH = 1.0
|
|
DISPLACEMENT_MIDLEVEL = 0.0
|
|
BUMP_BASE_DISTANCE = 0.001
|
|
MATERIAL_DISPLACEMENT_ATTRIBUTE = "o_material_displacement"
|
|
BACKGROUND_DEPTH_ATTRIBUTE = "o_background_depth"
|
|
DEPTH_SCALE_ATTRIBUTE = "o_depth_scale"
|
|
IMAGE_MATERIAL_NODE_GROUP_NAME = "O Image Layer"
|
|
DEPTH_MATERIAL_NODE_GROUP_NAME = "O Image Depth Layer"
|
|
SHADELESS_NODE_GROUP_NAME = "O Shadeless"
|
|
VIEW_TRANSFORM_COLOR_SPACES = (
|
|
("ACES 1.3", "ACES 1.3 sRGB"),
|
|
("ACES 2.0", "ACES 2.0 sRGB"),
|
|
("AgX", "AgX Base sRGB"),
|
|
("Filmic", "Filmic sRGB"),
|
|
("Khronos PBR Neutral", "Khronos PBR Neutral sRGB"),
|
|
)
|
|
|
|
|
|
def material_color_space_name(scene):
|
|
view_settings = getattr(scene, "view_settings", None)
|
|
view_transform = getattr(view_settings, "view_transform", "")
|
|
normalized = str(view_transform).strip().casefold()
|
|
for view_name, color_space in VIEW_TRANSFORM_COLOR_SPACES:
|
|
if normalized.startswith(view_name.casefold()):
|
|
return color_space
|
|
return "sRGB"
|
|
|
|
|
|
def set_material_color_space(image, scene=None):
|
|
if scene is None:
|
|
scene = getattr(bpy.context, "scene", None)
|
|
settings = image.colorspace_settings
|
|
preferred = material_color_space_name(scene)
|
|
original = settings.name
|
|
pixels = None
|
|
if preferred != original and (
|
|
getattr(image, "source", None) == "GENERATED"
|
|
or getattr(image, "is_dirty", False)
|
|
):
|
|
pixels = image_pixels(image)
|
|
for color_space in dict.fromkeys((preferred, "sRGB", original)):
|
|
if not color_space:
|
|
continue
|
|
try:
|
|
settings.name = color_space
|
|
except (TypeError, ValueError):
|
|
continue
|
|
if settings.name == color_space:
|
|
if pixels is not None:
|
|
image.pixels.foreach_set(pixels)
|
|
image.update()
|
|
return color_space
|
|
return settings.name
|
|
|
|
|
|
def material_node_group(depth_plane=False):
|
|
group_name = (
|
|
DEPTH_MATERIAL_NODE_GROUP_NAME
|
|
if depth_plane
|
|
else IMAGE_MATERIAL_NODE_GROUP_NAME
|
|
)
|
|
existing = bpy.data.node_groups.get(group_name)
|
|
if existing is not None:
|
|
for item in existing.interface.items_tree:
|
|
if (
|
|
item.item_type == "SOCKET"
|
|
and item.in_out == "INPUT"
|
|
and item.name == "Normal"
|
|
):
|
|
item.default_value = (0.5, 0.5, 1.0, 1.0)
|
|
return existing
|
|
group = bpy.data.node_groups.new(group_name, "ShaderNodeTree")
|
|
interface = group.interface
|
|
input_sockets = [
|
|
("Color", "NodeSocketColor"),
|
|
("Alpha", "NodeSocketFloat"),
|
|
("Bump Scale", "NodeSocketFloat"),
|
|
]
|
|
normal_group = not depth_plane
|
|
if normal_group:
|
|
input_sockets.insert(2, ("Normal", "NodeSocketColor"))
|
|
input_sockets.insert(3, ("Normal Scale", "NodeSocketFloat"))
|
|
input_sockets.insert(4, ("Object Space", "NodeSocketBool"))
|
|
if depth_plane:
|
|
input_sockets.insert(2, ("Depth", "NodeSocketFloat"))
|
|
for name, socket_type in input_sockets:
|
|
socket = interface.new_socket(
|
|
name=name,
|
|
in_out="INPUT",
|
|
socket_type=socket_type,
|
|
)
|
|
if name == "Alpha":
|
|
socket.default_value = 1.0
|
|
socket.min_value = 0.0
|
|
socket.max_value = 1.0
|
|
socket.subtype = "FACTOR"
|
|
elif name == "Bump Scale":
|
|
socket.default_value = 0.0
|
|
socket.min_value = -10000.0
|
|
socket.max_value = 10000.0
|
|
elif name == "Normal":
|
|
socket.default_value = (0.5, 0.5, 1.0, 1.0)
|
|
elif name == "Normal Scale":
|
|
socket.default_value = 1.0
|
|
socket.min_value = 0.0
|
|
socket.max_value = 10.0
|
|
elif name == "Object Space":
|
|
socket.default_value = False
|
|
output_sockets = [
|
|
("Color", "NodeSocketColor"),
|
|
("Roughness", "NodeSocketFloat"),
|
|
("Alpha", "NodeSocketFloat"),
|
|
("Normal", "NodeSocketVector"),
|
|
]
|
|
if depth_plane:
|
|
output_sockets.append(("Displacement", "NodeSocketVector"))
|
|
for name, socket_type in output_sockets:
|
|
interface.new_socket(
|
|
name=name,
|
|
in_out="OUTPUT",
|
|
socket_type=socket_type,
|
|
)
|
|
|
|
nodes = group.nodes
|
|
links = group.links
|
|
group_input = nodes.new("NodeGroupInput")
|
|
group_input.location = (-500.0, 100.0)
|
|
group_output = nodes.new("NodeGroupOutput")
|
|
group_output.location = (300.0, 100.0)
|
|
roughness_curve = nodes.new("ShaderNodeFloatCurve")
|
|
roughness_curve.mapping.initialize()
|
|
curve_points = roughness_curve.mapping.curves[0].points
|
|
curve_points[0].location = (0.0, 0.5)
|
|
curve_points[-1].location = (1.0, 0.1)
|
|
roughness_curve.mapping.update()
|
|
roughness_curve.location = (-250.0, 350.0)
|
|
bump = nodes.new("ShaderNodeBump")
|
|
bump.location = (100.0, 0.0)
|
|
if normal_group:
|
|
tangent_normal = nodes.new("ShaderNodeNormalMap")
|
|
tangent_normal.space = "TANGENT"
|
|
tangent_normal.location = (-500.0, -100.0)
|
|
object_normal = nodes.new("ShaderNodeNormalMap")
|
|
object_normal.space = "OBJECT"
|
|
object_normal.location = (-500.0, -350.0)
|
|
choose_normal = nodes.new("ShaderNodeMix")
|
|
choose_normal.data_type = "VECTOR"
|
|
choose_normal.location = (-150.0, -150.0)
|
|
for normal_map in (tangent_normal, object_normal):
|
|
links.new(group_input.outputs["Normal"], normal_map.inputs["Color"])
|
|
links.new(
|
|
group_input.outputs["Normal Scale"],
|
|
normal_map.inputs["Strength"],
|
|
)
|
|
links.new(group_input.outputs["Object Space"], choose_normal.inputs[0])
|
|
links.new(tangent_normal.outputs["Normal"], choose_normal.inputs[4])
|
|
links.new(object_normal.outputs["Normal"], choose_normal.inputs[5])
|
|
links.new(choose_normal.outputs["Result"], bump.inputs["Normal"])
|
|
displacement = None
|
|
|
|
processed_alpha = group_input.outputs["Alpha"]
|
|
bump_depth = group_input.outputs["Color"]
|
|
if depth_plane:
|
|
displacement = nodes.new("ShaderNodeDisplacement")
|
|
displacement.inputs["Midlevel"].default_value = 0.0
|
|
displacement.inputs["Scale"].default_value = 1.0
|
|
group_input.location = (-1200.0, 100.0)
|
|
group_output.location = (1050.0, 100.0)
|
|
roughness_curve.location = (-300.0, 450.0)
|
|
bump.location = (750.0, 50.0)
|
|
displacement.location = (750.0, -300.0)
|
|
background_attribute = nodes.new("ShaderNodeAttribute")
|
|
background_attribute.attribute_type = "GEOMETRY"
|
|
background_attribute.attribute_name = BACKGROUND_DEPTH_ATTRIBUTE
|
|
background_attribute.location = (-950.0, -500.0)
|
|
material_attribute = nodes.new("ShaderNodeAttribute")
|
|
material_attribute.attribute_type = "GEOMETRY"
|
|
material_attribute.attribute_name = MATERIAL_DISPLACEMENT_ATTRIBUTE
|
|
material_attribute.location = (-50.0, -650.0)
|
|
strength_attribute = nodes.new("ShaderNodeAttribute")
|
|
strength_attribute.attribute_type = "GEOMETRY"
|
|
strength_attribute.attribute_name = DEPTH_SCALE_ATTRIBUTE
|
|
strength_attribute.location = (200.0, -650.0)
|
|
foreground_mask = nodes.new("ShaderNodeMath")
|
|
foreground_mask.operation = "GREATER_THAN"
|
|
foreground_mask.inputs[1].default_value = 0.5
|
|
foreground_mask.location = (-950.0, -250.0)
|
|
clamp_foreground = nodes.new("ShaderNodeMath")
|
|
clamp_foreground.operation = "MAXIMUM"
|
|
clamp_foreground.location = (-650.0, -400.0)
|
|
masked_depth = nodes.new("ShaderNodeMix")
|
|
masked_depth.data_type = "FLOAT"
|
|
masked_depth.location = (-350.0, -350.0)
|
|
enabled_depth = nodes.new("ShaderNodeMath")
|
|
enabled_depth.operation = "MULTIPLY"
|
|
enabled_depth.location = (250.0, -350.0)
|
|
scaled_depth = nodes.new("ShaderNodeMath")
|
|
scaled_depth.operation = "MULTIPLY"
|
|
scaled_depth.location = (450.0, -350.0)
|
|
links.new(group_input.outputs["Alpha"], foreground_mask.inputs[0])
|
|
links.new(group_input.outputs["Depth"], clamp_foreground.inputs[0])
|
|
links.new(background_attribute.outputs["Fac"], clamp_foreground.inputs[1])
|
|
links.new(foreground_mask.outputs[0], masked_depth.inputs[0])
|
|
links.new(background_attribute.outputs["Fac"], masked_depth.inputs[2])
|
|
links.new(clamp_foreground.outputs[0], masked_depth.inputs[3])
|
|
links.new(masked_depth.outputs["Result"], enabled_depth.inputs[0])
|
|
links.new(material_attribute.outputs["Fac"], enabled_depth.inputs[1])
|
|
links.new(enabled_depth.outputs[0], scaled_depth.inputs[0])
|
|
links.new(strength_attribute.outputs["Fac"], scaled_depth.inputs[1])
|
|
processed_alpha = foreground_mask.outputs[0]
|
|
processed_depth = scaled_depth.outputs[0]
|
|
|
|
links.new(group_input.outputs["Color"], group_output.inputs["Color"])
|
|
links.new(group_input.outputs["Color"], roughness_curve.inputs["Value"])
|
|
links.new(
|
|
roughness_curve.outputs["Value"],
|
|
group_output.inputs["Roughness"],
|
|
)
|
|
links.new(processed_alpha, group_output.inputs["Alpha"])
|
|
links.new(bump_depth, bump.inputs["Height"])
|
|
bump_distance = nodes.new("ShaderNodeMath")
|
|
bump_distance.operation = "MULTIPLY"
|
|
bump_distance.inputs[1].default_value = BUMP_BASE_DISTANCE
|
|
bump_distance.location = (500.0, 100.0) if depth_plane else (-250.0, 150.0)
|
|
links.new(group_input.outputs["Bump Scale"], bump_distance.inputs[0])
|
|
links.new(bump_distance.outputs[0], bump.inputs["Distance"])
|
|
links.new(bump.outputs["Normal"], group_output.inputs["Normal"])
|
|
if displacement is not None:
|
|
links.new(processed_depth, displacement.inputs["Height"])
|
|
links.new(
|
|
displacement.outputs["Displacement"],
|
|
group_output.inputs["Displacement"],
|
|
)
|
|
group.color_tag = "SHADER"
|
|
group.description = (
|
|
"Prepare image depth material outputs."
|
|
if depth_plane
|
|
else f"Prepare {group_name} material outputs."
|
|
)
|
|
group.use_fake_user = True
|
|
return group
|
|
|
|
|
|
def create_image_material(
|
|
source_object,
|
|
color_image,
|
|
depth_scale,
|
|
displacement_image=None,
|
|
displacement_midlevel=0.0,
|
|
displacement_scale=1.0,
|
|
shadeless=False,
|
|
frame_count=1,
|
|
frame_start=1,
|
|
color_frame_offset=0,
|
|
color_frame_duration=None,
|
|
depth_frame_offset=0,
|
|
depth_frame_duration=None,
|
|
use_cyclic=None,
|
|
normal_image=None,
|
|
normal_space="TANGENT",
|
|
ior=None,
|
|
scene=None,
|
|
):
|
|
material = bpy.data.materials.new(image_base_name(source_object.data))
|
|
material.use_nodes = True
|
|
if hasattr(material, "surface_render_method"):
|
|
material.surface_render_method = "DITHERED"
|
|
nodes = material.node_tree.nodes
|
|
nodes.clear()
|
|
|
|
color_texture = nodes.new("ShaderNodeTexImage")
|
|
uv_map = nodes.new("ShaderNodeUVMap")
|
|
output = nodes.new("ShaderNodeOutputMaterial")
|
|
|
|
uv_map.uv_map = "UVMap"
|
|
uv_map.location = (-1250.0, 350.0)
|
|
material_image = color_image
|
|
set_material_color_space(material_image, scene)
|
|
color_texture.image = material_image
|
|
color_texture.interpolation = "Linear"
|
|
color_texture.extension = "EXTEND"
|
|
color_texture.location = (-1000.0, 350.0)
|
|
output.location = (850.0, 350.0)
|
|
extracted_frames = frame_count if frame_count > 1 else None
|
|
color_frames = (
|
|
color_frame_duration if color_frame_duration is not None else extracted_frames
|
|
)
|
|
depth_frames = (
|
|
depth_frame_duration if depth_frame_duration is not None else extracted_frames
|
|
)
|
|
material_frames = color_frames
|
|
material_frame_offset = color_frame_offset
|
|
if frame_count > 1 or is_animated_image(material_image):
|
|
configure_image_user(
|
|
color_texture.image_user,
|
|
material_frames,
|
|
frame_start=frame_start,
|
|
frame_offset=material_frame_offset,
|
|
use_cyclic=use_cyclic,
|
|
)
|
|
|
|
links = material.node_tree.links
|
|
links.new(uv_map.outputs["UV"], color_texture.inputs["Vector"])
|
|
alpha = color_texture.outputs["Alpha"]
|
|
material_inputs = nodes.new("ShaderNodeGroup")
|
|
is_depth_plane = displacement_image is not None
|
|
material_inputs.node_tree = material_node_group(is_depth_plane)
|
|
material_inputs.location = (-300.0, 350.0)
|
|
material_inputs.inputs["Bump Scale"].default_value = depth_scale
|
|
if "Object Space" in material_inputs.inputs:
|
|
if normal_space not in {"TANGENT", "OBJECT"}:
|
|
raise ValueError(f"Unsupported normal space: {normal_space}")
|
|
material_inputs.inputs["Object Space"].default_value = normal_space == "OBJECT"
|
|
links.new(color_texture.outputs["Color"], material_inputs.inputs["Color"])
|
|
links.new(alpha, material_inputs.inputs["Alpha"])
|
|
if normal_image is not None and "Normal" in material_inputs.inputs:
|
|
normal_texture = nodes.new("ShaderNodeTexImage")
|
|
normal_texture.image = normal_image
|
|
normal_texture.interpolation = "Linear"
|
|
normal_texture.extension = "EXTEND"
|
|
normal_texture.location = (-1000.0, -50.0)
|
|
links.new(uv_map.outputs["UV"], normal_texture.inputs["Vector"])
|
|
links.new(normal_texture.outputs["Color"], material_inputs.inputs["Normal"])
|
|
if shadeless:
|
|
shader = nodes.new("ShaderNodeGroup")
|
|
shader.node_tree = load_asset_node_group(SHADELESS_NODE_GROUP_NAME)
|
|
shader.name = SHADELESS_NODE_GROUP_NAME
|
|
transparent = nodes.new("ShaderNodeBsdfTransparent")
|
|
mix_shader = nodes.new("ShaderNodeMixShader")
|
|
shader.location = (0.0, 550.0)
|
|
transparent.location = (0.0, 150.0)
|
|
mix_shader.location = (300.0, 350.0)
|
|
links.new(
|
|
material_inputs.outputs["Color"],
|
|
shader.inputs["Color"],
|
|
)
|
|
links.new(
|
|
material_inputs.outputs["Alpha"],
|
|
mix_shader.inputs["Fac"],
|
|
)
|
|
links.new(
|
|
transparent.outputs["BSDF"],
|
|
mix_shader.inputs[1],
|
|
)
|
|
links.new(
|
|
shader.outputs["Shader"],
|
|
mix_shader.inputs[2],
|
|
)
|
|
links.new(
|
|
mix_shader.outputs["Shader"],
|
|
output.inputs["Surface"],
|
|
)
|
|
else:
|
|
shader = nodes.new("ShaderNodeBsdfPrincipled")
|
|
shader.location = (250.0, 400.0)
|
|
if ior is not None:
|
|
shader.inputs["IOR"].default_value = float(ior)
|
|
|
|
links.new(
|
|
material_inputs.outputs["Color"],
|
|
shader.inputs["Base Color"],
|
|
)
|
|
links.new(
|
|
material_inputs.outputs["Color"],
|
|
shader.inputs["Subsurface Radius"],
|
|
)
|
|
links.new(
|
|
material_inputs.outputs["Color"],
|
|
shader.inputs["Emission Color"],
|
|
)
|
|
links.new(material_inputs.outputs["Alpha"], shader.inputs["Alpha"])
|
|
links.new(material_inputs.outputs["Normal"], shader.inputs["Normal"])
|
|
links.new(
|
|
material_inputs.outputs["Normal"],
|
|
shader.inputs["Coat Normal"],
|
|
)
|
|
links.new(
|
|
material_inputs.outputs["Roughness"],
|
|
shader.inputs["Roughness"],
|
|
)
|
|
links.new(shader.outputs["BSDF"], output.inputs["Surface"])
|
|
|
|
if displacement_image is not None:
|
|
depth_texture = nodes.new("ShaderNodeTexImage")
|
|
depth_texture.image = displacement_image
|
|
depth_texture.interpolation = "Linear"
|
|
depth_texture.extension = "EXTEND"
|
|
if frame_count > 1 or is_animated_image(displacement_image):
|
|
configure_image_user(
|
|
depth_texture.image_user,
|
|
depth_frames,
|
|
frame_start=frame_start,
|
|
frame_offset=depth_frame_offset,
|
|
use_cyclic=use_cyclic,
|
|
)
|
|
depth_texture.location = (-1000.0, -400.0)
|
|
links.new(uv_map.outputs["UV"], depth_texture.inputs["Vector"])
|
|
displacement_height = depth_texture.outputs["Color"]
|
|
if displacement_midlevel != 0.0 or displacement_scale != 1.0:
|
|
depth_adjust = nodes.new("ShaderNodeMath")
|
|
depth_adjust.operation = "SUBTRACT"
|
|
depth_adjust.inputs[1].default_value = displacement_midlevel
|
|
depth_adjust.location = (500.0, -550.0)
|
|
depth_scale_node = nodes.new("ShaderNodeMath")
|
|
depth_scale_node.operation = "MULTIPLY"
|
|
depth_scale_node.inputs[1].default_value = displacement_scale
|
|
depth_scale_node.location = (700.0, -550.0)
|
|
links.new(displacement_height, depth_adjust.inputs[0])
|
|
links.new(depth_adjust.outputs[0], depth_scale_node.inputs[0])
|
|
links.new(depth_scale_node.outputs[0], material_inputs.inputs["Depth"])
|
|
else:
|
|
links.new(displacement_height, material_inputs.inputs["Depth"])
|
|
links.new(
|
|
material_inputs.outputs["Displacement"],
|
|
output.inputs["Displacement"],
|
|
)
|
|
set_displacement_only(material)
|
|
return material
|
|
|
|
|
|
def set_displacement_only(material):
|
|
for owner, property_name in (
|
|
(material, "surface_displacement_method"),
|
|
(material, "displacement_method"),
|
|
(getattr(material, "cycles", None), "displacement_method"),
|
|
):
|
|
if owner is not None and hasattr(owner, property_name):
|
|
setattr(owner, property_name, "DISPLACEMENT")
|
|
return
|
|
|
|
|
|
def finalize_object_result(
|
|
context,
|
|
source_object,
|
|
result_object,
|
|
*,
|
|
keep_source=False,
|
|
):
|
|
"""Activate a result object and optionally replace its source object."""
|
|
source_name = source_object.name
|
|
for selected in context.selected_objects:
|
|
selected.select_set(False)
|
|
result_object.select_set(True)
|
|
context.view_layer.objects.active = result_object
|
|
if not keep_source:
|
|
bpy.data.objects.remove(source_object, do_unlink=True)
|
|
result_object.name = source_name
|
|
return result_object
|