rapture_20261006164516.png

# ================================================================
# Blender 5.0+
# Cylinder Grid Floor Replacement
#
# Floorの代わりに、X方向・Y方向の円柱を直交させて
# 格子状の床を生成するアドオン
#
# deepdeep design:
#   生成
#    ↓
#   リアルタイム更新
#    ↓
#   完成Objectとして切離し
#    ↓
#   完成品を固定
#    ↓
#   次のObject
#    ↓
#   必要なら一体化
# ================================================================

bl_info = {
    "name": "Cylinder Grid Floor",
    "author": "zionad",
    "description": "Floor replacement using perpendicular X/Y cylinder grids",
    "version": (1, 0, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > Cylinder Grid Floor",
    "category": "3D View",
}

# ================================================================
# 1. 作成日・更新日
# ================================================================

CREATED_DATE = "2026-10-05"
UPDATED_DATE = "2026-10-05"

# ================================================================
# 2. Imports
# ================================================================

import bpy
import math

from mathutils import Vector
from bpy.props import (
    IntProperty,
    FloatProperty,
    FloatVectorProperty,
    BoolProperty,
    StringProperty,
)

# ================================================================
# 3. 共通定義
# ================================================================

# アドオンの目的から自動設計したPrefix
PREFIX_VAL = "CYL_GRID_"

MAIN_COLLECTION_NAME = "CylinderGrid_Floor"
FIXED_COLLECTION_NAME = "CylinderGrid_Fixed"

CURRENT_SET_PROPERTY = "cyl_grid_current_set"
COMPLETED_PROPERTY = "cyl_grid_completed"

UPDATE_GUARD = False

# ================================================================
# 4. 初期パラメータ
# ================================================================

# ------------------------------------------------
# X方向
# ------------------------------------------------

DEFAULT_X_COUNT = 11
DEFAULT_X_RADIUS = 0.05
DEFAULT_X_LENGTH = 10.0

DEFAULT_X_COLOR = (0.15, 0.45, 1.0, 1.0)
DEFAULT_X_ALPHA = 1.0

# ------------------------------------------------
# Y方向
# ------------------------------------------------

DEFAULT_Y_COUNT = 11
DEFAULT_Y_RADIUS = 0.05
DEFAULT_Y_LENGTH = 10.0

DEFAULT_Y_COLOR = (1.0, 0.25, 0.15, 1.0)
DEFAULT_Y_ALPHA = 1.0

# ------------------------------------------------
# 円柱生成
# ------------------------------------------------

CYLINDER_VERTICES = 32

# ================================================================
# 5. Collection Utility
# ================================================================

def get_or_create_collection(name, parent=None):
    """
    同名Collectionを重複作成しない。
    """

    collection = bpy.data.collections.get(name)

    if collection is None:
        collection = bpy.data.collections.new(name)

        if parent is None:
            bpy.context.scene.collection.children.link(collection)
        else:
            parent.children.link(collection)

    return collection

def get_main_collection():
    return get_or_create_collection(MAIN_COLLECTION_NAME)

def get_fixed_collection():
    return get_or_create_collection(FIXED_COLLECTION_NAME)

def move_object_to_collection(obj, target_collection):
    """
    Objectを指定Collectionへ移動。
    """

    for collection in list(obj.users_collection):
        collection.objects.unlink(obj)

    target_collection.objects.link(obj)

# ================================================================
# 6. Name Utility
# ================================================================

def make_name(kind, index):
    return f"{PREFIX_VAL}{kind}_{index:03d}"

# ================================================================
# 7. Material Utility
# ================================================================

def create_grid_material(name, color, alpha):
    """
    円柱専用Materialを作成。
    """

    material = bpy.data.materials.new(name=name)

    material.use_nodes = True

    nodes = material.node_tree.nodes

    bsdf = nodes.get("Principled BSDF")

    if bsdf:
        rgba = (
            color[0],
            color[1],
            color[2],
            alpha,
        )

        bsdf.inputs["Base Color"].default_value = rgba
        bsdf.inputs["Alpha"].default_value = alpha

    material.diffuse_color = (
        color[0],
        color[1],
        color[2],
        alpha,
    )

    # Blender 5系で使用可能な場合のみ設定
    if hasattr(material, "surface_render_method"):
        if alpha < 0.999:
            try:
                material.surface_render_method = 'DITHERED'
            except Exception:
                pass

    return material

def update_material(material, color, alpha):
    """
    既存Materialを更新。
    """

    if material is None:
        return

    material.diffuse_color = (
        color[0],
        color[1],
        color[2],
        alpha,
    )

    if material.use_nodes:

        bsdf = material.node_tree.nodes.get("Principled BSDF")

        if bsdf:

            bsdf.inputs["Base Color"].default_value = (
                color[0],
                color[1],
                color[2],
                alpha,
            )

            bsdf.inputs["Alpha"].default_value = alpha

    if hasattr(material, "surface_render_method"):

        if alpha < 0.999:
            try:
                material.surface_render_method = 'DITHERED'
            except Exception:
                pass

def make_independent_material(obj):
    """
    完成Objectとして切離す際、
    MaterialをObject専用としてコピーする。
    """

    if obj.type != 'MESH':
        return

    for slot in obj.material_slots:

        if slot.material:
            slot.material = slot.material.copy()

# ================================================================
# 8. Cylinder Creation
# ================================================================

def create_cylinder_object(
    name,
    radius,
    length,
    location,
    rotation,
    material,
):
    """
    円柱を1本生成する。
    """

    bpy.ops.mesh.primitive_cylinder_add(
        vertices=CYLINDER_VERTICES,
        radius=radius,
        depth=length,
        location=location,
        rotation=rotation,
    )

    obj = bpy.context.object

    obj.name = name

    obj.data.name = f"{name}_Mesh"

    if material:
        obj.data.materials.append(material)

    return obj

# ================================================================
# 9. Current Object Management
# ================================================================

def get_current_objects():
    """
    現在リアルタイム制御対象のObjectを取得。
    """

    result = []

    for obj in bpy.data.objects:

        if obj.get(CURRENT_SET_PROPERTY, False):
            result.append(obj)

    return result

def clear_current_objects():
    """
    現在の生成対象だけを削除。

    完成済みObjectは削除しない。
    """

    for obj in list(get_current_objects()):

        if obj.name in bpy.data.objects:
            bpy.data.objects.remove(obj, do_unlink=True)

def mark_current_object(obj):
    obj[CURRENT_SET_PROPERTY] = True
    obj[COMPLETED_PROPERTY] = False

def mark_completed_object(obj):
    obj[CURRENT_SET_PROPERTY] = False
    obj[COMPLETED_PROPERTY] = True

# ================================================================
# 10. Grid Generation
# ================================================================

def generate_grid(settings):
    """
    X方向円柱 + Y方向円柱による格子を生成。

    X方向円柱:
        X軸に沿って配置
        Y方向へ均等配置

    Y方向円柱:
        Y軸に沿って配置
        X方向へ均等配置
    """

    global UPDATE_GUARD

    if UPDATE_GUARD:
        return

    UPDATE_GUARD = True

    try:

        clear_current_objects()

        main_collection = get_main_collection()

        # --------------------------------------------------------
        # Material
        # --------------------------------------------------------

        x_material = create_grid_material(
            f"{PREFIX_VAL}Material_X",
            settings.x_color,
            settings.x_alpha,
        )

        y_material = create_grid_material(
            f"{PREFIX_VAL}Material_Y",
            settings.y_color,
            settings.y_alpha,
        )

        # --------------------------------------------------------
        # X方向
        # --------------------------------------------------------

        x_count = max(1, settings.x_count)

        for i in range(x_count):

            if x_count == 1:
                y = 0.0
            else:
                t = i / (x_count - 1)
                y = -settings.y_length / 2.0 + (
                    settings.y_length * t
                )

            obj = create_cylinder_object(
                name=make_name("X", i + 1),
                radius=settings.x_radius,
                length=settings.x_length,
                location=(0.0, y, 0.0),
                rotation=(0.0, math.pi / 2.0, 0.0),
                material=x_material,
            )

            move_object_to_collection(obj, main_collection)

            mark_current_object(obj)

        # --------------------------------------------------------
        # Y方向
        # --------------------------------------------------------

        y_count = max(1, settings.y_count)

        for i in range(y_count):

            if y_count == 1:
                x = 0.0
            else:
                t = i / (y_count - 1)
                x = -settings.x_length / 2.0 + (
                    settings.x_length * t
                )

            obj = create_cylinder_object(
                name=make_name("Y", i + 1),
                radius=settings.y_radius,
                length=settings.y_length,
                location=(x, 0.0, 0.0),
                rotation=(math.pi / 2.0, 0.0, 0.0),
                material=y_material,
            )

            move_object_to_collection(obj, main_collection)

            mark_current_object(obj)

    finally:

        UPDATE_GUARD = False

# ================================================================
# 11. Update Callback
# ================================================================

def update_grid(self, context):

    if UPDATE_GUARD:
        return

    try:
        generate_grid(self)
    except Exception as exc:
        print(
            f"[Cylinder Grid Floor] Update Error: {exc}"
        )

# ================================================================
# 12. Property Group
# ================================================================

class CYLGRID_PG_Settings(bpy.types.PropertyGroup):

    # ============================================================
    # X方向
    # ============================================================

    x_count: IntProperty(
        name="本数",
        description="X方向に並べる円柱の本数",
        default=DEFAULT_X_COUNT,
        min=1,
        max=500,
        update=update_grid,
    )

    x_radius: FloatProperty(
        name="太さ",
        description="X方向円柱の半径",
        default=DEFAULT_X_RADIUS,
        min=0.001,
        max=100.0,
        precision=4,
        update=update_grid,
    )

    x_length: FloatProperty(
        name="長さ",
        description="X方向円柱の長さ",
        default=DEFAULT_X_LENGTH,
        min=0.001,
        max=1000.0,
        precision=3,
        update=update_grid,
    )

    x_color: FloatVectorProperty(
        name="色",
        subtype='COLOR',
        size=4,
        default=DEFAULT_X_COLOR,
        min=0.0,
        max=1.0,
        update=update_grid,
    )

    x_alpha: FloatProperty(
        name="透明度",
        description="X方向円柱の透明度",
        default=DEFAULT_X_ALPHA,
        min=0.0,
        max=1.0,
        subtype='FACTOR',
        update=update_grid,
    )

    # ============================================================
    # Y方向
    # ============================================================

    y_count: IntProperty(
        name="本数",
        description="Y方向に並べる円柱の本数",
        default=DEFAULT_Y_COUNT,
        min=1,
        max=500,
        update=update_grid,
    )

    y_radius: FloatProperty(
        name="太さ",
        description="Y方向円柱の半径",
        default=DEFAULT_Y_RADIUS,
        min=0.001,
        max=100.0,
        precision=4,
        update=update_grid,
    )

    y_length: FloatProperty(
        name="長さ",
        description="Y方向円柱の長さ",
        default=DEFAULT_Y_LENGTH,
        min=0.001,
        max=1000.0,
        precision=3,
        update=update_grid,
    )

    y_color: FloatVectorProperty(
        name="色",
        subtype='COLOR',
        size=4,
        default=DEFAULT_Y_COLOR,
        min=0.0,
        max=1.0,
        update=update_grid,
    )

    y_alpha: FloatProperty(
        name="透明度",
        description="Y方向円柱の透明度",
        default=DEFAULT_Y_ALPHA,
        min=0.0,
        max=1.0,
        subtype='FACTOR',
        update=update_grid,
    )

# ================================================================
# 13. Operator
# ================================================================

class CYLGRID_OT_create(bpy.types.Operator):

    bl_idname = "cylgrid.create"
    bl_label = "格子を生成 / 更新"
    bl_description = "X方向・Y方向の円柱格子を生成または更新"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):

        settings = context.scene.cyl_grid_settings

        generate_grid(settings)

        self.report(
            {'INFO'},
            "円柱格子を生成しました"
        )

        return {'FINISHED'}

# ================================================================
# 14. Next Object
# ================================================================

class CYLGRID_OT_next_object(bpy.types.Operator):

    bl_idname = "cylgrid.next_object"
    bl_label = "次のObjectを作成"
    bl_description = "現在の生成物を完成品として残し、次のObjectを作成"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):

        current_objects = get_current_objects()

        if not current_objects:
            self.report(
                {'WARNING'},
                "現在の生成Objectがありません"
            )
            return {'CANCELLED'}

        # --------------------------------------------------------
        # 現在のObjectを完成品として固定
        # --------------------------------------------------------

        fixed_collection = get_fixed_collection()

        for obj in current_objects:

            make_independent_material(obj)

            mark_completed_object(obj)

            move_object_to_collection(
                obj,
                fixed_collection
            )

        # --------------------------------------------------------
        # 新しいObjectを生成
        # --------------------------------------------------------

        settings = context.scene.cyl_grid_settings

        generate_grid(settings)

        self.report(
            {'INFO'},
            "前の完成Objectを保持して、次のObjectを作成しました"
        )

        return {'FINISHED'}

# ================================================================
# 15. Detach / Complete
# ================================================================

class CYLGRID_OT_detach(bpy.types.Operator):

    bl_idname = "cylgrid.detach"
    bl_label = "完成Objectとして切離し"
    bl_description = "現在の格子を完成Objectとして固定"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):

        current_objects = get_current_objects()

        if not current_objects:

            self.report(
                {'WARNING'},
                "現在の生成Objectがありません"
            )

            return {'CANCELLED'}

        fixed_collection = get_fixed_collection()

        for obj in current_objects:

            make_independent_material(obj)

            mark_completed_object(obj)

            move_object_to_collection(
                obj,
                fixed_collection
            )

        self.report(
            {'INFO'},
            "完成Objectとして切離しました"
        )

        return {'FINISHED'}

# ================================================================
# 16. Join Completed Objects
# ================================================================

class CYLGRID_OT_join_completed(bpy.types.Operator):

    bl_idname = "cylgrid.join_completed"
    bl_label = "完成品を一体化"
    bl_description = "Cylinder Grid Floorで作成した完成Objectだけを一体化"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):

        completed = [
            obj
            for obj in bpy.data.objects
            if obj.get(COMPLETED_PROPERTY, False)
        ]

        if len(completed) < 2:

            self.report(
                {'WARNING'},
                "一体化する完成Objectが2つ以上ありません"
            )

            return {'CANCELLED'}

        # --------------------------------------------------------
        # 対象を明確に限定
        # --------------------------------------------------------

        bpy.ops.object.select_all(action='DESELECT')

        for obj in completed:
            obj.select_set(True)

        context.view_layer.objects.active = completed[0]

        # --------------------------------------------------------
        # Join
        # --------------------------------------------------------

        bpy.ops.object.join()

        joined = context.view_layer.objects.active

        joined.name = f"{PREFIX_VAL}Completed_Joined"

        joined[CURRENT_SET_PROPERTY] = False
        joined[COMPLETED_PROPERTY] = True

        self.report(
            {'INFO'},
            "Cylinder Gridの完成品だけを一体化しました"
        )

        return {'FINISHED'}

# ================================================================
# 17. Position Information
# ================================================================

class CYLGRID_OT_copy_position(bpy.types.Operator):

    bl_idname = "cylgrid.copy_position"
    bl_label = "位置情報をコピー"
    bl_description = "現在の格子の位置・サイズ情報をコピー"

    def execute(self, context):

        settings = context.scene.cyl_grid_settings

        text = (
            "Cylinder Grid Floor\n"
            f"X方向 本数: {settings.x_count}\n"
            f"X方向 太さ: {settings.x_radius}\n"
            f"X方向 長さ: {settings.x_length}\n"
            f"Y方向 本数: {settings.y_count}\n"
            f"Y方向 太さ: {settings.y_radius}\n"
            f"Y方向 長さ: {settings.y_length}\n"
            "中心位置: (0.000, 0.000, 0.000)\n"
            "格子面: XY\n"
            "高さ Z: 0.000\n"
        )

        context.window_manager.clipboard = text

        self.report(
            {'INFO'},
            "位置・サイズ情報をコピーしました"
        )

        return {'FINISHED'}

# ================================================================
# 18. Reset
# ================================================================

class CYLGRID_OT_reset(bpy.types.Operator):

    bl_idname = "cylgrid.reset"
    bl_label = "設定を初期値に戻す"
    bl_description = "現在の設定値を初期値に戻す"

    def execute(self, context):

        settings = context.scene.cyl_grid_settings

        settings.x_count = DEFAULT_X_COUNT
        settings.x_radius = DEFAULT_X_RADIUS
        settings.x_length = DEFAULT_X_LENGTH
        settings.x_color = DEFAULT_X_COLOR
        settings.x_alpha = DEFAULT_X_ALPHA

        settings.y_count = DEFAULT_Y_COUNT
        settings.y_radius = DEFAULT_Y_RADIUS
        settings.y_length = DEFAULT_Y_LENGTH
        settings.y_color = DEFAULT_Y_COLOR
        settings.y_alpha = DEFAULT_Y_ALPHA

        generate_grid(settings)

        self.report(
            {'INFO'},
            "設定を初期値に戻しました"
        )

        return {'FINISHED'}

# ================================================================
# 19. Links Panel
#    deepdeep rule:
#    Panelsの最後から2番目
# ================================================================

class CYLGRID_PT_links(bpy.types.Panel):

    bl_label = "Links"
    bl_idname = "CYLGRID_PT_links"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Cylinder Grid Floor"
    bl_order = 998

    def draw(self, context):

        layout = self.layout

        layout.label(
            text="Cylinder Grid Floor"
        )

        # --------------------------------------------------------
        # Links
        #
        # ここは参照アドオンのLinks Panelを
        # 後からそのまま移植する位置。
        # --------------------------------------------------------

        layout.label(
            text="Documentation"
        )

        layout.label(
            text=f"Created: {CREATED_DATE}"
        )

        layout.label(
            text=f"Updated: {UPDATED_DATE}"
        )

# ================================================================
# 20. Addon Remove Panel
#    deepdeep rule:
#    Panelsの最後
# ================================================================

class CYLGRID_OT_remove_addon(bpy.types.Operator):

    bl_idname = "cylgrid.remove_addon"
    bl_label = "アドオン削除"
    bl_description = "Cylinder Grid Floorアドオンを無効化"

    def execute(self, context):

        # --------------------------------------------------------
        # deepdeep / blender5 standard
        # --------------------------------------------------------

        unregister()

        self.report(
            {'INFO'},
            "Cylinder Grid Floorを削除しました"
        )

        return {'FINISHED'}

class CYLGRID_PT_remove(bpy.types.Panel):

    bl_label = "アドオン削除"
    bl_idname = "CYLGRID_PT_remove"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Cylinder Grid Floor"
    bl_order = 999

    def draw(self, context):

        layout = self.layout

        layout.label(
            text="Cylinder Grid Floor"
        )

        layout.operator(
            "cylgrid.remove_addon",
            icon='TRASH'
        )

# ================================================================
# 21. Main Panel
# ================================================================

class CYLGRID_PT_main(bpy.types.Panel):

    bl_label = "Floor代替・円柱格子"
    bl_idname = "CYLGRID_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Cylinder Grid Floor"

    def draw(self, context):

        layout = self.layout

        settings = context.scene.cyl_grid_settings

        # ========================================================
        # X方向
        # ========================================================

        box = layout.box()

        box.label(
            text="X軸方向の円柱",
            icon='AXIS_X'
        )

        box.prop(
            settings,
            "x_count"
        )

        box.prop(
            settings,
            "x_radius"
        )

        box.prop(
            settings,
            "x_length"
        )

        box.prop(
            settings,
            "x_color"
        )

        box.prop(
            settings,
            "x_alpha"
        )

        # ========================================================
        # Y方向
        # ========================================================

        box = layout.box()

        box.label(
            text="Y軸方向の円柱",
            icon='AXIS_Y'
        )

        box.prop(
            settings,
            "y_count"
        )

        box.prop(
            settings,
            "y_radius"
        )

        box.prop(
            settings,
            "y_length"
        )

        box.prop(
            settings,
            "y_color"
        )

        box.prop(
            settings,
            "y_alpha"
        )

        # ========================================================
        # Generate
        # ========================================================

        box = layout.box()

        box.label(
            text="生成"
        )

        box.operator(
            "cylgrid.create",
            icon='MESH_GRID'
        )

        box.operator(
            "cylgrid.reset",
            icon='FILE_REFRESH'
        )

        # ========================================================
        # Completed Object
        # ========================================================

        box = layout.box()

        box.label(
            text="完成Object管理"
        )

        box.operator(
            "cylgrid.detach",
            icon='CHECKMARK'
        )

        box.operator(
            "cylgrid.next_object",
            icon='DUPLICATE'
        )

        box.separator()

        box.label(
            text="完成品を一体化する場合のみ実行"
        )

        box.operator(
            "cylgrid.join_completed",
            icon='AUTOMERGE'
        )

        # ========================================================
        # Position
        # ========================================================

        box = layout.box()

        box.label(
            text="位置情報"
        )

        box.label(
            text="中心: X 0 / Y 0 / Z 0"
        )

        box.label(
            text=f"X長さ: {settings.x_length}"
        )

        box.label(
            text=f"Y長さ: {settings.y_length}"
        )

        box.operator(
            "cylgrid.copy_position",
            icon='COPYDOWN'
        )

# ================================================================
# 22. Classes
# ================================================================

classes = (
    CYLGRID_PG_Settings,

    CYLGRID_OT_create,
    CYLGRID_OT_next_object,
    CYLGRID_OT_detach,
    CYLGRID_OT_join_completed,
    CYLGRID_OT_copy_position,
    CYLGRID_OT_reset,

    CYLGRID_OT_remove_addon,

    CYLGRID_PT_main,
    CYLGRID_PT_links,
    CYLGRID_PT_remove,
)

# ================================================================
# 23. Register
# ================================================================

def register():

    for cls in classes:
        bpy.utils.register_class(cls)

    bpy.types.Scene.cyl_grid_settings = bpy.props.PointerProperty(
        type=CYLGRID_PG_Settings
    )

    # ------------------------------------------------------------
    # 初回生成
    # ------------------------------------------------------------

    try:

        settings = bpy.context.scene.cyl_grid_settings

        if not get_current_objects():
            generate_grid(settings)

    except Exception as exc:

        print(
            f"[Cylinder Grid Floor] Initial generation error: {exc}"
        )

# ================================================================
# 24. Unregister
# ================================================================

def unregister():

    global UPDATE_GUARD

    UPDATE_GUARD = True

    try:

        # --------------------------------------------------------
        # Scene Property
        # --------------------------------------------------------

        if hasattr(
            bpy.types.Scene,
            "cyl_grid_settings"
        ):
            del bpy.types.Scene.cyl_grid_settings

        # --------------------------------------------------------
        # Classes
        # --------------------------------------------------------

        for cls in reversed(classes):

            try:
                bpy.utils.unregister_class(cls)
            except RuntimeError:
                pass

    finally:

        UPDATE_GUARD = False

# ================================================================
# 25. Direct execution
# ================================================================

if __name__ == "__main__":
    register()