Blenderの辺を目的のワールド位置までスライドさせる係数計算アドオン

# 概要

Blenderの任意の位置に(UVを崩さずに)辺をスライドさせたい！という場合、手段がありません。
![Slide.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272076/b9f0e455-8bc5-7054-21a8-dd9b28901ddd.png)


Gキーを2回押すとUVの状態を崩さずに辺をスライドさせられますが、数値指定できるのは「係数」とかいう謎の値……。

![スクリーンショット 2024-12-29 051815.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272076/e2c6734f-aee0-26bf-3234-f977ad2449e8.png)

この「係数」を計算するアドオンを作りました。

# 対応

Blenderアドオンをインストールしてください。

```py:slide_coefficient_calculator.cs
bl_info = {
    "name": "Slide Coefficient Calculator",
    "blender": (2, 82, 0),  # 対応するBlenderのバージョン
    "category": "Object",
    "author": "Your Name",
    "version": (1, 0),
    "description": "Calculates slide coefficient based on selected edge positions.",
    "warning": "",
    "doc_url": "",
    "tracker_url": "",
    "support": "COMMUNITY",
    "location": "View3D > Tool Shelf",
    "warning": "",
    "category": "Object",
}

import bpy

# 選択した辺の座標を取得する関数
def get_selected_edge_coordinates(axis):
    obj = bpy.context.object

    # オブジェクトが選択されているか確認
    if obj and obj.type == 'MESH':
        # メッシュデータの更新
        obj.update_from_editmode()
        
        # 選択されたエッジを取得
        edges = [e for e in obj.data.edges if e.select]
        if edges:
            # エッジの頂点座標をワールド座標に変換
            edge_coords = []
            for edge in edges:
                v1_co = obj.matrix_world @ obj.data.vertices[edge.vertices[0]].co
                v2_co = obj.matrix_world @ obj.data.vertices[edge.vertices[1]].co
                edge_coords.append((v1_co, v2_co))

            # ワールド座標の指定軸だけを抽出
            if axis == 'X':
                return [coord.x for edge in edge_coords for coord in edge]
            elif axis == 'Y':
                return [coord.y for edge in edge_coords for coord in edge]
            elif axis == 'Z':
                return [coord.z for edge in edge_coords for coord in edge]
    return None

# オペレーターを作成
class SLIDE_OT_CalculateSlideCoefficient(bpy.types.Operator):
    bl_idname = "object.calculate_slide_coefficient"
    bl_label = "Calculate Slide Coefficient"

    def execute(self, context):
        target_edge_position = context.scene.target_edge_position
        end_edge_position = context.scene.end_edge_position
        world_destination_position = context.scene.world_destination_position

        print(f"Target Edge Position: {target_edge_position}, End Edge Position: {end_edge_position}, World Destination Position: {world_destination_position}")

        if end_edge_position == target_edge_position:
            self.report({'ERROR'}, "Target Edge and End Edge cannot have the same position.")
            return {'CANCELLED'}

        try:
            coefficient = (world_destination_position - target_edge_position) / (end_edge_position - target_edge_position)
            self.report({'INFO'}, f"Calculated Coefficient: {coefficient:.9f}")
        except ZeroDivisionError:
            self.report({'ERROR'}, "Division by zero error.")
            return {'CANCELLED'}

        return {'FINISHED'}

class SLIDE_OT_SetEdgePosition(bpy.types.Operator):
    bl_idname = "object.set_target_edge_position"
    bl_label = "Set Target Edge Position"

    def execute(self, context):
        axis = context.scene.selected_axis
        selected_edge_coords = get_selected_edge_coordinates(axis)
        if selected_edge_coords is not None and selected_edge_coords:
            context.scene.target_edge_position = selected_edge_coords[0]
        else:
            self.report({'ERROR'}, "No edge selected.")
            return {'CANCELLED'}
        return {'FINISHED'}

class SLIDE_OT_SetEndEdgePosition(bpy.types.Operator):
    bl_idname = "object.set_end_edge_position"
    bl_label = "Set End Edge Position"

    def execute(self, context):
        axis = context.scene.selected_axis
        selected_edge_coords = get_selected_edge_coordinates(axis)
        if selected_edge_coords is not None and selected_edge_coords:
            context.scene.end_edge_position = selected_edge_coords[1]
        else:
            self.report({'ERROR'}, "No edge selected.")
            return {'CANCELLED'}
        return {'FINISHED'}

class SLIDE_OT_SetWorldDestinationPosition(bpy.types.Operator):
    bl_idname = "object.set_world_destination_position"
    bl_label = "Set World Destination Position"

    def execute(self, context):
        axis = context.scene.selected_axis
        selected_edge_coords = get_selected_edge_coordinates(axis)
        if selected_edge_coords is not None and selected_edge_coords:
            context.scene.world_destination_position = selected_edge_coords[0]
        else:
            self.report({'ERROR'}, "No edge selected.")
            return {'CANCELLED'}
        return {'FINISHED'}

class SLIDE_PT_CalculateSlidePanel(bpy.types.Panel):
    bl_label = "Slide Coefficient Calculator"
    bl_idname = "OBJECT_PT_slide_coefficient"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Tool'

    def draw(self, context):
        layout = self.layout

        layout.label(text="Select Axis:")
        row = layout.row(align=True)
        row.prop(context.scene, "selected_axis", expand=True)

        row = layout.row(align=True)
        row.prop(context.scene, "target_edge_position")
        row.operator("object.set_target_edge_position", text="Set")

        row = layout.row(align=True)
        row.prop(context.scene, "end_edge_position")
        row.operator("object.set_end_edge_position", text="Set")

        row = layout.row(align=True)
        row.prop(context.scene, "world_destination_position")
        row.operator("object.set_world_destination_position", text="Set")

        layout.operator("object.calculate_slide_coefficient")

def register():
    bpy.utils.register_class(SLIDE_OT_CalculateSlideCoefficient)
    bpy.utils.register_class(SLIDE_OT_SetEdgePosition)
    bpy.utils.register_class(SLIDE_OT_SetEndEdgePosition)
    bpy.utils.register_class(SLIDE_OT_SetWorldDestinationPosition)
    bpy.utils.register_class(SLIDE_PT_CalculateSlidePanel)
    
    bpy.types.Scene.selected_axis = bpy.props.EnumProperty(
        name="Axis",
        description="Choose the axis to use for edge coordinates",
        items=[('X', 'X Axis', 'Use the X axis'),
               ('Y', 'Y Axis', 'Use the Y axis'),
               ('Z', 'Z Axis', 'Use the Z axis')],
        default='X'
    )
    bpy.types.Scene.target_edge_position = bpy.props.FloatProperty(name="Target Edge Position", default=0.0)
    bpy.types.Scene.end_edge_position = bpy.props.FloatProperty(name="End Edge Position", default=0.0)
    bpy.types.Scene.world_destination_position = bpy.props.FloatProperty(name="World Destination Position", default=0.0)

def unregister():
    bpy.utils.unregister_class(SLIDE_OT_CalculateSlideCoefficient)
    bpy.utils.unregister_class(SLIDE_OT_SetEdgePosition)
    bpy.utils.unregister_class(SLIDE_OT_SetEndEdgePosition)
    bpy.utils.unregister_class(SLIDE_OT_SetWorldDestinationPosition)
    bpy.utils.unregister_class(SLIDE_PT_CalculateSlidePanel)
    
    del bpy.types.Scene.selected_axis
    del bpy.types.Scene.target_edge_position
    del bpy.types.Scene.end_edge_position
    del bpy.types.Scene.world_destination_position

if __name__ == "__main__":
    register()

```

インストールするとツールタブに新しく「Slide Coefficient Calculator」というウィンドウが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272076/401a8d42-34af-fff4-d02b-a63941188e38.png)


今回はY軸方向にスライドさせたいので「Y軸」トグルボタンを選択します。
そうしたら3つの値を入力します。
・移動させたい辺を選択して「Set」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272076/f539d25b-2581-201a-6801-28049c5879c9.png)

・移動先のMaxの行き先の辺を選択して「Set」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272076/08d2bd8d-0406-8102-a25b-4ea73bb39f76.png)

・目的の座標の辺を選択して「Set」（普通にスライドに手入力でもいいです）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272076/8e8847f8-c837-d101-44bf-b7b5ff5e394f.png)

3つの値を入力し終わったら、「Calculate Slide Cofficient」ボタンを押します。
すると、infoログに計算結果が出力されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272076/cc2960d5-14a2-22f2-b2dd-661a406c472a.png)

この値をコピペして、スライド時の「係数」に入力します。
（EndEdgePositionとして選択した辺によっては符号が反転している場合があるので、必要に応じてマイナスをかけてください）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272076/cc9a02b4-07ce-4877-3109-145c81df7463.png)

以上です。