feat: Introduce Balloon Lasso Tool for mesh extraction
- Added BalloonTool workspace tool to facilitate the extraction of balloon meshes in the 3D viewport. - Implemented ActivateBalloonTool operator to activate the Balloon Lasso Tool. - Refactored BalloonLasso operator into ExtractBalloon for better clarity and functionality. - Enhanced properties for balloon extraction, including mode, thickness, resolution, and material reuse options. - Updated SnapFormSettings to include new balloon-related properties. - Improved material and image handling for balloon extraction, ensuring compatibility and reusability. - Added tests to validate the registration and functionality of the new balloon tools and properties.
This commit is contained in:
@@ -2,6 +2,7 @@ import bpy
|
||||
|
||||
from .runtime import runtime
|
||||
from .operators import CLASSES as OPERATOR_CLASSES
|
||||
from .operators.balloon import BalloonTool
|
||||
from .preferences import SnapFormPreferences
|
||||
from .properties import SnapFormSettings
|
||||
from .menu import SnapFormImageMenu, draw_image_context_menu
|
||||
@@ -32,6 +33,10 @@ CLASSES = (
|
||||
def register():
|
||||
for class_type in CLASSES:
|
||||
bpy.utils.register_class(class_type)
|
||||
bpy.utils.register_tool(
|
||||
BalloonTool,
|
||||
separator=False,
|
||||
)
|
||||
bpy.types.Scene.snapform_settings = bpy.props.PointerProperty(
|
||||
type=SnapFormSettings,
|
||||
)
|
||||
@@ -42,6 +47,7 @@ def register():
|
||||
|
||||
def unregister():
|
||||
runtime.unregister()
|
||||
bpy.utils.unregister_tool(BalloonTool)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.remove(draw_image_context_menu)
|
||||
bpy.types.OUTLINER_MT_object.remove(draw_image_context_menu)
|
||||
if hasattr(bpy.types.Scene, "snapform_settings"):
|
||||
|
||||
@@ -14,8 +14,8 @@ class SnapFormImageMenu(bpy.types.Menu):
|
||||
layout = self.layout
|
||||
layout.operator_context = "INVOKE_DEFAULT"
|
||||
layout.operator(
|
||||
"snapform.balloon_lasso",
|
||||
text="Balloon Mesh (Lasso)",
|
||||
"snapform.activate_balloon_tool",
|
||||
text="Balloon Lasso Tool",
|
||||
icon="MOD_SKIN",
|
||||
)
|
||||
layout.separator()
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from .background import ProcessImageBackground, RemoveImageBackground
|
||||
from .balloon import BalloonLasso, ExtractBalloon
|
||||
from .balloon import (
|
||||
ActivateBalloonTool,
|
||||
ExtractBalloon,
|
||||
)
|
||||
from .debug import DebugBen2, DebugDa3, DebugMoge2
|
||||
from .depth_plane import (
|
||||
ConvertToDepthPlane,
|
||||
@@ -27,7 +30,7 @@ CLASSES = (
|
||||
ExtractOriginalFrames,
|
||||
ClearModels,
|
||||
ExtractBalloon,
|
||||
BalloonLasso,
|
||||
ActivateBalloonTool,
|
||||
RemoveImageBackground,
|
||||
ConvertToDepthPlane,
|
||||
ConvertToSurface,
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
from .operator import BalloonLasso, ExtractBalloon
|
||||
from .operator import (
|
||||
ActivateBalloonTool,
|
||||
BalloonTool,
|
||||
ExtractBalloon,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,18 @@ import math
|
||||
import bpy
|
||||
|
||||
from .geometry import INFLATION, POINT_SPACING, triangulate_balloon
|
||||
from ..image import color_data_name, is_image_empty, plane_bounds
|
||||
from ...properties import (
|
||||
balloon_mode_property as mode_property,
|
||||
balloon_resolution_property as resolution_property,
|
||||
balloon_reuse_material_property as reuse_material_property,
|
||||
balloon_thickness_property as thickness_property,
|
||||
)
|
||||
from ..image import (
|
||||
color_data_name,
|
||||
is_image_empty,
|
||||
material_data_name,
|
||||
plane_bounds,
|
||||
)
|
||||
from ..material import TEXTURE_MATERIAL_NODE_GROUP_NAME, create_material
|
||||
from ..surface import assign_vertex_uv
|
||||
|
||||
@@ -99,6 +110,55 @@ def balloon_geometry_parameters(thickness, resolution):
|
||||
return resolution, INFLATION * thickness
|
||||
|
||||
|
||||
def data_name_matches(name, base_name):
|
||||
if name == base_name:
|
||||
return True
|
||||
suffix = name.removeprefix(f"{base_name}.")
|
||||
return suffix != name and suffix.isdigit()
|
||||
|
||||
|
||||
def material_uses_balloon_texture(material, color_name):
|
||||
node_tree = getattr(material, "node_tree", None)
|
||||
if node_tree is None:
|
||||
return False
|
||||
has_texture_layer = any(
|
||||
getattr(node, "type", None) == "GROUP"
|
||||
and getattr(getattr(node, "node_tree", None), "name", None)
|
||||
== TEXTURE_MATERIAL_NODE_GROUP_NAME
|
||||
for node in node_tree.nodes
|
||||
)
|
||||
has_color_image = any(
|
||||
getattr(node, "type", None) == "TEX_IMAGE"
|
||||
and getattr(node, "image", None) is not None
|
||||
and data_name_matches(node.image.name, color_name)
|
||||
for node in node_tree.nodes
|
||||
)
|
||||
return has_texture_layer and has_color_image
|
||||
|
||||
|
||||
def reusable_balloon_material(material_name, color_name):
|
||||
return next(
|
||||
(
|
||||
material
|
||||
for material in bpy.data.materials
|
||||
if data_name_matches(material.name, material_name)
|
||||
and material_uses_balloon_texture(material, color_name)
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def reusable_color_image(color_name):
|
||||
return next(
|
||||
(
|
||||
image
|
||||
for image in bpy.data.images
|
||||
if data_name_matches(image.name, color_name)
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def extract_balloon_object(
|
||||
context,
|
||||
source_object,
|
||||
@@ -108,6 +168,7 @@ def extract_balloon_object(
|
||||
thickness=1.0,
|
||||
resolution=POINT_SPACING,
|
||||
select_result=True,
|
||||
reuse_material=True,
|
||||
):
|
||||
image = source_object.data
|
||||
image_size = tuple(image.size)
|
||||
@@ -130,19 +191,37 @@ def extract_balloon_object(
|
||||
|
||||
source_name = source_object.name
|
||||
balloon_name = source_name
|
||||
color_image = image.copy()
|
||||
color_image.name = color_data_name(source_object)
|
||||
material = None
|
||||
material_name = material_data_name(source_object)
|
||||
color_name = color_data_name(source_object)
|
||||
color_image = None
|
||||
material = (
|
||||
reusable_balloon_material(material_name, color_name)
|
||||
if reuse_material
|
||||
else None
|
||||
)
|
||||
owns_color_image = False
|
||||
owns_material = False
|
||||
mesh = None
|
||||
balloon = None
|
||||
try:
|
||||
material = create_material(
|
||||
balloon_name,
|
||||
color_image,
|
||||
0.0,
|
||||
surface_group_name=TEXTURE_MATERIAL_NODE_GROUP_NAME,
|
||||
scene=context.scene,
|
||||
)
|
||||
if material is None:
|
||||
color_image = (
|
||||
reusable_color_image(color_name)
|
||||
if reuse_material
|
||||
else None
|
||||
)
|
||||
if color_image is None:
|
||||
color_image = image.copy()
|
||||
color_image.name = color_name
|
||||
owns_color_image = True
|
||||
material = create_material(
|
||||
material_name,
|
||||
color_image,
|
||||
0.0,
|
||||
surface_group_name=TEXTURE_MATERIAL_NODE_GROUP_NAME,
|
||||
scene=context.scene,
|
||||
)
|
||||
owns_material = True
|
||||
mesh = bpy.data.meshes.new(balloon_name)
|
||||
mesh.from_pydata(vertices, [], faces)
|
||||
mesh.update()
|
||||
@@ -164,9 +243,13 @@ def extract_balloon_object(
|
||||
bpy.data.objects.remove(balloon, do_unlink=True)
|
||||
if mesh is not None and mesh.users == 0:
|
||||
bpy.data.meshes.remove(mesh)
|
||||
if material is not None and material.users == 0:
|
||||
if owns_material and material is not None and material.users == 0:
|
||||
bpy.data.materials.remove(material)
|
||||
if color_image.users == 0:
|
||||
if (
|
||||
owns_color_image
|
||||
and color_image is not None
|
||||
and color_image.users == 0
|
||||
):
|
||||
bpy.data.images.remove(color_image)
|
||||
raise
|
||||
|
||||
@@ -177,97 +260,44 @@ def extract_balloon_object(
|
||||
return balloon
|
||||
|
||||
|
||||
def thickness_property():
|
||||
return bpy.props.FloatProperty(
|
||||
name="Thickness",
|
||||
description="Scale the Balloon thickness while keeping the lasso region",
|
||||
default=1.0,
|
||||
min=0.05,
|
||||
soft_max=5.0,
|
||||
)
|
||||
|
||||
|
||||
def mode_property():
|
||||
return bpy.props.EnumProperty(
|
||||
name="Mode",
|
||||
description="Algorithm used to inflate the saved lasso region",
|
||||
items=(
|
||||
(
|
||||
"POISSON",
|
||||
"Poisson",
|
||||
"Use a global Poisson field for smooth inflation",
|
||||
),
|
||||
(
|
||||
"TEDDY",
|
||||
"Teddy / Chordal Axis",
|
||||
"Use a Teddy-style chordal axis and local oval sections",
|
||||
),
|
||||
),
|
||||
default="POISSON",
|
||||
)
|
||||
|
||||
|
||||
def resolution_property():
|
||||
return bpy.props.FloatProperty(
|
||||
name="Resolution",
|
||||
description=(
|
||||
"World-space distance between sampled mesh points; smaller "
|
||||
"values create more geometry"
|
||||
),
|
||||
default=POINT_SPACING,
|
||||
min=0.005,
|
||||
soft_min=0.02,
|
||||
soft_max=0.5,
|
||||
max=10.0,
|
||||
subtype="DISTANCE",
|
||||
unit="LENGTH",
|
||||
)
|
||||
|
||||
|
||||
def hidden_string_property():
|
||||
return bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
|
||||
|
||||
def hidden_bool_property(default=False):
|
||||
return bpy.props.BoolProperty(
|
||||
default=default,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
def meters_to_world_distance(scene, meters):
|
||||
unit_settings = getattr(scene, "unit_settings", None)
|
||||
scale_length = getattr(unit_settings, "scale_length", 1.0)
|
||||
if scale_length <= 0.0:
|
||||
scale_length = 1.0
|
||||
return meters / scale_length
|
||||
|
||||
|
||||
def draw_line_strip(vertices, color, width=2.0):
|
||||
import gpu
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
batch = batch_for_shader(shader, "LINE_STRIP", {"pos": vertices})
|
||||
gpu.state.blend_set("ALPHA")
|
||||
gpu.state.line_width_set(width)
|
||||
shader.bind()
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
gpu.state.line_width_set(1.0)
|
||||
gpu.state.blend_set("NONE")
|
||||
|
||||
|
||||
def drawing_in_region(area_pointer, region_pointer):
|
||||
area = getattr(bpy.context, "area", None)
|
||||
region = getattr(bpy.context, "region", None)
|
||||
return (
|
||||
area is not None
|
||||
and region is not None
|
||||
and area.as_pointer() == area_pointer
|
||||
and region.as_pointer() == region_pointer
|
||||
)
|
||||
|
||||
|
||||
def event_over_interface_region(context, event):
|
||||
area = getattr(context, "area", None)
|
||||
if area is None:
|
||||
return False
|
||||
mouse_x = getattr(event, "mouse_x", -1)
|
||||
mouse_y = getattr(event, "mouse_y", -1)
|
||||
for region in area.regions:
|
||||
if region.type == "WINDOW":
|
||||
continue
|
||||
if (
|
||||
region.x <= mouse_x < region.x + region.width
|
||||
and region.y <= mouse_y < region.y + region.height
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
VIEW_NAVIGATION_EVENTS = {
|
||||
"MIDDLEMOUSE",
|
||||
"WHEELUPMOUSE",
|
||||
"WHEELDOWNMOUSE",
|
||||
"WHEELINMOUSE",
|
||||
"WHEELOUTMOUSE",
|
||||
"WHEELLEFTMOUSE",
|
||||
"WHEELRIGHTMOUSE",
|
||||
"TRACKPADPAN",
|
||||
"TRACKPADZOOM",
|
||||
"MOUSEROTATE",
|
||||
"MOUSESMARTZOOM",
|
||||
"NDOF_MOTION",
|
||||
}
|
||||
|
||||
|
||||
class ExtractBalloon(bpy.types.Operator):
|
||||
bl_idname = "snapform.extract_balloon"
|
||||
bl_label = "Extract Balloon"
|
||||
@@ -278,148 +308,94 @@ class ExtractBalloon(bpy.types.Operator):
|
||||
thickness: thickness_property()
|
||||
resolution: resolution_property()
|
||||
source_object_name: hidden_string_property()
|
||||
reuse_material: reuse_material_property()
|
||||
source_matrix_data: hidden_string_property()
|
||||
selection_data: hidden_string_property()
|
||||
keep_source_selected: hidden_bool_property()
|
||||
|
||||
_path = None
|
||||
_handle = None
|
||||
_area_pointer = 0
|
||||
_region_pointer = 0
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "mode")
|
||||
layout.prop(self, "thickness")
|
||||
layout.prop(self, "resolution")
|
||||
|
||||
def execute(self, context):
|
||||
source_object = bpy.data.objects.get(self.source_object_name)
|
||||
if not is_image_empty(source_object):
|
||||
self.report({"ERROR"}, "The source Image Empty is no longer available")
|
||||
return {"CANCELLED"}
|
||||
try:
|
||||
polygon = json.loads(self.selection_data)
|
||||
if len(polygon) < 3:
|
||||
raise ValueError("The stored lasso region is invalid")
|
||||
matrix_world = deserialize_matrix(self.source_matrix_data)
|
||||
extract_balloon_object(
|
||||
context,
|
||||
source_object,
|
||||
polygon,
|
||||
matrix_world,
|
||||
self.mode,
|
||||
thickness=self.thickness,
|
||||
resolution=self.resolution,
|
||||
select_result=not self.keep_source_selected,
|
||||
)
|
||||
except (json.JSONDecodeError, RuntimeError, TypeError, ValueError) as error:
|
||||
self.report({"ERROR"}, str(error))
|
||||
return {"CANCELLED"}
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BalloonLasso(bpy.types.Operator):
|
||||
bl_idname = "snapform.balloon_lasso"
|
||||
bl_label = "Balloon Mesh"
|
||||
bl_description = "Continuously draw lasso regions and extract Balloon meshes"
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
mode: mode_property()
|
||||
thickness: thickness_property()
|
||||
resolution: resolution_property()
|
||||
source_object_name: hidden_string_property()
|
||||
source_matrix_data: hidden_string_property()
|
||||
|
||||
_path = None
|
||||
_drawing = False
|
||||
_handle = None
|
||||
layout.prop(self, "resolution", text="Resolution (m)")
|
||||
layout.prop(self, "reuse_material")
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.area is not None and context.area.type == "VIEW_3D"
|
||||
return (
|
||||
context.area is not None
|
||||
and context.area.type == "VIEW_3D"
|
||||
and is_image_empty(context.active_object)
|
||||
)
|
||||
|
||||
def _draw_lasso(self, context):
|
||||
if not self._path:
|
||||
def _draw_lasso(self):
|
||||
if (
|
||||
not self._path
|
||||
or not drawing_in_region(self._area_pointer, self._region_pointer)
|
||||
):
|
||||
return
|
||||
import gpu
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
vertices = list(self._path)
|
||||
if len(vertices) > 2:
|
||||
vertices.append(vertices[0])
|
||||
shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
batch = batch_for_shader(shader, "LINE_STRIP", {"pos": vertices})
|
||||
gpu.state.blend_set("ALPHA")
|
||||
gpu.state.line_width_set(2.0)
|
||||
shader.bind()
|
||||
shader.uniform_float("color", (0.15, 0.65, 1.0, 0.95))
|
||||
batch.draw(shader)
|
||||
gpu.state.line_width_set(1.0)
|
||||
gpu.state.blend_set("NONE")
|
||||
draw_line_strip(vertices, (0.15, 0.65, 1.0, 0.95), 2.0)
|
||||
|
||||
def _finish(self, context):
|
||||
if self._handle is not None:
|
||||
bpy.types.SpaceView3D.draw_handler_remove(self._handle, "WINDOW")
|
||||
self._handle = None
|
||||
context.workspace.status_text_set(None)
|
||||
if context.area is not None:
|
||||
context.area.header_text_set(None)
|
||||
context.area.tag_redraw()
|
||||
|
||||
def cancel(self, context):
|
||||
self._finish(context)
|
||||
|
||||
def invoke(self, context, event):
|
||||
source_object = context.active_object
|
||||
if not is_image_empty(source_object):
|
||||
self.report({"ERROR"}, "Select an Image Empty before extracting")
|
||||
return {"CANCELLED"}
|
||||
settings = context.scene.snapform_settings
|
||||
self.mode = settings.balloon_mode
|
||||
self.thickness = settings.balloon_thickness
|
||||
self.resolution = settings.balloon_resolution
|
||||
self.reuse_material = settings.balloon_reuse_material
|
||||
self.source_object_name = source_object.name
|
||||
self.source_matrix_data = serialize_matrix(source_object.matrix_world)
|
||||
self._path = []
|
||||
self._drawing = False
|
||||
self.selection_data = ""
|
||||
self._path = [(event.mouse_region_x, event.mouse_region_y)]
|
||||
self._area_pointer = context.area.as_pointer()
|
||||
self._region_pointer = context.region.as_pointer()
|
||||
self._handle = bpy.types.SpaceView3D.draw_handler_add(
|
||||
self._draw_lasso,
|
||||
(context,),
|
||||
(),
|
||||
"WINDOW",
|
||||
"POST_PIXEL",
|
||||
)
|
||||
context.window_manager.modal_handler_add(self)
|
||||
context.area.header_text_set(
|
||||
"Balloon Mesh: drag LMB repeatedly, navigate normally, Esc to finish"
|
||||
context.workspace.status_text_set(
|
||||
"Balloon Lasso: release LMB to extract, RMB or Esc to cancel"
|
||||
)
|
||||
context.window_manager.modal_handler_add(self)
|
||||
context.area.tag_redraw()
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def modal(self, context, event):
|
||||
if event.type == "ESC":
|
||||
if event.type in {"ESC", "RIGHTMOUSE"} and event.value == "PRESS":
|
||||
self._finish(context)
|
||||
return {"CANCELLED"}
|
||||
|
||||
if (
|
||||
event.type in VIEW_NAVIGATION_EVENTS
|
||||
or event_over_interface_region(context, event)
|
||||
or (event.type == "LEFTMOUSE" and event.alt)
|
||||
):
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
if event.type == "RIGHTMOUSE" and event.value == "PRESS":
|
||||
self._drawing = False
|
||||
self._path = []
|
||||
context.area.tag_redraw()
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
if event.type == "LEFTMOUSE" and event.value == "PRESS":
|
||||
self._drawing = True
|
||||
self._path = [(event.mouse_region_x, event.mouse_region_y)]
|
||||
context.area.tag_redraw()
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
if event.type == "MOUSEMOVE" and self._drawing:
|
||||
if event.type == "MOUSEMOVE":
|
||||
self._path.append((event.mouse_region_x, event.mouse_region_y))
|
||||
context.area.tag_redraw()
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
if event.type == "LEFTMOUSE" and event.value == "RELEASE" and self._drawing:
|
||||
self._drawing = False
|
||||
if event.type == "LEFTMOUSE" and event.value == "RELEASE":
|
||||
path = simplify_screen_path(self._path)
|
||||
self._path = []
|
||||
if len(path) < 3:
|
||||
self.report({"WARNING"}, "Draw a larger lasso")
|
||||
context.area.tag_redraw()
|
||||
return {"RUNNING_MODAL"}
|
||||
self._finish(context)
|
||||
return {"CANCELLED"}
|
||||
|
||||
source_object = bpy.data.objects.get(self.source_object_name)
|
||||
if not is_image_empty(source_object):
|
||||
@@ -436,21 +412,125 @@ class BalloonLasso(bpy.types.Operator):
|
||||
plane_bounds(source_object),
|
||||
tuple(source_object.data.size),
|
||||
)
|
||||
result = bpy.ops.snapform.extract_balloon(
|
||||
"EXEC_DEFAULT",
|
||||
source_object_name=self.source_object_name,
|
||||
source_matrix_data=self.source_matrix_data,
|
||||
selection_data=json.dumps(polygon, separators=(",", ":")),
|
||||
mode=self.mode,
|
||||
thickness=self.thickness,
|
||||
resolution=self.resolution,
|
||||
keep_source_selected=True,
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
raise RuntimeError("Unable to extract the Balloon")
|
||||
except (json.JSONDecodeError, RuntimeError, TypeError, ValueError) as error:
|
||||
self.selection_data = json.dumps(polygon, separators=(",", ":"))
|
||||
except (RuntimeError, TypeError, ValueError) as error:
|
||||
self.report({"ERROR"}, str(error))
|
||||
context.area.tag_redraw()
|
||||
return {"RUNNING_MODAL"}
|
||||
self._finish(context)
|
||||
return {"CANCELLED"}
|
||||
|
||||
self._finish(context)
|
||||
return self.execute(context)
|
||||
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
def execute(self, context):
|
||||
source_object = bpy.data.objects.get(self.source_object_name)
|
||||
if not is_image_empty(source_object):
|
||||
self.report({"ERROR"}, "The source Image Empty is no longer available")
|
||||
return {"CANCELLED"}
|
||||
try:
|
||||
polygon = json.loads(self.selection_data)
|
||||
if len(polygon) < 3:
|
||||
raise ValueError("The stored lasso region is invalid")
|
||||
matrix_world = deserialize_matrix(self.source_matrix_data)
|
||||
resolution_world = meters_to_world_distance(
|
||||
context.scene,
|
||||
self.resolution,
|
||||
)
|
||||
balloon = extract_balloon_object(
|
||||
context,
|
||||
source_object,
|
||||
polygon,
|
||||
matrix_world,
|
||||
self.mode,
|
||||
thickness=self.thickness,
|
||||
resolution=resolution_world,
|
||||
select_result=False,
|
||||
reuse_material=self.reuse_material,
|
||||
)
|
||||
balloon["snapform_resolution_m"] = self.resolution
|
||||
except (json.JSONDecodeError, RuntimeError, TypeError, ValueError) as error:
|
||||
self.report({"ERROR"}, str(error))
|
||||
return {"CANCELLED"}
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
class ActivateBalloonTool(bpy.types.Operator):
|
||||
bl_idname = "snapform.activate_balloon_tool"
|
||||
bl_label = "Balloon Lasso Tool"
|
||||
bl_description = "Activate the Balloon Lasso tool in the 3D Viewport"
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (
|
||||
context.area is not None
|
||||
and context.area.type == "VIEW_3D"
|
||||
and is_image_empty(context.active_object)
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
return bpy.ops.wm.tool_set_by_id(
|
||||
name=BalloonTool.bl_idname,
|
||||
space_type="VIEW_3D",
|
||||
)
|
||||
|
||||
|
||||
class BalloonTool(bpy.types.WorkSpaceTool):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_context_mode = "OBJECT"
|
||||
bl_idname = "snapform.balloon_lasso"
|
||||
bl_label = "Balloon Lasso"
|
||||
bl_description = (
|
||||
"Draw an Image Empty region and extract one independently undoable "
|
||||
"Balloon per stroke"
|
||||
)
|
||||
bl_icon = "ops.mesh.polybuild_hover"
|
||||
bl_cursor = "PAINT_BRUSH"
|
||||
bl_operator = ExtractBalloon.bl_idname
|
||||
bl_keymap = (
|
||||
(
|
||||
ExtractBalloon.bl_idname,
|
||||
{"type": "LEFTMOUSE", "value": "PRESS"},
|
||||
None,
|
||||
),
|
||||
(
|
||||
"wm.radial_control",
|
||||
{"type": "S", "value": "PRESS"},
|
||||
{
|
||||
"properties": [
|
||||
(
|
||||
"data_path_primary",
|
||||
"scene.snapform_settings.balloon_thickness",
|
||||
),
|
||||
(
|
||||
"zoom_path",
|
||||
"scene.snapform_settings.balloon_thickness_radial_zoom",
|
||||
)
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"wm.radial_control",
|
||||
{"type": "D", "value": "PRESS"},
|
||||
{
|
||||
"properties": [
|
||||
(
|
||||
"data_path_primary",
|
||||
"scene.snapform_settings.balloon_resolution",
|
||||
),
|
||||
(
|
||||
"zoom_path",
|
||||
"scene.snapform_settings.balloon_resolution_radial_zoom",
|
||||
)
|
||||
]
|
||||
},
|
||||
), )
|
||||
|
||||
def draw_settings(context, layout, tool):
|
||||
settings = context.scene.snapform_settings
|
||||
layout.prop(settings, "balloon_mode", text="")
|
||||
layout.prop(settings, "balloon_thickness")
|
||||
layout.prop(settings, "balloon_resolution", text="Resolution (m)")
|
||||
layout.prop(settings, "balloon_reuse_material")
|
||||
@@ -6,6 +6,7 @@ from ..image import (
|
||||
depth_data_name,
|
||||
image_user_settings,
|
||||
is_animated_image,
|
||||
material_data_name,
|
||||
plane_bounds,
|
||||
sorted_frame_paths,
|
||||
)
|
||||
@@ -298,7 +299,7 @@ def build_image_depth_plane(
|
||||
|
||||
depth_scale = abs(float(plane_object.scale.z))
|
||||
material = create_material(
|
||||
source_name,
|
||||
material_data_name(source_object),
|
||||
material_image,
|
||||
depth_scale,
|
||||
displacement_image=depth_source_image,
|
||||
@@ -401,7 +402,7 @@ def build_depth_plane(
|
||||
)
|
||||
|
||||
material = create_material(
|
||||
source_name,
|
||||
material_data_name(source_object),
|
||||
material_image,
|
||||
depth_scale,
|
||||
displacement_image=depth_image,
|
||||
|
||||
@@ -159,6 +159,10 @@ def color_data_name(source_object):
|
||||
return f"{image_base_name(source_object.data)}.color"
|
||||
|
||||
|
||||
def material_data_name(source_object):
|
||||
return image_base_name(source_object.data)
|
||||
|
||||
|
||||
def scaled_process_resolution(image, scale_percent):
|
||||
"""Return the scaled long edge clamped and aligned for DA3 inference."""
|
||||
width, height = image.size
|
||||
|
||||
@@ -19,6 +19,7 @@ from .common import (
|
||||
from .image import (
|
||||
draw_process_resolution,
|
||||
is_image_empty,
|
||||
material_data_name,
|
||||
normal_data_name,
|
||||
plane_bounds,
|
||||
prepare_image_input,
|
||||
@@ -464,7 +465,7 @@ def build_image_mesh(
|
||||
else None
|
||||
)
|
||||
material = create_material(
|
||||
source_name,
|
||||
material_data_name(source_object),
|
||||
color_image,
|
||||
depth_scale,
|
||||
shadeless=shadeless,
|
||||
|
||||
@@ -172,7 +172,77 @@ def surface_model_enum_items(_owner, _context):
|
||||
return _model_items(models)
|
||||
|
||||
|
||||
BALLOON_MODE_ITEMS = (
|
||||
(
|
||||
"POISSON",
|
||||
"Poisson",
|
||||
"Use a global Poisson field for smooth inflation",
|
||||
),
|
||||
(
|
||||
"TEDDY",
|
||||
"Teddy / Chordal Axis",
|
||||
"Use a Teddy-style chordal axis and local oval sections",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def balloon_mode_property():
|
||||
return bpy.props.EnumProperty(
|
||||
name="Mode",
|
||||
description="Algorithm used to inflate the saved lasso region",
|
||||
items=BALLOON_MODE_ITEMS,
|
||||
default="POISSON",
|
||||
)
|
||||
|
||||
|
||||
def balloon_thickness_property():
|
||||
return bpy.props.FloatProperty(
|
||||
name="Thickness",
|
||||
description="Scale the Balloon thickness while keeping the lasso region",
|
||||
default=1.0,
|
||||
min=0.05,
|
||||
soft_max=5.0,
|
||||
)
|
||||
|
||||
|
||||
def balloon_resolution_property():
|
||||
return bpy.props.FloatProperty(
|
||||
name="Resolution (m)",
|
||||
description=(
|
||||
"Distance in meters between sampled mesh points; smaller values "
|
||||
"create more geometry"
|
||||
),
|
||||
default=0.1,
|
||||
min=0.005,
|
||||
soft_min=0.02,
|
||||
soft_max=0.5,
|
||||
max=10.0,
|
||||
)
|
||||
|
||||
|
||||
def balloon_reuse_material_property():
|
||||
return bpy.props.BoolProperty(
|
||||
name="Reuse Material",
|
||||
description="Reuse a compatible Balloon material made from the same image",
|
||||
default=True,
|
||||
)
|
||||
|
||||
|
||||
def balloon_radial_zoom_property(scale):
|
||||
return bpy.props.FloatVectorProperty(
|
||||
size=2,
|
||||
default=(scale, scale),
|
||||
min=0.01,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
|
||||
class SnapFormSettings(bpy.types.PropertyGroup):
|
||||
balloon_mode: balloon_mode_property()
|
||||
balloon_thickness: balloon_thickness_property()
|
||||
balloon_resolution: balloon_resolution_property()
|
||||
balloon_reuse_material: balloon_reuse_material_property()
|
||||
balloon_thickness_radial_zoom: balloon_radial_zoom_property(100.0)
|
||||
balloon_resolution_radial_zoom: balloon_radial_zoom_property(1000.0)
|
||||
input_path: bpy.props.StringProperty(
|
||||
name="Image or Video",
|
||||
description="A single image or video file used for reconstruction",
|
||||
|
||||
Reference in New Issue
Block a user