import bpy
import bmesh
import math
import random
import colorsys
from datetime import datetime
from mathutils import Vector, Euler
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import IntProperty, FloatProperty, FloatVectorProperty, PointerProperty, BoolProperty, EnumProperty

CUSTOM_CATEGORY_NAME = "[ トーラス球体 ]"

PREVIEW_COLL_SPHERES = "zPreview_TS_Spheres"
PREVIEW_COLL_FRAME = "zPreview_TS_Frame"

_is_updating = False
_is_unloading = False

# =========================================================
# ヘルパー: コレクションの表示同期 / クリーンアップ
# =========================================================
def set_collection_visibility(context, coll_name, visible):
    if not hasattr(context, "view_layer") or not context.view_layer:
        return
    layer_coll = context.view_layer.layer_collection

    def find_layer_coll(layer, name):
        if layer.collection.name == name: return layer
        for child in layer.children:
            res = find_layer_coll(child, name)
            if res: return res
        return None

    lc = find_layer_coll(layer_coll, coll_name)
    if lc:
        lc.exclude = False
        lc.hide_viewport = not visible

def cleanup_specific_preview(coll_name):
    preview_coll = bpy.data.collections.get(coll_name)
    if preview_coll:
        for obj in list(preview_coll.objects):
            mesh = obj.data
            bpy.data.objects.remove(obj, do_unlink=True)
            if mesh:
                try: bpy.data.meshes.remove(mesh, do_unlink=True)
                except: pass
        bpy.data.collections.remove(preview_coll)

def cleanup_all_previews():
    cleanup_specific_preview(PREVIEW_COLL_SPHERES)
    cleanup_specific_preview(PREVIEW_COLL_FRAME)
    
    for mat in list(bpy.data.materials):
        if mat.name.startswith("zPreview_TS_"):
            bpy.data.materials.remove(mat, do_unlink=True)

def get_or_create_material(mat_name, color):
    mat = bpy.data.materials.get(mat_name)
    if not mat:
        mat = bpy.data.materials.new(name=mat_name)
        mat.use_nodes = True
        mat.blend_method = 'BLEND'
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    if bsdf:
        bsdf.inputs["Base Color"].default_value = color
    mat.diffuse_color = color
    return mat

# =========================================================
# 矢印生成ヘルパー
# =========================================================
def create_arrow_mesh(name, origin, target, thickness, head_size):
    mesh = bpy.data.meshes.new(name)
    bm = bmesh.new()
    
    vec = target - origin
    dist = vec.length
    if dist < 0.0001:
        bm.to_mesh(mesh)
        bm.free()
        return mesh
        
    dir_vec = vec.normalized()
    rot_quat = dir_vec.to_track_quat('Z', 'Y')
    
    shaft_len = max(0.0, dist - head_size)
    if shaft_len > 0:
        geom_shaft = bmesh.ops.create_cone(
            bm, cap_ends=True, cap_tris=False, segments=16,
            radius1=thickness, radius2=thickness, depth=shaft_len
        )
        bmesh.ops.translate(bm, verts=geom_shaft['verts'], vec=(0, 0, shaft_len / 2.0))
        
    if head_size > 0:
        geom_head = bmesh.ops.create_cone(
            bm, cap_ends=True, cap_tris=False, segments=16,
            radius1=thickness * 2.5, radius2=0.0, depth=head_size
        )
        bmesh.ops.translate(bm, verts=geom_head['verts'], vec=(0, 0, shaft_len + head_size / 2.0))
        
    bmesh.ops.rotate(bm, verts=bm.verts, cent=(0,0,0), matrix=rot_quat.to_matrix())
    bmesh.ops.translate(bm, verts=bm.verts, vec=origin)
    
    bm.to_mesh(mesh)
    bm.free()
    
    for poly in mesh.polygons: poly.use_smooth = True
    return mesh

# =========================================================
# ライブアップデート
# =========================================================
def deferred_build():
    global _is_unloading
    if _is_unloading: return None
    build_previews(bpy.context)
    return None

def on_prop_update(self, context):
    global _is_unloading
    if _is_unloading: return
    if self.live_update and not bpy.app.timers.is_registered(deferred_build):
        bpy.app.timers.register(deferred_build, first_interval=0.05)

def update_spheres_preview(self, context):
    global _is_unloading
    if _is_unloading: return
    if not self.show_spheres_preview:
        cleanup_specific_preview(PREVIEW_COLL_SPHERES)
    build_previews(context)

def update_frame_preview(self, context):
    global _is_unloading
    if _is_unloading: return
    if not self.show_frame_preview:
        cleanup_specific_preview(PREVIEW_COLL_FRAME)
    build_previews(context)

# =========================================================
# プレビュー生成コア処理 (球体と額縁を完全に分離)
# =========================================================
def build_previews(context):
    global _is_updating, _is_unloading
    if _is_unloading or _is_updating: return
    _is_updating = True

    try:
        settings = context.scene.ts_settings
        
        # -----------------------------------------------------
        # 1. トーラス球体の生成
        # -----------------------------------------------------
        if settings.show_spheres_preview:
            cleanup_specific_preview(PREVIEW_COLL_SPHERES)
            coll_spheres = bpy.data.collections.new(PREVIEW_COLL_SPHERES)
            context.scene.collection.children.link(coll_spheres)
            set_collection_visibility(context, PREVIEW_COLL_SPHERES, True)

            rot_euler = Euler(settings.spheres_rotation, 'XYZ')
            arrow_origin_vec = Vector(settings.arrow_origin)

            if settings.show_arrows:
                mat_arrow = get_or_create_material("zPreview_TS_Mat_Arrow", settings.arrow_color)

            for i in range(settings.count):
                angle = (2.0 * math.pi / settings.count) * i
                pos = Vector((
                    settings.torus_radius * math.cos(angle),
                    settings.torus_radius * math.sin(angle),
                    0.0
                ))
                pos.rotate(rot_euler)
                pos += Vector(settings.spheres_loc)
                
                # 球体の生成
                mesh_sphere = bpy.data.meshes.new(f"zPreview_TS_Sphere_{i}")
                bm = bmesh.new()
                bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=settings.sphere_radius)
                bm.to_mesh(mesh_sphere)
                bm.free()
                
                obj_sphere = bpy.data.objects.new(f"zPreview_TS_Sphere_{i}", mesh_sphere)
                coll_spheres.objects.link(obj_sphere)
                obj_sphere.location = pos
                for poly in mesh_sphere.polygons: poly.use_smooth = True

                if settings.color_mode == 'SINGLE':
                    mat = get_or_create_material("zPreview_TS_Mat_Sphere_Single", settings.single_color)
                else:
                    random.seed(settings.random_seed + i)
                    r, g, b = colorsys.hsv_to_rgb(random.random(), 0.8, 0.9)
                    mat = get_or_create_material(f"zPreview_TS_Mat_Sphere_Rand_{i}", (r, g, b, 1.0))
                obj_sphere.data.materials.append(mat)

                # 矢印の生成
                if settings.show_arrows:
                    mesh_arrow = create_arrow_mesh(
                        f"zPreview_TS_Arrow_{i}", 
                        arrow_origin_vec, pos, 
                        settings.arrow_thickness, settings.arrow_head_size
                    )
                    obj_arrow = bpy.data.objects.new(f"zPreview_TS_Arrow_{i}", mesh_arrow)
                    coll_spheres.objects.link(obj_arrow)
                    obj_arrow.data.materials.append(mat_arrow)

        # -----------------------------------------------------
        # 2. 独立した額縁の生成
        # -----------------------------------------------------
        if settings.show_frame_preview:
            cleanup_specific_preview(PREVIEW_COLL_FRAME)
            coll_frame = bpy.data.collections.new(PREVIEW_COLL_FRAME)
            context.scene.collection.children.link(coll_frame)
            set_collection_visibility(context, PREVIEW_COLL_FRAME, True)

            mesh_frame = bpy.data.meshes.new("zPreview_TS_Frame")
            bm_sq = bmesh.new()
            
            S = settings.frame_size / 2.0
            w = settings.frame_width
            s = max(0.001, S - w)
            z = -settings.frame_thickness / 2.0
            
            v0 = bm_sq.verts.new((-S, -S, z))
            v1 = bm_sq.verts.new(( S, -S, z))
            v2 = bm_sq.verts.new(( S,  S, z))
            v3 = bm_sq.verts.new((-S,  S, z))
            
            v4 = bm_sq.verts.new((-s, -s, z))
            v5 = bm_sq.verts.new(( s, -s, z))
            v6 = bm_sq.verts.new(( s,  s, z))
            v7 = bm_sq.verts.new((-s,  s, z))
            
            f1 = bm_sq.faces.new((v0, v1, v5, v4))
            f2 = bm_sq.faces.new((v1, v2, v6, v5))
            f3 = bm_sq.faces.new((v2, v3, v7, v6))
            f4 = bm_sq.faces.new((v3, v0, v4, v7))
            
            extruded = bmesh.ops.extrude_face_region(bm_sq, geom=[f1, f2, f3, f4])
            extrude_verts = [elem for elem in extruded['geom'] if isinstance(elem, bmesh.types.BMVert)]
            bmesh.ops.translate(bm_sq, verts=extrude_verts, vec=(0, 0, settings.frame_thickness))
            
            # 独立した回転と位置の適用
            rot_mat = Euler(settings.frame_rotation, 'XYZ').to_matrix()
            bmesh.ops.rotate(bm_sq, verts=bm_sq.verts, cent=(0,0,0), matrix=rot_mat)
            bmesh.ops.translate(bm_sq, verts=bm_sq.verts, vec=settings.frame_loc)
            
            bmesh.ops.recalc_face_normals(bm_sq, faces=bm_sq.faces)
            
            bm_sq.to_mesh(mesh_frame)
            bm_sq.free()
            
            for poly in mesh_frame.polygons: poly.use_smooth = True
            
            obj_frame = bpy.data.objects.new("zPreview_TS_Frame_Obj", mesh_frame)
            coll_frame.objects.link(obj_frame)
            mat_sq = get_or_create_material("zPreview_TS_Mat_Frame", settings.frame_color)
            obj_frame.data.materials.append(mat_sq)

        context.view_layer.update()

    finally:
        _is_updating = False

# =========================================================
# プロパティ定義
# =========================================================
class TS_Settings(PropertyGroup):
    live_update: BoolProperty(name="Live Update", default=True)

    # --- 球体ジェネレーター設定 ---
    show_spheres_preview: BoolProperty(name="球体プレビュー表示", default=True, update=update_spheres_preview)
    count: IntProperty(name="球の数", default=12, min=3, update=on_prop_update)
    torus_radius: FloatProperty(name="配置半径", default=5.0, min=0.1, update=on_prop_update)
    sphere_radius: FloatProperty(name="球の半径", default=1.0, min=0.01, update=on_prop_update)
    spheres_loc: FloatVectorProperty(name="配置中心位置", default=(0.0, 0.0, 0.0), subtype='TRANSLATION', update=on_prop_update)
    spheres_rotation: FloatVectorProperty(name="回転軸 (XYZ)", default=(0.0, 0.0, 0.0), subtype='EULER', update=on_prop_update)

    color_mode: EnumProperty(
        name="色設定", items=[('SINGLE', "1色 (全部同じ)", ""), ('RANDOM', "ランダム", "")],
        default='SINGLE', update=on_prop_update
    )
    single_color: FloatVectorProperty(name="カラー", subtype='COLOR', size=4, default=(0.2, 0.6, 1.0, 1.0), min=0, max=1, update=on_prop_update)
    random_seed: IntProperty(name="ランダムシード", default=123, update=on_prop_update)

    show_arrows: BoolProperty(name="矢印を表示", default=False, update=on_prop_update)
    arrow_origin: FloatVectorProperty(name="矢印の起点", default=(0.0, 0.0, 0.0), subtype='TRANSLATION', update=on_prop_update)
    arrow_thickness: FloatProperty(name="太さ", default=0.05, min=0.001, update=on_prop_update)
    arrow_head_size: FloatProperty(name="ヘッド長さ", default=0.5, min=0.01, update=on_prop_update)
    arrow_color: FloatVectorProperty(name="矢印の色", subtype='COLOR', size=4, default=(0.8, 0.2, 0.2, 1.0), min=0, max=1, update=on_prop_update)

    # --- 独立額縁ジェネレーター設定 ---
    show_frame_preview: BoolProperty(name="額縁プレビュー表示", default=False, update=update_frame_preview)
    frame_size: FloatProperty(name="サイズ (外寸)", default=4.0, min=0.5, update=on_prop_update)
    frame_width: FloatProperty(name="枠の太さ", default=0.5, min=0.01, update=on_prop_update)
    frame_thickness: FloatProperty(name="厚み", default=0.2, min=0.001, update=on_prop_update)
    frame_color: FloatVectorProperty(name="額縁の色", subtype='COLOR', size=4, default=(0.1, 0.8, 0.3, 1.0), min=0, max=1, update=on_prop_update)
    frame_loc: FloatVectorProperty(name="中心位置", default=(0.0, -10.0, 0.0), subtype='TRANSLATION', update=on_prop_update)
    frame_rotation: FloatVectorProperty(name="回転 (XYZ)", default=(0.0, 0.0, 0.0), subtype='EULER', update=on_prop_update)

# =========================================================
# オペレーター (それぞれの確定)
# =========================================================
class TS_OT_DetachSpheres(Operator):
    bl_idname = "ts.detach_spheres"
    bl_label = "球体を確定 & 切り離し"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        preview_coll = bpy.data.collections.get(PREVIEW_COLL_SPHERES)
        if not preview_coll or not preview_coll.objects:
            self.report({'WARNING'}, "球体のプレビューがありません。")
            return {'CANCELLED'}

        run_id = datetime.now().strftime("%H%M%S")
        preview_coll.name = f"Spheres_{run_id}"

        for obj in preview_coll.objects:
            obj.name = obj.name.replace("zPreview_TS_", "TS_") + f"_{run_id}"
            if obj.data: obj.data.name = obj.data.name.replace("zPreview_TS_", "Mesh_") + f"_{run_id}"
            for mat_slot in obj.material_slots:
                if mat_slot.material and "zPreview_TS_" in mat_slot.material.name:
                    mat_slot.material.name = mat_slot.material.name.replace("zPreview_TS_", "Mat_TS_") + f"_{run_id}"
                
        self.report({'INFO'}, f"球体を確定しました: Spheres_{run_id}")
        build_previews(context)
        return {'FINISHED'}

class TS_OT_DetachFrame(Operator):
    bl_idname = "ts.detach_frame"
    bl_label = "額縁を確定 & 切り離し"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        preview_coll = bpy.data.collections.get(PREVIEW_COLL_FRAME)
        if not preview_coll or not preview_coll.objects:
            self.report({'WARNING'}, "額縁のプレビューがありません。")
            return {'CANCELLED'}

        run_id = datetime.now().strftime("%H%M%S")
        preview_coll.name = f"Frame_{run_id}"

        for obj in preview_coll.objects:
            obj.name = obj.name.replace("zPreview_TS_", "TS_") + f"_{run_id}"
            if obj.data: obj.data.name = obj.data.name.replace("zPreview_TS_", "Mesh_") + f"_{run_id}"
            for mat_slot in obj.material_slots:
                if mat_slot.material and "zPreview_TS_" in mat_slot.material.name:
                    mat_slot.material.name = mat_slot.material.name.replace("zPreview_TS_", "Mat_TS_") + f"_{run_id}"
                
        self.report({'INFO'}, f"額縁を確定しました: Frame_{run_id}")
        build_previews(context)
        return {'FINISHED'}

# =========================================================
# パネル UI 1: トーラス球体ジェネレーター
# =========================================================
class TS_PT_SpheresPanel(Panel):
    bl_label = "トーラス球体 ジェネレーター"
    bl_idname = "TS_PT_spheres"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = CUSTOM_CATEGORY_NAME

    def draw(self, context):
        layout = self.layout
        settings = context.scene.ts_settings

        row = layout.row()
        row.prop(settings, "show_spheres_preview", text="プレビュー表示")
        row.prop(settings, "live_update")
        
        if not settings.show_spheres_preview: return
        layout.separator()

        box = layout.box()
        col = box.column(align=True)
        col.prop(settings, "count")
        col.prop(settings, "torus_radius")
        col.prop(settings, "sphere_radius")
        box.separator()
        col = box.column(align=True)
        col.prop(settings, "spheres_loc")
        col.prop(settings, "spheres_rotation")

        box = layout.box()
        box.label(text="カラー設定", icon='COLOR')
        box.prop(settings, "color_mode", expand=True)
        if settings.color_mode == 'SINGLE':
            box.prop(settings, "single_color", text="")
        else:
            box.prop(settings, "random_seed")

        box = layout.box()
        box.label(text="矢印設定", icon='EMPTY_SINGLE_ARROW')
        box.prop(settings, "show_arrows")
        if settings.show_arrows:
            col = box.column(align=True)
            col.prop(settings, "arrow_origin", text="起点")
            col.prop(settings, "arrow_thickness")
            col.prop(settings, "arrow_head_size")
            col.prop(settings, "arrow_color")
            
        layout.separator()
        row = layout.row()
        row.scale_y = 1.5
        row.operator(TS_OT_DetachSpheres.bl_idname, icon='DUPLICATE')

# =========================================================
# パネル UI 2: 独立額縁ジェネレーター
# =========================================================
class TS_PT_FramePanel(Panel):
    bl_label = "独立額縁 ジェネレーター"
    bl_idname = "TS_PT_frame"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = CUSTOM_CATEGORY_NAME

    def draw(self, context):
        layout = self.layout
        settings = context.scene.ts_settings

        layout.prop(settings, "show_frame_preview", text="プレビュー表示")
        
        if not settings.show_frame_preview: return
        layout.separator()

        box = layout.box()
        col = box.column(align=True)
        col.prop(settings, "frame_size")
        col.prop(settings, "frame_width")
        col.prop(settings, "frame_thickness")
        box.separator()
        col = box.column(align=True)
        col.prop(settings, "frame_loc")
        col.prop(settings, "frame_rotation")
        box.separator()
        box.prop(settings, "frame_color")
        
        layout.separator()
        row = layout.row()
        row.scale_y = 1.5
        row.operator(TS_OT_DetachFrame.bl_idname, icon='DUPLICATE')

# =========================================================
# パネル UI 3: アドオン削除機能
# =========================================================
def delayed_unregister():
    try: unregister()
    except Exception: pass
    return None

class TS_OT_RemoveAllPanels(Operator):
    bl_idname = "ts.remove_all_panels"
    bl_label = "Unregister Addon"
    def execute(self, context): 
        bpy.app.timers.register(delayed_unregister, first_interval=0.01)
        return {'FINISHED'}

class TS_PT_RemovePanel(Panel):
    bl_label = "Remove Addon"
    bl_idname = "TS_PT_remove"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = CUSTOM_CATEGORY_NAME
    bl_options = {'DEFAULT_CLOSED'}
    
    def draw(self, context): 
        self.layout.operator(TS_OT_RemoveAllPanels.bl_idname, icon='CANCEL')

# =========================================================
# 登録処理
# =========================================================
classes = (
    TS_Settings,
    TS_OT_DetachSpheres,
    TS_OT_DetachFrame,
    TS_OT_RemoveAllPanels,
    TS_PT_SpheresPanel,
    TS_PT_FramePanel,
    TS_PT_RemovePanel,
)

def register():
    global _is_unloading
    _is_unloading = False 
    
    if hasattr(bpy.types.Scene, 'ts_settings'):
        del bpy.types.Scene.ts_settings

    for cls in classes:
        try: bpy.utils.register_class(cls)
        except ValueError:
            bpy.utils.unregister_class(cls)
            bpy.utils.register_class(cls)
            
    bpy.types.Scene.ts_settings = PointerProperty(type=TS_Settings)

def unregister():
    global _is_unloading
    _is_unloading = True 
    
    if bpy.app.timers.is_registered(deferred_build):
        bpy.app.timers.unregister(deferred_build)

    cleanup_all_previews()

    if hasattr(bpy.types.Scene, 'ts_settings'):
        del bpy.types.Scene.ts_settings
        
    for cls in reversed(classes):
        try: bpy.utils.unregister_class(cls)
        except Exception: pass

if __name__ == "__main__":
    register()

import bpy
import bmesh
import math
import random
import colorsys
from datetime import datetime
from mathutils import Vector, Euler
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import IntProperty, FloatProperty, FloatVectorProperty, PointerProperty, BoolProperty, EnumProperty

CUSTOM_TAG_NAME = "TorusSpheres"
CUSTOM_CATEGORY_NAME = "[ トーラス球体 ]"

PREVIEW_COLL_NAME = f"zPreview_{CUSTOM_TAG_NAME}"
PREVIEW_MAT_NAME_BASE = f"zPreview_{CUSTOM_TAG_NAME}_Mat"

_is_updating = False
_is_unloading = False

# =========================================================
# ヘルパー: コレクションの表示同期 / クリーンアップ
# =========================================================
def set_collection_visibility(context, coll_name, visible):
    if not hasattr(context, "view_layer") or not context.view_layer:
        return
    layer_coll = context.view_layer.layer_collection

    def find_layer_coll(layer, name):
        if layer.collection.name == name: return layer
        for child in layer.children:
            res = find_layer_coll(child, name)
            if res: return res
        return None

    lc = find_layer_coll(layer_coll, coll_name)
    if lc:
        lc.exclude = False
        lc.hide_viewport = not visible

def cleanup_preview():
    preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
    if preview_coll:
        for obj in list(preview_coll.objects):
            mesh = obj.data
            bpy.data.objects.remove(obj, do_unlink=True)
            if mesh:
                try: bpy.data.meshes.remove(mesh, do_unlink=True)
                except: pass
        bpy.data.collections.remove(preview_coll)
        
    for mat in list(bpy.data.materials):
        if mat.name.startswith(PREVIEW_MAT_NAME_BASE):
            bpy.data.materials.remove(mat, do_unlink=True)

# =========================================================
# ライブアップデートとマテリアル生成
# =========================================================
def deferred_build():
    global _is_unloading
    if _is_unloading: return None
    build_preview(bpy.context)
    return None

def on_prop_update(self, context):
    global _is_unloading
    if _is_unloading: return
    if self.live_update and not bpy.app.timers.is_registered(deferred_build):
        bpy.app.timers.register(deferred_build, first_interval=0.05)

def update_preview_visibility(self, context):
    global _is_unloading
    if _is_unloading: return
    if self.show_preview:
        build_preview(context)
    else:
        preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
        if preview_coll:
            preview_coll.hide_viewport = True
            preview_coll.hide_render = True
            set_collection_visibility(context, PREVIEW_COLL_NAME, False)
            context.view_layer.update()

def get_or_create_material(mat_name, color):
    mat = bpy.data.materials.get(mat_name)
    if not mat:
        mat = bpy.data.materials.new(name=mat_name)
        mat.use_nodes = True
        mat.blend_method = 'BLEND'
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    if bsdf:
        bsdf.inputs["Base Color"].default_value = color
    mat.diffuse_color = color
    return mat

# =========================================================
# プレビュー生成コア処理
# =========================================================
def build_preview(context):
    global _is_updating, _is_unloading
    if _is_unloading or _is_updating: return
    _is_updating = True

    try:
        settings = context.scene.ts_settings
        
        cleanup_preview()
        
        if not settings.show_preview:
            return

        preview_coll = bpy.data.collections.new(PREVIEW_COLL_NAME)
        context.scene.collection.children.link(preview_coll)
        preview_coll.hide_viewport = False
        preview_coll.hide_render = False
        set_collection_visibility(context, PREVIEW_COLL_NAME, True)

        rot_euler = Euler(settings.rotation, 'XYZ')

        for i in range(settings.count):
            angle = (2.0 * math.pi / settings.count) * i
            
            # Z=0 (XY平面) 計算
            pos = Vector((
                settings.torus_radius * math.cos(angle),
                settings.torus_radius * math.sin(angle),
                0.0
            ))
            
            # 回転と中心オフセットの適用
            pos.rotate(rot_euler)
            pos += Vector(settings.center_loc)
            
            # プレビュー用メッシュの生成
            mesh = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_Sphere_{i}")
            bm = bmesh.new()
            bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=settings.sphere_radius)
            bm.to_mesh(mesh)
            bm.free()
            
            obj = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_Sphere_{i}", mesh)
            preview_coll.objects.link(obj)
            obj.location = pos

            for poly in mesh.polygons:
                poly.use_smooth = True

            # マテリアル設定 (1色 or ランダム)
            if settings.color_mode == 'SINGLE':
                color = settings.single_color
                mat_name = f"{PREVIEW_MAT_NAME_BASE}_Single"
            else:
                random.seed(settings.random_seed + i)
                r, g, b = colorsys.hsv_to_rgb(random.random(), 0.8, 0.9)
                color = (r, g, b, 1.0)
                mat_name = f"{PREVIEW_MAT_NAME_BASE}_Rand_{i}"

            mat = get_or_create_material(mat_name, color)
            obj.data.materials.append(mat)

        context.view_layer.update()

    finally:
        _is_updating = False

# =========================================================
# プロパティ定義
# =========================================================
class TS_Settings(PropertyGroup):
    show_preview: BoolProperty(name="プレビュー表示", default=True, update=update_preview_visibility)
    live_update: BoolProperty(name="Live Update", default=True)

    count: IntProperty(name="球の数", default=12, min=3, update=on_prop_update)
    torus_radius: FloatProperty(name="配置半径", default=5.0, min=0.1, update=on_prop_update)
    sphere_radius: FloatProperty(name="球の半径", default=1.0, min=0.01, update=on_prop_update)
    
    center_loc: FloatVectorProperty(name="中心位置", default=(0.0, 0.0, 0.0), subtype='TRANSLATION', update=on_prop_update)
    rotation: FloatVectorProperty(name="回転軸 (XYZ)", default=(0.0, 0.0, 0.0), subtype='EULER', update=on_prop_update)

    color_mode: EnumProperty(
        name="色設定",
        items=[('SINGLE', "1色 (全部同じ)", ""), ('RANDOM', "ランダム", "")],
        default='SINGLE', update=on_prop_update
    )
    single_color: FloatVectorProperty(name="カラー", subtype='COLOR', size=4, default=(0.2, 0.6, 1.0, 1.0), min=0, max=1, update=on_prop_update)
    random_seed: IntProperty(name="ランダムシード", default=123, update=on_prop_update)

# =========================================================
# オペレーター (確定・削除)
# =========================================================
class TS_OT_Detach(Operator):
    bl_idname = "ts.detach"
    bl_label = "確定 & 切り離し"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
        if not preview_coll or not preview_coll.objects:
            self.report({'WARNING'}, "プレビューが表示されていません。")
            return {'CANCELLED'}

        run_id = datetime.now().strftime("%H%M%S")
        preview_coll.name = f"{CUSTOM_TAG_NAME}_{run_id}"

        # オブジェクトとマテリアルを独立名に変更(重複増殖を防ぐ)
        for obj in preview_coll.objects:
            obj.name = obj.name.replace(f"zPreview_{CUSTOM_TAG_NAME}_", f"{CUSTOM_TAG_NAME}_") + f"_{run_id}"
            if obj.data: 
                obj.data.name = obj.data.name.replace(f"zPreview_{CUSTOM_TAG_NAME}_", "Mesh_") + f"_{run_id}"
            
            for mat_slot in obj.material_slots:
                if mat_slot.material and PREVIEW_MAT_NAME_BASE in mat_slot.material.name:
                    new_name = mat_slot.material.name.replace(PREVIEW_MAT_NAME_BASE, f"{CUSTOM_TAG_NAME}_Mat") + f"_{run_id}"
                    mat_slot.material.name = new_name
                
        self.report({'INFO'}, f"切り離し完了: {CUSTOM_TAG_NAME}_{run_id}")
        
        # 次の配置のために新しいプレビューを作り直す
        build_preview(context)
        return {'FINISHED'}

def delayed_unregister():
    try:
        unregister()
    except Exception as e:
        print("Unregister error:", e)
    return None

class TS_OT_RemovePanels(Operator):
    bl_idname = "ts.remove_panels"
    bl_label = "Unregister Addon"
    
    def execute(self, context): 
        # エラー回避: UIの描画サイクルが終わった直後(0.01秒後)に安全に削除を実行する
        bpy.app.timers.register(delayed_unregister, first_interval=0.01)
        self.report({'INFO'}, "パネルとプレビューを削除しました")
        return {'FINISHED'}

# =========================================================
# パネル
# =========================================================
class TS_PT_MainPanel(Panel):
    bl_label = "ドキュメント / 設定"
    bl_idname = "TS_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = CUSTOM_CATEGORY_NAME

    def draw(self, context):
        layout = self.layout
        settings = context.scene.ts_settings

        box = layout.box()
        box.label(text="【使用方法】", icon='INFO')
        box.label(text="数値を変更するとリアルタイムで更新されます。")
        box.label(text="確定ボタンを押すと別コレクションに分離されます。")

        layout.separator()

        row = layout.row()
        row.prop(settings, "show_preview")
        row.prop(settings, "live_update")

        layout.separator()
        
        col = layout.column(align=True)
        col.prop(settings, "count")
        col.prop(settings, "torus_radius")
        col.prop(settings, "sphere_radius")
        
        layout.separator()
        col = layout.column(align=True)
        col.prop(settings, "center_loc")
        col.prop(settings, "rotation")

        layout.separator()
        
        box = layout.box()
        box.label(text="カラー設定", icon='COLOR')
        box.prop(settings, "color_mode", expand=True)
        if settings.color_mode == 'SINGLE':
            box.prop(settings, "single_color")
        else:
            box.prop(settings, "random_seed")

        layout.separator()
        
        row = layout.row()
        row.scale_y = 1.5
        row.operator(TS_OT_Detach.bl_idname, icon='DUPLICATE')

class TS_PT_RemovePanel(Panel):
    bl_label = "アドオン削除"
    bl_idname = "TS_PT_remove"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = CUSTOM_CATEGORY_NAME
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        self.layout.operator(TS_OT_RemovePanels.bl_idname, icon='CANCEL', text="パネルとプレビューを消去")

# =========================================================
# 登録処理
# =========================================================
classes = (
    TS_Settings,
    TS_OT_Detach,
    TS_OT_RemovePanels,
    TS_PT_MainPanel,
    TS_PT_RemovePanel,
)

def register():
    global _is_unloading
    _is_unloading = False 
    for cls in classes:
        # 重複登録エラーを防ぐため安全に登録
        if not hasattr(bpy.types, cls.__name__):
            bpy.utils.register_class(cls)
    bpy.types.Scene.ts_settings = PointerProperty(type=TS_Settings)

def unregister():
    global _is_unloading
    _is_unloading = True 
    
    if bpy.app.timers.is_registered(deferred_build):
        bpy.app.timers.unregister(deferred_build)

    cleanup_preview()

    if hasattr(bpy.types.Scene, 'ts_settings'):
        del bpy.types.Scene.ts_settings
        
    for cls in reversed(classes):
        if hasattr(bpy.types, cls.__name__):
            try:
                bpy.utils.unregister_class(cls)
            except RuntimeError:
                pass

if __name__ == "__main__":
    register()