サイドバー", } # ============================================================================== # 定数 & 設定エリア # ============================================================================== ADDON_CATEGORY_NAME = bl_info["category"] PREFIX = "interval_track_gen_2026" MAIN_COLLECTION_NAME = "光時計グループ" BASE_SHAPE_COLLECTION_NAME = "ベース図形" SPHERE_COLLECTION_NAME = "球体" END_TORUS_COLLECTION_NAME = "先端トーラス" BASE_OBJ_NAME = "光時計" SPHERE_OBJ_BASE_NAME = "マーカー" "> サイドバー", } # ============================================================================== # 定数 & 設定エリア # ============================================================================== ADDON_CATEGORY_NAME = bl_info["category"] PREFIX = "interval_track_gen_2026" MAIN_COLLECTION_NAME = "光時計グループ" BASE_SHAPE_COLLECTION_NAME = "ベース図形" SPHERE_COLLECTION_NAME = "球体" END_TORUS_COLLECTION_NAME = "先端トーラス" BASE_OBJ_NAME = "光時計" SPHERE_OBJ_BASE_NAME = "マーカー" "> サイドバー", } # ============================================================================== # 定数 & 設定エリア # ============================================================================== ADDON_CATEGORY_NAME = bl_info["category"] PREFIX = "interval_track_gen_2026" MAIN_COLLECTION_NAME = "光時計グループ" BASE_SHAPE_COLLECTION_NAME = "ベース図形" SPHERE_COLLECTION_NAME = "球体" END_TORUS_COLLECTION_NAME = "先端トーラス" BASE_OBJ_NAME = "光時計" SPHERE_OBJ_BASE_NAME = "マーカー" ">

rapture_20260707225440.png

import bpy
import bmesh
import math
import mathutils
import random
from bpy.types import Operator, Panel, PropertyGroup

# ==============================================================================
# アドオンのメタデータ
# ==============================================================================
bl_info = {
    "name": "軌跡エフェクト生成 (等間隔 / 到達ハイブリッド)",
    "author": "zionadchat",
    "version": (3, 6, 0),
    "blender": (4, 4, 0),
    "category": " 22  [  軌跡ジェネレーター  ]   ",
    "description": "円柱/トーラス、軌跡生成、先端トーラス生成機能搭載",
    "location": "3Dビュー > サイドバー",
}

# ==============================================================================
# 定数 & 設定エリア
# ==============================================================================
ADDON_CATEGORY_NAME = bl_info["category"]
PREFIX = "interval_track_gen_2026"

MAIN_COLLECTION_NAME = "光時計グループ"
BASE_SHAPE_COLLECTION_NAME = "ベース図形"
SPHERE_COLLECTION_NAME = "球体"
END_TORUS_COLLECTION_NAME = "先端トーラス"

BASE_OBJ_NAME = "光時計"
SPHERE_OBJ_BASE_NAME = "マーカー"

ADDON_LINKS = [
    {"label": "アドオン削除パネル 20260628", "url": "<https://note.com/zionadmillion/n/ndc6fd2eabd2a>", "icon": "URL"},
    {"label": "進化版 画面中央 透視投影視座位置", "url": "<https://www.notion.so/20260319bb-327f5dacaf43801e8e37ce489dc1d593>", "icon": "URL"},
    {"label": "5520 背景色 変更", "url": "<https://www.notion.so/5520-20260316-314f5dacaf4380da9be4c05551d40710>", "icon": "URL"},
]

# ==============================================================================
# ユーティリティ関数
# ==============================================================================
def get_color_material(color, prefix="Mat_Track_"):
    """色(RGB)を名前にしたマテリアルを取得・生成し、重複を自動防止"""
    r = int(max(0.0, min(1.0, color[0])) * 255)
    g = int(max(0.0, min(1.0, color[1])) * 255)
    b = int(max(0.0, min(1.0, color[2])) * 255)
    mat_name = f"{prefix}{r:02X}{g:02X}{b:02X}"
    
    mat = bpy.data.materials.get(mat_name)
    if not mat:
        mat = bpy.data.materials.new(name=mat_name)
        mat.use_nodes = True
        bsdf = mat.node_tree.nodes.get("Principled BSDF")
        if bsdf: bsdf.inputs['Base Color'].default_value = color
        mat.diffuse_color = color
    else:
        bsdf = mat.node_tree.nodes.get("Principled BSDF")
        if bsdf: bsdf.inputs['Base Color'].default_value = color
        mat.diffuse_color = color
        
    return mat

def cleanup_unused_materials():
    """使われていない自動生成マテリアルを削除し、スライダー操作時のゴミを消す"""
    for mat in list(bpy.data.materials):
        if (mat.name.startswith("Mat_Base_") or 
            mat.name.startswith("Mat_Sphere_") or 
            mat.name.startswith("Mat_EndTorus_")) and mat.users == 0:
            bpy.data.materials.remove(mat)

def create_torus_bmesh(bm, major_segments, minor_segments, major_radius, minor_radius):
    verts = []
    for i in range(major_segments):
        theta = i * 2 * math.pi / major_segments
        cos_theta = math.cos(theta)
        sin_theta = math.sin(theta)
        loop_verts = []
        for j in range(minor_segments):
            phi = j * 2 * math.pi / minor_segments
            cos_phi = math.cos(phi)
            sin_phi = math.sin(phi)
            
            x = (major_radius + minor_radius * cos_phi) * cos_theta
            y = (major_radius + minor_radius * cos_phi) * sin_theta
            z = minor_radius * sin_phi
            
            v = bm.verts.new((x, y, z))
            loop_verts.append(v)
        verts.append(loop_verts)
        
    for i in range(major_segments):
        next_i = (i + 1) % major_segments
        for j in range(minor_segments):
            next_j = (j + 1) % minor_segments
            bm.faces.new((verts[i][j], verts[i][next_j], verts[next_i][next_j], verts[next_i][j]))
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)

def get_or_create_collection(name, parent_col=None):
    col = bpy.data.collections.get(name)
    if not col:
        col = bpy.data.collections.new(name)
        if parent_col: parent_col.children.link(col)
        else: bpy.context.scene.collection.children.link(col)
    return col

def get_active_light_clock(context):
    for obj in context.scene.objects:
        if obj.get("is_light_clock") == 1: return obj
    return None

def update_light_clock(self, context):
    clock_obj = get_active_light_clock(context)
    if not clock_obj: return
    
    props = context.scene.light_clock_props
    
    # --------------------------------------------------------------------------
    # 1. ベース図形の更新
    # --------------------------------------------------------------------------
    bm = bmesh.new()
    
    if props.base_shape == 'CYLINDER':
        start_pos = mathutils.Vector((props.start_x, props.start_y, props.start_z))
        end_pos = mathutils.Vector((props.end_x, props.end_y, props.end_z))
        vec = end_pos - start_pos
        length = vec.length
        
        bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
        for v in bm.verts: v.co.z += 0.5
        
        clock_obj.location = start_pos
        if length > 0.0001:
            clock_obj.rotation_euler = vec.to_track_quat('Z', 'Y').to_euler()
        else:
            clock_obj.rotation_euler = (0, 0, 0)
        radius = props.thickness / 2.0
        clock_obj.scale = (radius, radius, length)
        
    elif props.base_shape == 'TORUS':
        create_torus_bmesh(bm, major_segments=48, minor_segments=16, 
                           major_radius=props.torus_major_radius, 
                           minor_radius=props.torus_minor_radius)
        
        clock_obj.location = (props.torus_loc_x, props.torus_loc_y, props.torus_loc_z)
        clock_obj.rotation_euler = (props.torus_rot_x, props.torus_rot_y, props.torus_rot_z)
        clock_obj.scale = (1.0, 1.0, 1.0)
        
    bm.to_mesh(clock_obj.data)
    bm.free()

    clock_mat = get_color_material(props.color, prefix="Mat_Base_")
    if clock_obj.data.materials: clock_obj.data.materials[0] = clock_mat
    else: clock_obj.data.materials.append(clock_mat)

    # --------------------------------------------------------------------------
    # 2. 球体の生成と数合わせ
    # --------------------------------------------------------------------------
    sphere_objs = [obj for obj in bpy.data.objects if obj.get("is_light_clock_sphere") == 1]
    
    base_count = props.sphere_count if props.use_spheres else 0
    gather_add = props.gather_count if (props.use_spheres and props.use_gather) else 0
    target_count = base_count + (base_count * gather_add)
    current_count = len(sphere_objs)
    
    main_col = get_or_create_collection(MAIN_COLLECTION_NAME, context.scene.collection)
    sphere_col = get_or_create_collection(SPHERE_COLLECTION_NAME, main_col)
    
    if current_count < target_count:
        for _ in range(target_count - current_count):
            mesh = bpy.data.meshes.new(f"{SPHERE_OBJ_BASE_NAME}_Mesh")
            bm = bmesh.new()
            bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
            bm.to_mesh(mesh)
            bm.free()
            new_sphere = bpy.data.objects.new(f"{SPHERE_OBJ_BASE_NAME}_編集中", mesh)
            new_sphere["is_light_clock_sphere"] = 1
            new_sphere["individual_color"] = (random.random(), random.random(), random.random(), 1.0)
            sphere_col.objects.link(new_sphere)
            sphere_objs.append(new_sphere)
            
    elif current_count > target_count:
        for _ in range(current_count - target_count):
            obj_to_remove = sphere_objs.pop()
            mesh = obj_to_remove.data
            bpy.data.objects.remove(obj_to_remove, do_unlink=True)
            if mesh: bpy.data.meshes.remove(mesh, do_unlink=True)
                
    if "group_color" not in clock_obj:
        clock_obj["group_color"] = (random.random(), random.random(), random.random(), 1.0)

    # --------------------------------------------------------------------------
    # 2.5 先端トーラスの生成と数合わせ
    # --------------------------------------------------------------------------
    end_torus_objs = [obj for obj in bpy.data.objects if obj.get("is_light_clock_end_torus") == 1]
    target_end_torus_count = base_count if (props.use_spheres and props.use_gather and props.show_end_torus) else 0
    
    end_torus_col = get_or_create_collection(END_TORUS_COLLECTION_NAME, main_col)
    
    if target_end_torus_count > 0:
        end_torus_mesh = bpy.data.meshes.get("先端トーラス_Mesh")
        if not end_torus_mesh:
            end_torus_mesh = bpy.data.meshes.new("先端トーラス_Mesh")
            
        bm_t = bmesh.new()
        create_torus_bmesh(bm_t, major_segments=32, minor_segments=12, 
                           major_radius=props.end_torus_major_radius, 
                           minor_radius=props.end_torus_minor_radius)
        bm_t.to_mesh(end_torus_mesh)
        bm_t.free()

    current_t_count = len(end_torus_objs)
    if current_t_count < target_end_torus_count:
        # 必要な場合、作成した同一メッシュを共有して生成
        end_torus_mesh = bpy.data.meshes.get("先端トーラス_Mesh")
        for _ in range(target_end_torus_count - current_t_count):
            new_torus = bpy.data.objects.new("先端トーラス_編集中", end_torus_mesh)
            new_torus["is_light_clock_end_torus"] = 1
            end_torus_col.objects.link(new_torus)
            end_torus_objs.append(new_torus)
    elif current_t_count > target_end_torus_count:
        for _ in range(current_t_count - target_end_torus_count):
            obj_to_remove = end_torus_objs.pop()
            bpy.data.objects.remove(obj_to_remove, do_unlink=True)
            
    # --------------------------------------------------------------------------
    # 3. 球体と先端トーラスの配置・色設定
    # --------------------------------------------------------------------------
    base_data = [] 
    
    for i in range(base_count):
        sphere = sphere_objs[i]
        
        if props.base_shape == 'CYLINDER':
            ratio = i / (base_count - 1) if base_count > 1 else 0.5
            world_pos = start_pos.lerp(end_pos, ratio)
            t_size_ratio = ratio
        else: # TORUS
            ratio = i / base_count
            theta = ratio * 2 * math.pi
            
            x = math.cos(theta) * props.torus_major_radius
            y = math.sin(theta) * props.torus_major_radius
            local_pos = mathutils.Vector((x, y, 0.0))
            
            t_loc = mathutils.Vector((props.torus_loc_x, props.torus_loc_y, props.torus_loc_z))
            t_rot = mathutils.Euler((props.torus_rot_x, props.torus_rot_y, props.torus_rot_z), 'XYZ').to_matrix()
            world_pos = t_loc + t_rot @ local_pos
            
            t_size_ratio = i / max(1, base_count - 1) if base_count > 1 else 0.0
            
        sphere.location = world_pos
        
        target_ratio = props.sphere_size_target / 10.0
        t_size = min(t_size_ratio / target_ratio, 1.0) if target_ratio > 0 else 1.0
        
        scale_factor = 1.0
        if props.sphere_size_pattern == 'DECREASE': scale_factor = 1.0 - 0.8 * t_size
        elif props.sphere_size_pattern == 'INCREASE': scale_factor = 0.2 + 0.8 * t_size
        final_radius = props.sphere_base_radius * scale_factor
        sphere.scale = (final_radius, final_radius, final_radius)
        
        c = (1,1,1,1)
        if props.sphere_color_type == 'CYLINDER_COLOR': c = props.color
        elif props.sphere_color_type == 'CUSTOM_COLOR': c = props.sphere_custom_color
        elif props.sphere_color_type == 'GROUP_RANDOM': c = clock_obj["group_color"]
        elif props.sphere_color_type == 'INDIVIDUAL_RANDOM': c = sphere["individual_color"]
            
        sp_mat = get_color_material(c, prefix="Mat_Sphere_")
        if sphere.data.materials: sphere.data.materials[0] = sp_mat
        else: sphere.data.materials.append(sp_mat)
            
        base_data.append({'pos': world_pos, 'radius': final_radius, 'color': c})

    # --- 軌跡の配置 ---
    if props.use_spheres and props.use_gather:
        gather_pos = mathutils.Vector((props.gather_x, props.gather_y, props.gather_z))
        
        for i, data in enumerate(base_data):
            
            dir_vec = mathutils.Vector((0, 0, 1))
            diff = gather_pos - data['pos']
            if diff.length > 0.0001:
                dir_vec = diff.normalized()

            for j in range(gather_add):
                traj_idx = base_count + (i * gather_add) + j
                traj_sphere = sphere_objs[traj_idx]
                
                if props.gather_mode == 'REACH':
                    t = (j + 1) / gather_add
                    traj_sphere.location = data['pos'].lerp(gather_pos, t)
                else:
                    dist = float(j + 1) * props.gather_interval
                    traj_sphere.location = data['pos'] + dir_vec * dist
                    t = (j + 1) / gather_add
                
                g_scale = 1.0
                if props.gather_size_pattern == 'DECREASE': g_scale = 1.0 - 0.8 * t 
                elif props.gather_size_pattern == 'INCREASE': g_scale = 1.0 + 2.0 * t 
                tr_radius = data['radius'] * g_scale
                traj_sphere.scale = (tr_radius, tr_radius, tr_radius)
                
                tr_mat = get_color_material(data['color'], prefix="Mat_Sphere_")
                if traj_sphere.data.materials: traj_sphere.data.materials[0] = tr_mat
                else: traj_sphere.data.materials.append(tr_mat)
                
                # --- 先端トーラスの配置 ---
                if props.show_end_torus and j == gather_add - 1:
                    t_obj = end_torus_objs[i]
                    t_obj.location = traj_sphere.location
                    
                    # 軌跡の進行方向に対して垂直(Z軸が進行方向)に向かせる
                    t_obj.rotation_euler = dir_vec.to_track_quat('Z', 'Y').to_euler()
                    
                    t_mat = get_color_material(data['color'], prefix="Mat_EndTorus_")
                    if t_obj.data.materials: t_obj.data.materials[0] = t_mat
                    else: t_obj.data.materials.append(t_mat)

    cleanup_unused_materials()

# ==============================================================================
# Properties
# ==============================================================================
class LightClockProperties(PropertyGroup):
    base_shape: bpy.props.EnumProperty(
        name="ベース形状", items=[('CYLINDER', "円柱", ""), ('TORUS', "トーラス", "")],
        default='CYLINDER', update=update_light_clock
    )
    color: bpy.props.FloatVectorProperty(name="図形の色", subtype='COLOR', size=4, default=(1.0, 0.0, 0.0, 1.0), min=0.0, max=1.0, update=update_light_clock)
    
    thickness: bpy.props.FloatProperty(name="太さ", default=1.0, min=0.01, update=update_light_clock)
    start_x: bpy.props.FloatProperty(name="X", default=-15.0, update=update_light_clock)
    start_y: bpy.props.FloatProperty(name="Y", default=0.0, update=update_light_clock)
    start_z: bpy.props.FloatProperty(name="Z", default=0.0, update=update_light_clock)
    end_x: bpy.props.FloatProperty(name="X", default=15.0, update=update_light_clock)
    end_y: bpy.props.FloatProperty(name="Y", default=0.0, update=update_light_clock)
    end_z: bpy.props.FloatProperty(name="Z", default=0.0, update=update_light_clock)

    torus_major_radius: bpy.props.FloatProperty(name="大半径", default=10.0, min=0.01, update=update_light_clock)
    torus_minor_radius: bpy.props.FloatProperty(name="小半径", default=0.5, min=0.01, update=update_light_clock)
    
    torus_loc_x: bpy.props.FloatProperty(name="X", default=0.0, update=update_light_clock)
    torus_loc_y: bpy.props.FloatProperty(name="Y", default=0.0, update=update_light_clock)
    torus_loc_z: bpy.props.FloatProperty(name="Z", default=0.0, update=update_light_clock)
    
    torus_rot_x: bpy.props.FloatProperty(name="X", default=0.0, subtype='ANGLE', update=update_light_clock)
    torus_rot_y: bpy.props.FloatProperty(name="Y", default=0.0, subtype='ANGLE', update=update_light_clock)
    torus_rot_z: bpy.props.FloatProperty(name="Z", default=0.0, subtype='ANGLE', update=update_light_clock)

    use_spheres: bpy.props.BoolProperty(name="球体マーカーを配置する", default=True, update=update_light_clock)
    sphere_count: bpy.props.IntProperty(name="配置数", default=11, min=1, max=36, update=update_light_clock)
    
    sphere_color_type: bpy.props.EnumProperty(
        name="色の種類",
        items=[('CYLINDER_COLOR', "図形と同じ色", ""), ('CUSTOM_COLOR', "指定色", ""),
               ('GROUP_RANDOM', "グループランダム", ""), ('INDIVIDUAL_RANDOM', "完全個別ランダム", "")],
        default='CYLINDER_COLOR', update=update_light_clock
    )
    sphere_custom_color: bpy.props.FloatVectorProperty(name="球体指定色", subtype='COLOR', size=4, default=(0.0, 1.0, 0.0, 1.0), min=0.0, max=1.0, update=update_light_clock)
    sphere_base_radius: bpy.props.FloatProperty(name="基本サイズ", default=1.5, min=0.01, update=update_light_clock)
    sphere_size_target: bpy.props.IntProperty(name="変化ターゲット位置", default=10, min=0, max=10, update=update_light_clock)
    sphere_size_pattern: bpy.props.EnumProperty(
        name="サイズ変化", items=[('CONSTANT', "変わらない", ""), ('DECREASE', "指定位置へ小さくなる", ""), ('INCREASE', "指定位置へ大きくなる", "")],
        default='CONSTANT', update=update_light_clock
    )

    use_gather: bpy.props.BoolProperty(name="軌跡を生成", default=False, update=update_light_clock)
    gather_mode: bpy.props.EnumProperty(
        name="軌跡の配置モード",
        items=[('REACH', "目標座標へ到達させる", ""), ('DIRECTION', "目標の方向へ等間隔で並べる", "")],
        default='DIRECTION', update=update_light_clock
    )
    gather_interval: bpy.props.FloatProperty(name="等間隔の距離", default=3.0, min=0.01, update=update_light_clock)
    
    gather_x: bpy.props.FloatProperty(name="X", default=0.0, update=update_light_clock)
    gather_y: bpy.props.FloatProperty(name="Y", default=0.0, update=update_light_clock)
    gather_z: bpy.props.FloatProperty(name="Z", default=0.0, update=update_light_clock)
    
    gather_count: bpy.props.IntProperty(name="追加配置数 (1ルート)", default=10, min=1, max=100, update=update_light_clock)
    gather_size_pattern: bpy.props.EnumProperty(
        name="軌跡サイズ変化", items=[('CONSTANT', "変わらない", ""), ('DECREASE', "先端へ行くほど小さくなる", ""), ('INCREASE', "先端へ行くほど大きくなる", "")],
        default='CONSTANT', update=update_light_clock
    )
    
    # --- 先端トーラス ---
    show_end_torus: bpy.props.BoolProperty(name="軌跡の先端にトーラスを表示", default=False, update=update_light_clock)
    end_torus_major_radius: bpy.props.FloatProperty(name="大半径", default=2.0, min=0.01, update=update_light_clock)
    end_torus_minor_radius: bpy.props.FloatProperty(name="小半径", default=0.2, min=0.01, update=update_light_clock)

# ==============================================================================
# Operators & Panels
# ==============================================================================
class FocusObjectAndSetViewOperator(Operator):
    bl_idname = f"{PREFIX}.focus_object_and_set_view"
    bl_label = "選択オブジェクトを中央に (視線-Y)"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        obj = context.active_object
        if not obj: return {'CANCELLED'}
        rv3d = None
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                rv3d = area.spaces.active.region_3d
                break
        if not rv3d: return {'CANCELLED'}
        rv3d.view_location = obj.location
        rv3d.view_rotation = mathutils.Euler((math.radians(90), 0.0, 0.0), 'XYZ').to_quaternion()
        rv3d.view_perspective = 'PERSP'
        return {'FINISHED'}

class CreateLightClockOperator(Operator):
    bl_idname = f"{PREFIX}.create_light_clock"
    bl_label = "図形を生成"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        if get_active_light_clock(context): getattr(bpy.ops, PREFIX).detach_light_clock()
        main_col = get_or_create_collection(MAIN_COLLECTION_NAME, context.scene.collection)
        base_col = get_or_create_collection(BASE_SHAPE_COLLECTION_NAME, main_col)
        
        mesh = bpy.data.meshes.new(f"{BASE_OBJ_NAME}_Mesh")
        clock_obj = bpy.data.objects.new(f"{BASE_OBJ_NAME}_編集中", mesh)
        clock_obj["is_light_clock"] = 1 
        base_col.objects.link(clock_obj)
        
        bpy.ops.object.select_all(action='DESELECT')
        clock_obj.select_set(True)
        context.view_layer.objects.active = clock_obj
        update_light_clock(None, context)
        return {'FINISHED'}

class DetachLightClockOperator(Operator):
    bl_idname = f"{PREFIX}.detach_light_clock"
    bl_label = "アドオンから切り離し"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        clock_obj = get_active_light_clock(context)
        if not clock_obj: return {'CANCELLED'}
        
        del clock_obj["is_light_clock"]
        if "group_color" in clock_obj: del clock_obj["group_color"]
        clock_obj.name = clock_obj.name.replace("_編集中", "")
        
        sphere_objs = [obj for obj in bpy.data.objects if obj.get("is_light_clock_sphere") == 1]
        for s in sphere_objs:
            del s["is_light_clock_sphere"]
            if "individual_color" in s: del s["individual_color"]
            s.name = s.name.replace("_編集中", "")
            
        end_torus_objs = [obj for obj in bpy.data.objects if obj.get("is_light_clock_end_torus") == 1]
        for t in end_torus_objs:
            del t["is_light_clock_end_torus"]
            t.name = t.name.replace("_編集中", "")
            
        return {'FINISHED'}

class RemoveAllPanels(Operator):
    bl_idname = f"{PREFIX}.remove_all_panels"
    bl_label = "アドオン削除"
    def execute(self, context):
        unregister()
        return {'FINISHED'}

class OpenURLOperator(Operator):
    bl_idname = f"{PREFIX}.open_url"
    bl_label = "URLを開く"
    url: bpy.props.StringProperty(default="")
    def execute(self, context):
        import webbrowser
        webbrowser.open(self.url)
        return {'FINISHED'}

# ==============================================================================
# Panels
# ==============================================================================
class BasePanel(Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY_NAME

class VIEW3D_PT_LightClockPanel(BasePanel):
    bl_label = "軌跡生成 (円柱 / トーラス)"
    bl_idname = f"{PREFIX}_PT_main_panel"
    bl_order = 1
    
    def draw(self, context):
        layout = self.layout
        props = context.scene.light_clock_props
        
        layout.operator(f"{PREFIX}.focus_object_and_set_view", icon='VIEW_CAMERA')
        layout.separator()
        
        box_pos = layout.box()
        box_pos.prop(props, "base_shape")
        box_pos.separator()
        
        if props.base_shape == 'CYLINDER':
            box_pos.label(text="円柱の座標:", icon='MESH_CYLINDER')
            col = box_pos.column(align=True)
            row1 = col.row(align=True); row1.label(text="出発点:")
            row1.prop(props, "start_x"); row1.prop(props, "start_y"); row1.prop(props, "start_z")
            row2 = col.row(align=True); row2.label(text="指定位置:")
            row2.prop(props, "end_x"); row2.prop(props, "end_y"); row2.prop(props, "end_z")
            box_pos.separator()
            box_pos.prop(props, "thickness")
        else:
            box_pos.label(text="トーラス設定 (XY平面ベース):", icon='MESH_TORUS')
            col = box_pos.column(align=True)
            row_rad = col.row(align=True)
            row_rad.prop(props, "torus_major_radius")
            row_rad.prop(props, "torus_minor_radius")
            col.separator()
            col.label(text="移動 (中心位置):")
            row_loc = col.row(align=True)
            row_loc.prop(props, "torus_loc_x"); row_loc.prop(props, "torus_loc_y"); row_loc.prop(props, "torus_loc_z")
            col.label(text="回転 (XYZ):")
            row_rot = col.row(align=True)
            row_rot.prop(props, "torus_rot_x"); row_rot.prop(props, "torus_rot_y"); row_rot.prop(props, "torus_rot_z")

        box_pos.separator()
        box_pos.prop(props, "color")
        
        box_sp = layout.box()
        box_sp.prop(props, "use_spheres", icon='SPHERE')
        if props.use_spheres:
            box_sp.prop(props, "sphere_count", slider=True)
            box_sp.prop(props, "sphere_color_type")
            if props.sphere_color_type == 'CUSTOM_COLOR': box_sp.prop(props, "sphere_custom_color")
            box_sp.separator()
            box_sp.prop(props, "sphere_base_radius")
            box_sp.prop(props, "sphere_size_pattern")
            if props.sphere_size_pattern != 'CONSTANT': box_sp.prop(props, "sphere_size_target", slider=True)
            
            box_gather = layout.box()
            box_gather.prop(props, "use_gather", icon='TRACKING')
            if props.use_gather:
                col_g = box_gather.column(align=True)
                col_g.prop(props, "gather_mode")
                col_g.separator()
                
                if props.gather_mode == 'REACH':
                    col_g.label(text="到達させる座標 (集合点):")
                else:
                    col_g.label(text="方向指示点の座標:")
                    
                row_g = col_g.row(align=True)
                row_g.prop(props, "gather_x")
                row_g.prop(props, "gather_y")
                row_g.prop(props, "gather_z")
                
                if props.gather_mode == 'DIRECTION':
                    col_g.separator()
                    col_g.prop(props, "gather_interval")
                
                box_gather.separator()
                box_gather.prop(props, "gather_count", slider=True)
                box_gather.prop(props, "gather_size_pattern")
                
                # --- 先端トーラスUI ---
                box_end = box_gather.box()
                box_end.prop(props, "show_end_torus", icon='MESH_TORUS')
                if props.show_end_torus:
                    col_e = box_end.column(align=True)
                    row_e = col_e.row(align=True)
                    row_e.prop(props, "end_torus_major_radius")
                    row_e.prop(props, "end_torus_minor_radius")
        
        layout.separator()
        row = layout.row(align=True)
        row.scale_y = 1.2
        row.operator(f"{PREFIX}.create_light_clock", text="図形作成", icon='TIME')
        row.operator(f"{PREFIX}.detach_light_clock", text="切り離し (独立)", icon='UNLINKED')

class VIEW3D_PT_LinkPanel(BasePanel):
    bl_label = "リンク"
    bl_idname = f"{PREFIX}_PT_link_panel"
    bl_order = 2
    def draw(self, context):
        for link in ADDON_LINKS:
            op = self.layout.operator(f"{PREFIX}.open_url", text=link["label"], icon=link.get("icon", "URL"))
            op.url = link["url"]

class VIEW3D_PT_RemovePanel(BasePanel):
    bl_label = "アドオン削除"
    bl_idname = f"{PREFIX}_PT_remove"
    bl_order = 3
    bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        self.layout.operator(f"{PREFIX}.remove_all_panels", text="アドオン削除", icon='CANCEL')

# ==============================================================================
# Registration
# ==============================================================================
classes = [
    LightClockProperties, FocusObjectAndSetViewOperator, CreateLightClockOperator,
    DetachLightClockOperator, RemoveAllPanels, OpenURLOperator,
    VIEW3D_PT_LightClockPanel, VIEW3D_PT_LinkPanel, VIEW3D_PT_RemovePanel
]
def register():
    for cls in classes: bpy.utils.register_class(cls)
    bpy.types.Scene.light_clock_props = bpy.props.PointerProperty(type=LightClockProperties)
def unregister():
    for cls in reversed(classes):
        try: bpy.utils.unregister_class(cls)
        except: pass
    if hasattr(bpy.types.Scene, "light_clock_props"): del bpy.types.Scene.light_clock_props
if __name__ == "__main__": register()