# ============================================================
# 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, 1, 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 = 11
DEFAULT_Y_COUNT = 11

DEFAULT_X_RADIUS = 0.1000
DEFAULT_Y_RADIUS = 0.1000

DEFAULT_X_SPACING = 1.0000
DEFAULT_Y_SPACING = 1.0000

DEFAULT_X_LENGTH = 10.0000
DEFAULT_Y_LENGTH = 10.0000

DEFAULT_X_COLOR = (0.96, 0.62, 1.00, 1.0)
DEFAULT_X_ALPHA = 1.00
DEFAULT_X_BACK_COLOR = (0.02, 0.10, 0.35, 1.0)
DEFAULT_X_BACK_ALPHA = 1.00

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

DEFAULT_TOP_COLOR = (0.15, 0.65, 1.00, 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.0000
DEFAULT_CENTER_Y = 0.0000
DEFAULT_CENTER_Z = 0.0000

# ============================================================
# 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):
    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])

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

    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,
):
    mesh = bpy.data.meshes.new(unique_mesh_name(f"{PREFIX_VAL}MESH_{axis}"))
    bm = bmesh.new()

    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):
    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

    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):
    delete_current_objects()
    current, _fixed = ensure_main_collections()
    session_id = int(props.session_id)

    # X方向
    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_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_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_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'}

        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'}

        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_copy_code(bpy.types.Operator):
    bl_idname = "cyl_grid.copy_code"
    bl_label = "設定をコード形式でコピー"
    bl_description = "現在の数値をPythonの初期値コードとしてクリップボードへコピーします"

    def execute(self, context):
        props = context.scene.cyl_grid_settings
        
        def fmt_c(c):
            return f"({c[0]:.2f}, {c[1]:.2f}, {c[2]:.2f}, 1.0)"

        lines = [
            "# ============================================================",
            "# 初期値 (ここをコピーしたコードで書き換え可能)",
            "# ============================================================",
            f"DEFAULT_X_COUNT = {props.x_count}",
            f"DEFAULT_Y_COUNT = {props.y_count}",
            "",
            f"DEFAULT_X_RADIUS = {props.x_radius:.4f}",
            f"DEFAULT_Y_RADIUS = {props.y_radius:.4f}",
            "",
            f"DEFAULT_X_SPACING = {props.x_spacing:.4f}",
            f"DEFAULT_Y_SPACING = {props.y_spacing:.4f}",
            "",
            f"DEFAULT_X_LENGTH = {props.x_length:.4f}",
            f"DEFAULT_Y_LENGTH = {props.y_length:.4f}",
            "",
            f"DEFAULT_X_COLOR = {fmt_c(props.x_color)}",
            f"DEFAULT_X_ALPHA = {props.x_alpha:.2f}",
            f"DEFAULT_X_BACK_COLOR = {fmt_c(props.x_back_color)}",
            f"DEFAULT_X_BACK_ALPHA = {props.x_back_alpha:.2f}",
            "",
            f"DEFAULT_Y_COLOR = {fmt_c(props.y_color)}",
            f"DEFAULT_Y_ALPHA = {props.y_alpha:.2f}",
            f"DEFAULT_Y_BACK_COLOR = {fmt_c(props.y_back_color)}",
            f"DEFAULT_Y_BACK_ALPHA = {props.y_back_alpha:.2f}",
            "",
            f"DEFAULT_TOP_COLOR = {fmt_c(props.top_color)}",
            f"DEFAULT_TOP_ALPHA = {props.top_alpha:.2f}",
            "",
            f"DEFAULT_BOTTOM_COLOR = {fmt_c(props.bottom_color)}",
            f"DEFAULT_BOTTOM_ALPHA = {props.bottom_alpha:.2f}",
            "",
            f"DEFAULT_SEGMENTS = {props.segments}",
            "",
            f"DEFAULT_CREATE_TOP_CAP = {props.create_top_cap}",
            f"DEFAULT_CREATE_BOTTOM_CAP = {props.create_bottom_cap}",
            "",
            f"DEFAULT_CENTER_X = {props.center_x:.4f}",
            f"DEFAULT_CENTER_Y = {props.center_y:.4f}",
            f"DEFAULT_CENTER_Z = {props.center_z:.4f}",
        ]
        
        context.window_manager.clipboard = "\n".join(lines)
        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 = "wm.cyl_grid_remove_addon"
    bl_label = "アドオン削除"

    def execute(self, context):
        unregister()
        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="リアルタイム更新中")

        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)

        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')

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

        # コードコピー
        box_out = layout.box()
        box_out.label(text="現在の数値を初期値に", icon='TEXT')
        box_out.operator("cyl_grid.copy_code", text="コード形式でコピー", icon='COPYDOWN')

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}")
        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.operator("wm.cyl_grid_remove_addon", icon='CANCEL')

# ============================================================
# 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_copy_code,
    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():
    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':
                            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()
    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

    try:
        bpy.app.timers.register(startup_ui, first_interval=0.5)
    except Exception:
        pass

def unregister():
    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__":
    try:
        unregister()
    except Exception:
        pass
    register()