# ============================================================
# Blender 5.0+ Cylinder Grid Floor
# ============================================================
# X/Y方向の円柱グリッド床を生成するアドオン
#
# 主な特徴
# - Blender 5.0+ 専用
# - X方向 / Y方向の円柱本数・太さ・間隔・長さを独立設定
# - X方向 / Y方向の表面色・裏面色・透明度を独立設定
# - 天井蓋 / 床面蓋を独立Objectとして生成
# - 天井蓋 / 床面蓋はそれぞれ独立した削除チェック
# - 円柱削除ボタンは設けない
# - リアルタイム更新
# - 「完成Objectとして切離し」
# - 「次のObjectを作成」
# - 完成Objectだけを対象にしたJoin確認ダイアログ
# - 位置情報表示 / コピー
# - 完成ObjectのMaterial完全独立化
# - Linksは最後から2番目、Addon Removeは最後
# ============================================================

bl_info = {
    "name": "Cylinder Grid Floor",
    "author": "zionad / ChatGPT",
    "version": (1, 0, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > Cylinder Grid",
    "description": "X/Y方向の独立円柱グリッド床を生成するBlender 5.0+専用アドオン",
    "category": "Add Mesh",
}

# ============================================================
# 作成・更新情報
# ============================================================

CREATED_DATE = "2026-10-06"
UPDATED_DATE = "2026-10-06"

# ============================================================
# imports
# ============================================================

import bpy
import bmesh
import math
from mathutils import Vector

# ============================================================
# 共通定義
# ============================================================

PREFIX_VAL = "CYL_GRID_FLOOR_"

MAIN_COLLECTION_NAME = f"{PREFIX_VAL}CURRENT"
FIXED_COLLECTION_NAME = f"{PREFIX_VAL}COMPLETED"

CURRENT_WALL_PREFIX = f"{PREFIX_VAL}WALL_"
CURRENT_TOP_PREFIX = f"{PREFIX_VAL}TOP_CAP_"
CURRENT_BOTTOM_PREFIX = f"{PREFIX_VAL}BOTTOM_CAP_"
FIXED_PREFIX = f"{PREFIX_VAL}FIXED_"

PANEL_CATEGORY = "Cylinder Grid"

X_AXIS = "X"
Y_AXIS = "Y"

# ============================================================
# 初期値
# ============================================================

DEFAULT_X_COUNT = 5
DEFAULT_Y_COUNT = 5

DEFAULT_X_RADIUS = 0.10
DEFAULT_Y_RADIUS = 0.10

DEFAULT_X_SPACING = 1.0
DEFAULT_Y_SPACING = 1.0

DEFAULT_X_LENGTH = 5.0
DEFAULT_Y_LENGTH = 5.0

DEFAULT_X_COLOR = (0.08, 0.32, 1.0, 1.0)
DEFAULT_X_ALPHA = 1.0
DEFAULT_X_BACK_COLOR = (0.02, 0.10, 0.35, 1.0)
DEFAULT_X_BACK_ALPHA = 1.0

DEFAULT_Y_COLOR = (0.10, 0.80, 0.35, 1.0)
DEFAULT_Y_ALPHA = 1.0
DEFAULT_Y_BACK_COLOR = (0.02, 0.30, 0.12, 1.0)
DEFAULT_Y_BACK_ALPHA = 1.0

DEFAULT_TOP_COLOR = (0.15, 0.65, 1.0, 1.0)
DEFAULT_TOP_ALPHA = 0.35

DEFAULT_BOTTOM_COLOR = (0.05, 0.45, 0.20, 1.0)
DEFAULT_BOTTOM_ALPHA = 0.35

DEFAULT_SEGMENTS = 32

# 天井 / 床面蓋
DEFAULT_CREATE_TOP_CAP = True
DEFAULT_CREATE_BOTTOM_CAP = True

# 生成中心
DEFAULT_CENTER_X = 0.0
DEFAULT_CENTER_Y = 0.0
DEFAULT_CENTER_Z = 0.0

# ============================================================
# Utility
# ============================================================

def ensure_collection(name, parent=None):
    collection = bpy.data.collections.get(name)

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

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

    return collection

def ensure_main_collections():
    current = ensure_collection(MAIN_COLLECTION_NAME)
    fixed = ensure_collection(FIXED_COLLECTION_NAME)
    return current, fixed

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

def link_to_collection(obj, collection):
    if obj.name not in collection.objects:
        collection.objects.link(obj)

def safe_remove_object(obj):
    if obj is not None and obj.name in bpy.data.objects:
        bpy.data.objects.remove(obj, do_unlink=True)

def unique_object_name(base_name):
    if bpy.data.objects.get(base_name) is None:
        return base_name

    index = 1
    while bpy.data.objects.get(f"{base_name}_{index:03d}") is not None:
        index += 1

    return f"{base_name}_{index:03d}"

def make_material_name(kind, session_id, axis=None):
    if axis:
        return f"{PREFIX_VAL}MAT_{kind}_{axis}_{session_id}"
    return f"{PREFIX_VAL}MAT_{kind}_{session_id}"

def set_material_blend(mat):
    # Blender 5系での変更に備えた安全な設定。
    # 利用可能なAPIだけを使用する。
    try:
        if hasattr(mat, "surface_render_method"):
            mat.surface_render_method = 'DITHERED'
    except Exception:
        pass

    try:
        mat.use_transparency_overlap = False
    except Exception:
        pass

def create_two_sided_material(
    name,
    front_color,
    front_alpha,
    back_color,
    back_alpha,
):
    mat = bpy.data.materials.get(name)

    if mat is None:
        mat = bpy.data.materials.new(name=name)

    mat.use_nodes = True
    set_material_blend(mat)

    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    nodes.clear()

    output = nodes.new("ShaderNodeOutputMaterial")
    output.location = (500, 0)

    bsdf = nodes.new("ShaderNodeBsdfPrincipled")
    bsdf.location = (220, 0)

    geometry = nodes.new("ShaderNodeNewGeometry")
    geometry.location = (-500, -180)

    color_mix = nodes.new("ShaderNodeMixRGB")
    color_mix.blend_type = 'MIX'
    color_mix.location = (-180, 100)

    alpha_mix = nodes.new("ShaderNodeMath")
    alpha_mix.operation = 'MULTIPLY'
    alpha_mix.location = (-180, -100)

    color_mix.inputs[1].default_value = tuple(front_color[:4])
    color_mix.inputs[2].default_value = tuple(back_color[:4])

    # Front/Back切替
    links.new(geometry.outputs["Backfacing"], color_mix.inputs[0])

    # AlphaもBackfacingに応じて独立切替
    alpha_mix2 = nodes.new("ShaderNodeMix")
    alpha_mix2.data_type = 'FLOAT'
    alpha_mix2.location = (-20, -100)
    alpha_mix2.inputs[2].default_value = float(front_alpha)
    alpha_mix2.inputs[3].default_value = float(back_alpha)

    links.new(geometry.outputs["Backfacing"], alpha_mix2.inputs[0])

    links.new(color_mix.outputs["Color"], bsdf.inputs["Base Color"])
    links.new(alpha_mix2.outputs[0], bsdf.inputs["Alpha"])
    links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])

    return mat

def create_simple_material(name, color, alpha):
    mat = bpy.data.materials.get(name)

    if mat is None:
        mat = bpy.data.materials.new(name=name)

    mat.use_nodes = True
    set_material_blend(mat)

    nodes = mat.node_tree.nodes
    links = mat.node_tree.links

    nodes.clear()

    output = nodes.new("ShaderNodeOutputMaterial")
    output.location = (300, 0)

    bsdf = nodes.new("ShaderNodeBsdfPrincipled")
    bsdf.location = (0, 0)

    bsdf.inputs["Base Color"].default_value = tuple(color[:4])
    bsdf.inputs["Alpha"].default_value = max(0.0, min(1.0, float(alpha)))

    links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])

    return mat

def clear_material_slots(obj):
    obj.data.materials.clear()

def assign_single_material(obj, mat):
    clear_material_slots(obj)
    obj.data.materials.append(mat)

def make_material_independent(obj):
    if obj is None or obj.data is None:
        return

    new_materials = []

    for mat in obj.data.materials:
        if mat is None:
            new_materials.append(None)
            continue

        copied = mat.copy()
        copied.name = unique_material_name(copied.name)
        new_materials.append(copied)

    obj.data.materials.clear()

    for mat in new_materials:
        if mat is not None:
            obj.data.materials.append(mat)

def unique_material_name(base_name):
    if bpy.data.materials.get(base_name) is None:
        return base_name

    index = 1
    while bpy.data.materials.get(f"{base_name}_{index:03d}") is not None:
        index += 1

    return f"{base_name}_{index:03d}"

def create_cylinder_mesh(
    radius,
    length,
    axis,
    segments,
    center,
):
    """
    bpy.ops.mesh.primitive_cylinder_add を使わず、
    bmeshで円柱メッシュを生成する。
    """

    mesh = bpy.data.meshes.new(
        unique_mesh_name(f"{PREFIX_VAL}MESH_{axis}")
    )

    bm = bmesh.new()

    # 円柱はZ軸で作成し、最後に回転。
    bmesh.ops.create_cone(
        bm,
        cap_ends=True,
        segments=max(8, int(segments)),
        radius1=max(0.0001, float(radius)),
        radius2=max(0.0001, float(radius)),
        depth=max(0.0001, float(length)),
    )

    bm.to_mesh(mesh)
    bm.free()

    obj = bpy.data.objects.new(
        unique_object_name(f"{PREFIX_VAL}TEMP"),
        mesh,
    )

    obj.location = center

    if axis == X_AXIS:
        obj.rotation_euler[1] = math.radians(90.0)
    elif axis == Y_AXIS:
        obj.rotation_euler[0] = math.radians(90.0)

    return obj

def unique_mesh_name(base_name):
    if bpy.data.meshes.get(base_name) is None:
        return base_name

    index = 1
    while bpy.data.meshes.get(f"{base_name}_{index:03d}") is not None:
        index += 1

    return f"{base_name}_{index:03d}"

def create_disk_mesh(radius, axis, center, segments):
    """
    円柱端部用の独立蓋。
    蓋は別Objectとして生成する。
    """

    mesh = bpy.data.meshes.new(
        unique_mesh_name(f"{PREFIX_VAL}CAP_MESH_{axis}")
    )

    bm = bmesh.new()

    bmesh.ops.create_circle(
        bm,
        cap_ends=True,
        segments=max(8, int(segments)),
        radius=max(0.0001, float(radius)),
    )

    bm.to_mesh(mesh)
    bm.free()

    obj = bpy.data.objects.new(
        unique_object_name(f"{PREFIX_VAL}CAP_TEMP"),
        mesh,
    )

    obj.location = center

    # create_circleはXY平面。
    if axis == X_AXIS:
        obj.rotation_euler[1] = math.radians(90.0)
    elif axis == Y_AXIS:
        obj.rotation_euler[0] = math.radians(90.0)

    return obj

def set_object_metadata(obj, session_id, role, axis=None):
    obj["cyl_grid_floor_generated"] = True
    obj["cyl_grid_floor_session"] = int(session_id)
    obj["cyl_grid_floor_role"] = role

    if axis:
        obj["cyl_grid_floor_axis"] = axis

    obj["cyl_grid_floor_completed"] = False

def get_current_objects():
    current, _fixed = ensure_main_collections()

    return [
        obj
        for obj in current.objects
        if obj.get("cyl_grid_floor_generated", False)
        and not obj.get("cyl_grid_floor_completed", False)
    ]

def get_completed_objects():
    _current, fixed = ensure_main_collections()

    return [
        obj
        for obj in fixed.objects
        if obj.get("cyl_grid_floor_generated", False)
        and obj.get("cyl_grid_floor_completed", False)
    ]

def delete_current_objects():
    for obj in list(get_current_objects()):
        safe_remove_object(obj)

def assign_material_to_new_object(obj, mat):
    if obj is None:
        return

    assign_single_material(obj, mat)

def get_or_replace_mesh_object(
    name,
    mesh_obj,
    collection,
    session_id,
    role,
    axis=None,
    material=None,
):
    old = bpy.data.objects.get(name)

    if old is not None:
        safe_remove_object(old)

    mesh_obj.name = unique_object_name(name)
    mesh_obj.data.name = unique_mesh_name(f"{name}_MESH")

    unlink_from_all_collections(mesh_obj)
    collection.objects.link(mesh_obj)

    set_object_metadata(
        mesh_obj,
        session_id,
        role,
        axis,
    )

    if material:
        assign_material_to_new_object(mesh_obj, material)

    return mesh_obj

def build_wall(
    props,
    session_id,
    axis,
    index,
    x,
    y,
    z,
    collection,
):
    if axis == X_AXIS:
        radius = props.x_radius
        length = props.x_length
        color = props.x_color
        alpha = props.x_alpha
        back_color = props.x_back_color
        back_alpha = props.x_back_alpha
    else:
        radius = props.y_radius
        length = props.y_length
        color = props.y_color
        alpha = props.y_alpha
        back_color = props.y_back_color
        back_alpha = props.y_back_alpha

    center = Vector((x, y, z))

    mesh_obj = create_cylinder_mesh(
        radius=radius,
        length=length,
        axis=axis,
        segments=props.segments,
        center=center,
    )

    name = f"{CURRENT_WALL_PREFIX}{axis}_{session_id:04d}_{index:03d}"

    material_name = make_material_name(
        "WALL",
        session_id,
        axis,
    )

    mat = create_two_sided_material(
        material_name,
        color,
        alpha,
        back_color,
        back_alpha,
    )

    return get_or_replace_mesh_object(
        name,
        mesh_obj,
        collection,
        session_id,
        "WALL",
        axis,
        mat,
    )

def build_cap(
    props,
    session_id,
    axis,
    index,
    x,
    y,
    z,
    collection,
    top=True,
):
    radius = props.x_radius if axis == X_AXIS else props.y_radius
    length = props.x_length if axis == X_AXIS else props.y_length

    half_length = length * 0.5

    if top:
        z_offset = half_length
        role = "TOP_CAP"
        color = props.top_color
        alpha = props.top_alpha
        prefix = CURRENT_TOP_PREFIX
    else:
        z_offset = -half_length
        role = "BOTTOM_CAP"
        color = props.bottom_color
        alpha = props.bottom_alpha
        prefix = CURRENT_BOTTOM_PREFIX

    # 円柱軸方向へオフセット
    if axis == X_AXIS:
        cap_center = Vector((x + z_offset, y, z))
    else:
        cap_center = Vector((x, y + z_offset, z))

    mesh_obj = create_disk_mesh(
        radius=radius,
        axis=axis,
        center=cap_center,
        segments=props.segments,
    )

    name = f"{prefix}{axis}_{session_id:04d}_{index:03d}"

    material_name = make_material_name(
        "TOP" if top else "BOTTOM",
        session_id,
        axis,
    )

    mat = create_simple_material(
        material_name,
        color,
        alpha,
    )

    return get_or_replace_mesh_object(
        name,
        mesh_obj,
        collection,
        session_id,
        role,
        axis,
        mat,
    )

def rebuild_scene(props):
    """
    現在の生成対象だけを再構築する。
    完成Objectは絶対に触らない。
    """

    delete_current_objects()

    current, _fixed = ensure_main_collections()

    session_id = int(props.session_id)

    # --------------------------------------------------------
    # X方向円柱
    # X軸に沿って配置し、Y方向に等間隔で並べる
    # --------------------------------------------------------

    x_count = max(1, int(props.x_count))

    for i in range(x_count):
        y = (
            props.center_y
            + (i - (x_count - 1) * 0.5) * props.x_spacing
        )

        build_wall(
            props,
            session_id,
            X_AXIS,
            i,
            props.center_x,
            y,
            props.center_z,
            current,
        )

        if props.create_top_cap:
            build_cap(
                props,
                session_id,
                X_AXIS,
                i,
                props.center_x,
                y,
                props.center_z,
                current,
                top=True,
            )

        if props.create_bottom_cap:
            build_cap(
                props,
                session_id,
                X_AXIS,
                i,
                props.center_x,
                y,
                props.center_z,
                current,
                top=False,
            )

    # --------------------------------------------------------
    # Y方向円柱
    # Y軸に沿って配置し、X方向に等間隔で並べる
    # --------------------------------------------------------

    y_count = max(1, int(props.y_count))

    for i in range(y_count):
        x = (
            props.center_x
            + (i - (y_count - 1) * 0.5) * props.y_spacing
        )

        build_wall(
            props,
            session_id,
            Y_AXIS,
            i,
            x,
            props.center_y,
            props.center_z,
            current,
        )

        if props.create_top_cap:
            build_cap(
                props,
                session_id,
                Y_AXIS,
                i,
                x,
                props.center_y,
                props.center_z,
                current,
                top=True,
            )

        if props.create_bottom_cap:
            build_cap(
                props,
                session_id,
                Y_AXIS,
                i,
                x,
                props.center_y,
                props.center_z,
                current,
                top=False,
            )

def schedule_rebuild(self, context):
    if getattr(self, "_updating", False):
        return

    self._updating = True

    try:
        if not self.detached:
            rebuild_scene(self)
    finally:
        self._updating = False

def update_property(self, context):
    schedule_rebuild(self, context)

# ============================================================
# PropertyGroup
# ============================================================

class CYLGRID_PG_settings(bpy.types.PropertyGroup):

    # --------------------------------------------------------
    # セッション
    # --------------------------------------------------------

    session_id: bpy.props.IntProperty(
        name="Object番号",
        default=1,
        min=1,
    )

    detached: bpy.props.BoolProperty(
        name="切離し済み",
        default=False,
    )

    segments: bpy.props.IntProperty(
        name="分割数",
        default=DEFAULT_SEGMENTS,
        min=8,
        max=256,
        update=update_property,
    )

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

    x_count: bpy.props.IntProperty(
        name="本数",
        default=DEFAULT_X_COUNT,
        min=1,
        max=200,
        update=update_property,
    )

    x_radius: bpy.props.FloatProperty(
        name="太さ",
        default=DEFAULT_X_RADIUS,
        min=0.001,
        soft_max=10.0,
        precision=4,
        update=update_property,
    )

    x_spacing: bpy.props.FloatProperty(
        name="間隔",
        default=DEFAULT_X_SPACING,
        min=0.001,
        soft_max=100.0,
        precision=4,
        update=update_property,
    )

    x_length: bpy.props.FloatProperty(
        name="長さ",
        default=DEFAULT_X_LENGTH,
        min=0.001,
        soft_max=1000.0,
        precision=4,
        update=update_property,
    )

    x_color: bpy.props.FloatVectorProperty(
        name="表",
        subtype='COLOR',
        size=4,
        default=DEFAULT_X_COLOR,
        min=0.0,
        max=1.0,
        update=update_property,
    )

    x_alpha: bpy.props.FloatProperty(
        name="表透明度",
        default=DEFAULT_X_ALPHA,
        min=0.0,
        max=1.0,
        subtype='FACTOR',
        update=update_property,
    )

    x_back_color: bpy.props.FloatVectorProperty(
        name="裏",
        subtype='COLOR',
        size=4,
        default=DEFAULT_X_BACK_COLOR,
        min=0.0,
        max=1.0,
        update=update_property,
    )

    x_back_alpha: bpy.props.FloatProperty(
        name="裏透明度",
        default=DEFAULT_X_BACK_ALPHA,
        min=0.0,
        max=1.0,
        subtype='FACTOR',
        update=update_property,
    )

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

    y_count: bpy.props.IntProperty(
        name="本数",
        default=DEFAULT_Y_COUNT,
        min=1,
        max=200,
        update=update_property,
    )

    y_radius: bpy.props.FloatProperty(
        name="太さ",
        default=DEFAULT_Y_RADIUS,
        min=0.001,
        soft_max=10.0,
        precision=4,
        update=update_property,
    )

    y_spacing: bpy.props.FloatProperty(
        name="間隔",
        default=DEFAULT_Y_SPACING,
        min=0.001,
        soft_max=100.0,
        precision=4,
        update=update_property,
    )

    y_length: bpy.props.FloatProperty(
        name="長さ",
        default=DEFAULT_Y_LENGTH,
        min=0.001,
        soft_max=1000.0,
        precision=4,
        update=update_property,
    )

    y_color: bpy.props.FloatVectorProperty(
        name="表",
        subtype='COLOR',
        size=4,
        default=DEFAULT_Y_COLOR,
        min=0.0,
        max=1.0,
        update=update_property,
    )

    y_alpha: bpy.props.FloatProperty(
        name="表透明度",
        default=DEFAULT_Y_ALPHA,
        min=0.0,
        max=1.0,
        subtype='FACTOR',
        update=update_property,
    )

    y_back_color: bpy.props.FloatVectorProperty(
        name="裏",
        subtype='COLOR',
        size=4,
        default=DEFAULT_Y_BACK_COLOR,
        min=0.0,
        max=1.0,
        update=update_property,
    )

    y_back_alpha: bpy.props.FloatProperty(
        name="裏透明度",
        default=DEFAULT_Y_BACK_ALPHA,
        min=0.0,
        max=1.0,
        subtype='FACTOR',
        update=update_property,
    )

    # --------------------------------------------------------
    # 天井 / 床面蓋
    # --------------------------------------------------------

    create_top_cap: bpy.props.BoolProperty(
        name="天井蓋を作る",
        default=DEFAULT_CREATE_TOP_CAP,
        update=update_property,
    )

    top_color: bpy.props.FloatVectorProperty(
        name="天井蓋色",
        subtype='COLOR',
        size=4,
        default=DEFAULT_TOP_COLOR,
        min=0.0,
        max=1.0,
        update=update_property,
    )

    top_alpha: bpy.props.FloatProperty(
        name="天井蓋透明度",
        default=DEFAULT_TOP_ALPHA,
        min=0.0,
        max=1.0,
        subtype='FACTOR',
        update=update_property,
    )

    create_bottom_cap: bpy.props.BoolProperty(
        name="床面蓋を作る",
        default=DEFAULT_CREATE_BOTTOM_CAP,
        update=update_property,
    )

    bottom_color: bpy.props.FloatVectorProperty(
        name="床面蓋色",
        subtype='COLOR',
        size=4,
        default=DEFAULT_BOTTOM_COLOR,
        min=0.0,
        max=1.0,
        update=update_property,
    )

    bottom_alpha: bpy.props.FloatProperty(
        name="床面蓋透明度",
        default=DEFAULT_BOTTOM_ALPHA,
        min=0.0,
        max=1.0,
        subtype='FACTOR',
        update=update_property,
    )

    # --------------------------------------------------------
    # 中心位置
    # --------------------------------------------------------

    center_x: bpy.props.FloatProperty(
        name="X",
        default=DEFAULT_CENTER_X,
        precision=4,
        update=update_property,
    )

    center_y: bpy.props.FloatProperty(
        name="Y",
        default=DEFAULT_CENTER_Y,
        precision=4,
        update=update_property,
    )

    center_z: bpy.props.FloatProperty(
        name="Z",
        default=DEFAULT_CENTER_Z,
        precision=4,
        update=update_property,
    )

# ============================================================
# Operators
# ============================================================

class CYLGRID_OT_create(bpy.types.Operator):
    bl_idname = "cyl_grid.create"
    bl_label = "作成 / 更新"
    bl_description = "現在の設定で円柱グリッドを作成・更新します"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        props = context.scene.cyl_grid_settings

        if props.detached:
            self.report(
                {'WARNING'},
                "現在のObjectは切離し済みです。「次のObjectを作成」を使用してください。",
            )
            return {'CANCELLED'}

        rebuild_scene(props)

        self.report({'INFO'}, "円柱グリッドを作成 / 更新しました。")
        return {'FINISHED'}

class CYLGRID_OT_detach(bpy.types.Operator):
    bl_idname = "cyl_grid.detach"
    bl_label = "完成Objectとして切離し"
    bl_description = "現在の生成物を完成Objectとして切離し、以後の更新対象から外します"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        props = context.scene.cyl_grid_settings

        current, fixed = ensure_main_collections()
        current_objects = get_current_objects()

        if not current_objects:
            self.report({'WARNING'}, "切離す対象の生成Objectがありません。")
            return {'CANCELLED'}

        # Materialを完全独立化
        for obj in current_objects:
            make_material_independent(obj)

            obj["cyl_grid_floor_completed"] = True
            obj["cyl_grid_floor_fixed"] = True

            unlink_from_all_collections(obj)
            fixed.objects.link(obj)

        props.detached = True

        self.report(
            {'INFO'},
            f"{len(current_objects)}個のObjectを完成Objectとして切離しました。",
        )

        return {'FINISHED'}

class CYLGRID_OT_next_object(bpy.types.Operator):
    bl_idname = "cyl_grid.next_object"
    bl_label = "次のObjectを作成"
    bl_description = "完成済みObjectを保持したまま、新しい生成Objectを作成します"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        props = context.scene.cyl_grid_settings

        # 旧完成品には触れない
        props.session_id += 1
        props.detached = False

        rebuild_scene(props)

        self.report(
            {'INFO'},
            f"Object {props.session_id} の新規生成を開始しました。",
        )

        return {'FINISHED'}

class CYLGRID_OT_confirm_join(bpy.types.Operator):
    bl_idname = "cyl_grid.confirm_join"
    bl_label = "完成品を一体化するか確認"
    bl_description = "完成Objectだけを対象として、一体化するか確認します"

    def invoke(self, context, event):
        completed = get_completed_objects()

        if len(completed) <= 1:
            self.report(
                {'INFO'},
                "完成Objectが1個以下のため、一体化する必要はありません。",
            )
            return {'CANCELLED'}

        return context.window_manager.invoke_props_dialog(self)

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

        layout.label(
            text=f"完成Object {len(completed)}個を一体化しますか?",
            icon='QUESTION',
        )
        layout.separator()
        layout.label(text="対象は完成Objectのみです。")
        layout.label(text="未完成Objectは対象外です。")
        layout.separator()
        layout.label(text="「一体化する」でJoinを実行します。")

    def execute(self, context):
        return bpy.ops.cyl_grid.join_completed('INVOKE_DEFAULT')

class CYLGRID_OT_join_completed(bpy.types.Operator):
    bl_idname = "cyl_grid.join_completed"
    bl_label = "一体化する"
    bl_description = "完成ObjectだけをJoinします"
    bl_options = {'REGISTER', 'UNDO'}

    def invoke(self, context, event):
        completed = get_completed_objects()

        if len(completed) <= 1:
            self.report(
                {'INFO'},
                "完成Objectが1個以下のため、Joinは実行しません。",
            )
            return {'CANCELLED'}

        return context.window_manager.invoke_confirm(
            self,
            event,
        )

    def execute(self, context):
        completed = get_completed_objects()

        if len(completed) <= 1:
            self.report(
                {'INFO'},
                "完成Objectが1個以下のため、Joinは実行しません。",
            )
            return {'CANCELLED'}

        # 完成Objectだけを選択
        bpy.ops.object.select_all(action='DESELECT')

        for obj in completed:
            if obj.name in bpy.context.view_layer.objects:
                obj.select_set(True)

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

        try:
            bpy.ops.object.join()
        except Exception as exc:
            self.report(
                {'ERROR'},
                f"Joinに失敗しました: {exc}",
            )
            return {'CANCELLED'}

        active["cyl_grid_floor_completed"] = True
        active["cyl_grid_floor_joined"] = True

        self.report(
            {'INFO'},
            f"{len(completed)}個の完成Objectを一体化しました。",
        )

        return {'FINISHED'}

class CYLGRID_OT_copy_position(bpy.types.Operator):
    bl_idname = "cyl_grid.copy_position"
    bl_label = "位置情報をコピー"
    bl_description = "現在の位置・配置情報をクリップボードへコピーします"

    def execute(self, context):
        props = context.scene.cyl_grid_settings

        x_span = (
            max(0, props.x_count - 1)
            * props.x_spacing
        )

        y_span = (
            max(0, props.y_count - 1)
            * props.y_spacing
        )

        text = (
            "Cylinder Grid Floor\n"
            f"Object番号: {props.session_id}\n"
            f"中心 X: {props.center_x:.4f}\n"
            f"中心 Y: {props.center_y:.4f}\n"
            f"中心 Z: {props.center_z:.4f}\n"
            f"X方向本数: {props.x_count}\n"
            f"X方向間隔: {props.x_spacing:.4f}\n"
            f"X方向配置幅: {x_span:.4f}\n"
            f"X方向長さ: {props.x_length:.4f}\n"
            f"Y方向本数: {props.y_count}\n"
            f"Y方向間隔: {props.y_spacing:.4f}\n"
            f"Y方向配置幅: {y_span:.4f}\n"
            f"Y方向長さ: {props.y_length:.4f}\n"
            f"天井蓋: {'ON' if props.create_top_cap else 'OFF'}\n"
            f"床面蓋: {'ON' if props.create_bottom_cap else 'OFF'}\n"
        )

        context.window_manager.clipboard = text

        self.report({'INFO'}, "位置情報をクリップボードへコピーしました。")
        return {'FINISHED'}

class CYLGRID_OT_reset_settings(bpy.types.Operator):
    bl_idname = "cyl_grid.reset_settings"
    bl_label = "設定を初期値へ戻す"
    bl_description = "現在の生成設定だけを初期値へ戻します"

    def execute(self, context):
        props = context.scene.cyl_grid_settings

        props.x_count = DEFAULT_X_COUNT
        props.y_count = DEFAULT_Y_COUNT

        props.x_radius = DEFAULT_X_RADIUS
        props.y_radius = DEFAULT_Y_RADIUS

        props.x_spacing = DEFAULT_X_SPACING
        props.y_spacing = DEFAULT_Y_SPACING

        props.x_length = DEFAULT_X_LENGTH
        props.y_length = DEFAULT_Y_LENGTH

        props.x_color = DEFAULT_X_COLOR
        props.x_alpha = DEFAULT_X_ALPHA
        props.x_back_color = DEFAULT_X_BACK_COLOR
        props.x_back_alpha = DEFAULT_X_BACK_ALPHA

        props.y_color = DEFAULT_Y_COLOR
        props.y_alpha = DEFAULT_Y_ALPHA
        props.y_back_color = DEFAULT_Y_BACK_COLOR
        props.y_back_alpha = DEFAULT_Y_BACK_ALPHA

        props.create_top_cap = DEFAULT_CREATE_TOP_CAP
        props.top_color = DEFAULT_TOP_COLOR
        props.top_alpha = DEFAULT_TOP_ALPHA

        props.create_bottom_cap = DEFAULT_CREATE_BOTTOM_CAP
        props.bottom_color = DEFAULT_BOTTOM_COLOR
        props.bottom_alpha = DEFAULT_BOTTOM_ALPHA

        props.center_x = DEFAULT_CENTER_X
        props.center_y = DEFAULT_CENTER_Y
        props.center_z = DEFAULT_CENTER_Z

        props.segments = DEFAULT_SEGMENTS

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

class CYLGRID_OT_remove_addon(bpy.types.Operator):
    bl_idname = "cyl_grid.remove_addon"
    bl_label = "アドオン削除"
    bl_description = "Cylinder Grid Floorアドオンを無効化します"

    def invoke(self, context, event):
        return context.window_manager.invoke_confirm(
            self,
            event,
        )

    def execute(self, context):
        try:
            unregister()
        except Exception as exc:
            self.report({'ERROR'}, f"アドオン削除に失敗しました: {exc}")
            return {'CANCELLED'}

        return {'FINISHED'}

# ============================================================
# Panels
# ============================================================

class CYLGRID_PT_main(bpy.types.Panel):
    bl_label = "Cylinder Grid Floor"
    bl_idname = "CYLGRID_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = PANEL_CATEGORY
    bl_order = 1

    def draw(self, context):
        layout = self.layout
        props = context.scene.cyl_grid_settings

        # ----------------------------------------------------
        # 状態
        # ----------------------------------------------------

        if props.detached:
            box = layout.box()
            box.label(text="完成Object", icon='CHECKMARK')
            box.label(text="アドオン制御から切離し済み")
            box.label(text="Materialも完全独立済み")
        else:
            box = layout.box()
            box.label(
                text=f"生成中 Object {props.session_id}",
                icon='MESH_CYLINDER',
            )
            box.label(text="リアルタイム更新中")

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

        box_x = layout.box()
        box_x.label(text="X方向円柱", icon='AXIS_TOP')

        row = box_x.row()
        row.prop(props, "x_count", text="本数")

        row = box_x.row()
        row.prop(props, "x_radius", text="太さ")

        row = box_x.row()
        row.prop(props, "x_spacing", text="間隔")

        row = box_x.row()
        row.prop(props, "x_length", text="長さ")

        row = box_x.row(align=True)
        row.label(text="表")
        row.prop(props, "x_color", text="")
        row.prop(props, "x_alpha", text="透明度", slider=True)

        row = box_x.row(align=True)
        row.label(text="裏")
        row.prop(props, "x_back_color", text="")
        row.prop(props, "x_back_alpha", text="透明度", slider=True)

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

        box_y = layout.box()
        box_y.label(text="Y方向円柱", icon='AXIS_FRONT')

        row = box_y.row()
        row.prop(props, "y_count", text="本数")

        row = box_y.row()
        row.prop(props, "y_radius", text="太さ")

        row = box_y.row()
        row.prop(props, "y_spacing", text="間隔")

        row = box_y.row()
        row.prop(props, "y_length", text="長さ")

        row = box_y.row(align=True)
        row.label(text="表")
        row.prop(props, "y_color", text="")
        row.prop(props, "y_alpha", text="透明度", slider=True)

        row = box_y.row(align=True)
        row.label(text="裏")
        row.prop(props, "y_back_color", text="")
        row.prop(props, "y_back_alpha", text="透明度", slider=True)

        # ----------------------------------------------------
        # 蓋・マテリアル
        # ----------------------------------------------------

        box_mat = layout.box()
        box_mat.label(
            text="蓋・マテリアル設定(完全独立)",
            icon='MATERIAL',
        )

        box_mat.prop(
            props,
            "create_top_cap",
            text="天井蓋を作る / 削除",
            toggle=True,
        )

        if props.create_top_cap:
            row = box_mat.row(align=True)
            row.label(text="天井蓋")
            row.prop(props, "top_color", text="")
            row.prop(
                props,
                "top_alpha",
                text="透明度",
                slider=True,
            )

        box_mat.separator()

        box_mat.prop(
            props,
            "create_bottom_cap",
            text="床面蓋を作る / 削除",
            toggle=True,
        )

        if props.create_bottom_cap:
            row = box_mat.row(align=True)
            row.label(text="床面蓋")
            row.prop(props, "bottom_color", text="")
            row.prop(
                props,
                "bottom_alpha",
                text="透明度",
                slider=True,
            )

        # ----------------------------------------------------
        # 共通
        # ----------------------------------------------------

        box_common = layout.box()
        box_common.label(text="共通設定", icon='SETTINGS')
        box_common.prop(props, "segments", text="分割数")

        # ----------------------------------------------------
        # 位置
        # ----------------------------------------------------

        box_pos = layout.box()
        box_pos.label(text="位置情報", icon='EMPTY_AXIS')

        row = box_pos.row()
        row.prop(props, "center_x", text="X")

        row = box_pos.row()
        row.prop(props, "center_y", text="Y")

        row = box_pos.row()
        row.prop(props, "center_z", text="Z")

        x_span = max(0, props.x_count - 1) * props.x_spacing
        y_span = max(0, props.y_count - 1) * props.y_spacing

        box_pos.label(text=f"X方向配置幅: {x_span:.4f}")
        box_pos.label(text=f"Y方向配置幅: {y_span:.4f}")

        box_pos.operator(
            "cyl_grid.copy_position",
            text="位置情報をコピー",
            icon='COPYDOWN',
        )

        # ----------------------------------------------------
        # 操作
        # ----------------------------------------------------

        box_action = layout.box()
        box_action.label(text="生成・完成Object管理", icon='OBJECT_DATA')

        if not props.detached:
            box_action.operator(
                "cyl_grid.create",
                text="作成 / 更新",
                icon='MESH_CYLINDER',
            )

            box_action.operator(
                "cyl_grid.detach",
                text="完成Objectとして切離し",
                icon='UNLINKED',
            )
        else:
            box_action.label(
                text="完成品は以後の設定変更から保護されています。",
                icon='LOCKED',
            )

            box_action.operator(
                "cyl_grid.next_object",
                text="次のObjectを作成",
                icon='ADD',
            )

        # ----------------------------------------------------
        # 一体化確認
        # ----------------------------------------------------

        completed_count = len(get_completed_objects())

        box_join = layout.box()
        box_join.label(
            text="完成Objectの一体化",
            icon='AUTOMERGE_ON',
        )
        box_join.label(
            text=f"完成Object数: {completed_count}",
        )

        box_join.operator(
            "cyl_grid.confirm_join",
            text="完成品を一体化するか確認",
            icon='AUTOMERGE_ON',
        )

        # ----------------------------------------------------
        # Reset
        # ----------------------------------------------------

        layout.operator(
            "cyl_grid.reset_settings",
            text="現在設定を初期値へ戻す",
            icon='LOOP_BACK',
        )

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 = PANEL_CATEGORY
    bl_order = 998

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

        layout.label(
            text="Cylinder Grid Floor",
            icon='URL',
        )

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

        # 参照コードで使えるようにリンク欄を固定。
        # 必要なURLはここへ追加できる。
        layout.label(text="Blender 5.0+ 専用")

class CYLGRID_PT_remove(bpy.types.Panel):
    bl_label = "Addon Remove"
    bl_idname = "CYLGRID_PT_remove"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = PANEL_CATEGORY
    bl_order = 999

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

        layout.label(
            text="Cylinder Grid Floor",
            icon='PANEL_CLOSE',
        )

        layout.label(
            text="アドオンを無効化します。",
            icon='INFO',
        )

        layout.operator(
            "cyl_grid.remove_addon",
            text="アドオン削除",
            icon='TRASH',
        )

# ============================================================
# register / unregister
# ============================================================

classes = (
    CYLGRID_PG_settings,
    CYLGRID_OT_create,
    CYLGRID_OT_detach,
    CYLGRID_OT_next_object,
    CYLGRID_OT_confirm_join,
    CYLGRID_OT_join_completed,
    CYLGRID_OT_copy_position,
    CYLGRID_OT_reset_settings,
    CYLGRID_OT_remove_addon,
    CYLGRID_PT_main,
    CYLGRID_PT_links,
    CYLGRID_PT_remove,
)

def set_material_preview():
    try:
        for window in bpy.context.window_manager.windows:
            screen = window.screen

            for area in screen.areas:
                if area.type == 'VIEW_3D':
                    space = area.spaces.active

                    if hasattr(space, "shading"):
                        space.shading.type = 'MATERIAL'
    except Exception:
        pass

def open_n_panel():
    """
    BlenderのUI状態を壊さないよう、timerから安全にNパネルを開く。
    """

    try:
        for window in bpy.context.window_manager.windows:
            screen = window.screen

            for area in screen.areas:
                if area.type == 'VIEW_3D':
                    for region in area.regions:
                        if region.type == 'WINDOW':
                            # Nパネルはbpy.opsではなくcontext overrideで開く。
                            with bpy.context.temp_override(
                                window=window,
                                area=area,
                                region=region,
                            ):
                                try:
                                    bpy.ops.view3d.sidebar(
                                        'INVOKE_DEFAULT'
                                    )
                                except Exception:
                                    pass
                            return
    except Exception:
        pass

def startup_ui():
    set_material_preview()

    # UI更新が完了してからNパネルを開く。
    try:
        open_n_panel()
    except Exception:
        pass

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

    bpy.types.Scene.cyl_grid_settings = bpy.props.PointerProperty(
        type=CYLGRID_PG_settings,
    )

    ensure_main_collections()

    # 初回生成
    props = bpy.context.scene.cyl_grid_settings

    try:
        rebuild_scene(props)
    except Exception:
        pass

    # Material Preview / N-panel
    try:
        bpy.app.timers.register(
            startup_ui,
            first_interval=0.5,
        )
    except Exception:
        pass

def unregister():
    # Scene Property
    if hasattr(bpy.types.Scene, "cyl_grid_settings"):
        del bpy.types.Scene.cyl_grid_settings

    for cls in reversed(classes):
        try:
            bpy.utils.unregister_class(cls)
        except Exception:
            pass

# ============================================================
# Direct execution
# ============================================================

if __name__ == "__main__":
    register()