Sidebar > Crystal Torus", "description": "初期値反映版: トーラスと球体マーカーの個別表示/非表示機能付き", "category": "3D View", } # ========================================================= # 2. コード作成日時 # ========================================================= # Code Created : 2026_0723_1920 # ========================================================= # 3. import # ========================================================= import bpy import bmesh import math import mathutils import webbrowser # ========================================================= # 4. 共通定義 # ========================================================= ADDON_TITLE = "Crystal Torus System" PREFIX_VAL = "CRYSTAL_TORUS" PREFIX = PREFIX_VAL"> Sidebar > Crystal Torus", "description": "初期値反映版: トーラスと球体マーカーの個別表示/非表示機能付き", "category": "3D View", } # ========================================================= # 2. コード作成日時 # ========================================================= # Code Created : 2026_0723_1920 # ========================================================= # 3. import # ========================================================= import bpy import bmesh import math import mathutils import webbrowser # ========================================================= # 4. 共通定義 # ========================================================= ADDON_TITLE = "Crystal Torus System" PREFIX_VAL = "CRYSTAL_TORUS" PREFIX = PREFIX_VAL"> Sidebar > Crystal Torus", "description": "初期値反映版: トーラスと球体マーカーの個別表示/非表示機能付き", "category": "3D View", } # ========================================================= # 2. コード作成日時 # ========================================================= # Code Created : 2026_0723_1920 # ========================================================= # 3. import # ========================================================= import bpy import bmesh import math import mathutils import webbrowser # ========================================================= # 4. 共通定義 # ========================================================= ADDON_TITLE = "Crystal Torus System" PREFIX_VAL = "CRYSTAL_TORUS" PREFIX = PREFIX_VAL">













# =========================================================
# 1. bl_info
# =========================================================
bl_info = {
    "name": "Crystal Torus & Center Markers (Ver 1.8)",
    "author": "ChatGPT",
    "version": (1, 8, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > Crystal Torus",
    "description": "初期値反映版: トーラスと球体マーカーの個別表示/非表示機能付き",
    "category": "3D View",
}

# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0723_1920

# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import mathutils
import webbrowser

# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "Crystal Torus System"
PREFIX_VAL = "CRYSTAL_TORUS"
PREFIX = PREFIX_VAL.lower()
ADDON_CATEGORY = "Crystal Torus"

# =========================================================
# 5. リンク定義
# =========================================================
LINK_NAME_1 = "専用マニュアル"
LINK_URL_1 = "<https://example.com/manual>"
LINK_NAME_2 = "コンテナ builder"
LINK_URL_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"
LINK_NAME_3 = "posfie"
LINK_URL_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_4 = "zionadchat"
LINK_URL_4 = "<https://x.com/zionadchat>"
LINK_NAME_5 = ""
LINK_URL_5 = ""

# =========================================================
# 6. 初期設定
# =========================================================
SCALE_FACTOR_DEFAULT = 1.0

CRYSTAL_MAJOR_DEFAULT = 0.10
CRYSTAL_MINOR_DEFAULT = 0.01
CRYSTAL_LOC_DEFAULT = (0.0, 0.0, 0.0)
COLOR_CRYSTAL_DEFAULT = (0.6, 0.9, 1.0, 0.5) 
CRYSTAL_SHOW_TORUS = True
CRYSTAL_SHOW_CENTER = True
CRYSTAL_CENTER_RADIUS_DEFAULT = 0.02
COLOR_CRYSTAL_CENTER_DEFAULT = (0.4, 0.5, 1.0, 1.0) 

RETINA_MAJOR_DEFAULT = 1.00
RETINA_MINOR_DEFAULT = 0.01
RETINA_LOC_DEFAULT = (1.10, 0.0, 0.0)
COLOR_RETINA_DEFAULT = (1.0, 0.2, 0.5, 1.0)  
RETINA_SHOW_TORUS = True
RETINA_SHOW_CENTER = True
RETINA_CENTER_RADIUS_DEFAULT = 0.20
COLOR_RETINA_CENTER_DEFAULT = (1.0, 0.3, 0.5, 1.0) 

SURFACE_MAJOR_DEFAULT = 1.00
SURFACE_MINOR_DEFAULT = 0.01
COLOR_SURFACE_DEFAULT = (0.5, 1.0, 0.5, 0.5)  
SURFACE_SHOW_TORUS = True
SURFACE_SHOW_CENTER = True
SURFACE_CENTER_RADIUS_DEFAULT = 0.02
COLOR_SURFACE_CENTER_DEFAULT = (0.8, 0.0, 1.0, 1.0) 

GAMMA_MAJOR_DEFAULT = 10.00
GAMMA_MINOR_DEFAULT = 0.05
GAMMA_LOC_DEFAULT = (0.0, 0.0, 0.0)
COLOR_GAMMA_DEFAULT = (0.0, 0.0, 1.0, 0.5)    
GAMMA_SHOW_TORUS = True
GAMMA_SHOW_CENTER = True
GAMMA_CENTER_RADIUS_DEFAULT = 0.00
COLOR_GAMMA_CENTER_DEFAULT = (0.2, 0.4, 1.0, 1.0) 

# =========================================================
# 7. 共通クラス
# =========================================================
class CRYSTAL_OT_OpenUrl(bpy.types.Operator):
    bl_idname = f"{PREFIX}.open_url"
    bl_label = "リンクを開く"
    url: bpy.props.StringProperty()
    def execute(self, context):
        if self.url: webbrowser.open(self.url)
        return {'FINISHED'}

class CRYSTAL_OT_RemoveAddon(bpy.types.Operator):
    bl_idname = f"{PREFIX}.remove_addon"
    bl_label = "Unregister Addon"
    bl_description = "このアドオンを無効化し、UIから消去します"
    def execute(self, context):
        bpy.app.timers.register(unregister, first_interval=0.01)
        self.report({'INFO'}, f"{ADDON_TITLE} を UI から消去しました")
        return {'FINISHED'}

# =========================================================
# 8. アドオン固有機能 (リアルタイム・生成・切り離し)
# =========================================================
def create_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name=name)
    mat.use_nodes = True
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    if bsdf:
        bsdf.inputs["Base Color"].default_value = color
        if color[3] < 1.0:
            bsdf.inputs["Alpha"].default_value = color[3]
            mat.blend_method = 'BLEND' if bpy.app.version < (4, 2, 0) else mat.blend_method
        else:
            bsdf.inputs["Alpha"].default_value = 1.0
    return mat

def apply_torus_bmesh(obj, major, minor):
    bm = bmesh.new()
    scaled_major = major * SCALE_FACTOR_DEFAULT
    scaled_minor = minor * SCALE_FACTOR_DEFAULT
    if scaled_major <= 0.0001: scaled_major = 0.0001
    if scaled_minor <= 0.0001: scaled_minor = 0.0001
    
    segments, rings = 48, 24     
    verts = []
    
    for i in range(segments):
        theta = 2.0 * math.pi * i / segments
        cos_theta = math.cos(theta)
        sin_theta = math.sin(theta)
        row = []
        for j in range(rings):
            phi = 2.0 * math.pi * j / rings
            x = (scaled_major + scaled_minor * math.cos(phi)) * cos_theta
            z = (scaled_major + scaled_minor * math.cos(phi)) * sin_theta
            y = scaled_minor * math.sin(phi)
            row.append(bm.verts.new((x, y, z)))
        verts.append(row)
        
    for i in range(segments):
        next_i = (i + 1) % segments
        for j in range(rings):
            next_j = (j + 1) % rings
            f = bm.faces.new((verts[i][j], verts[i][next_j], verts[next_i][next_j], verts[next_i][j]))
            f.smooth = True 

    bm.to_mesh(obj.data)
    bm.free()

def apply_center_bmesh(obj, radius):
    bm = bmesh.new()
    scaled_radius = radius * SCALE_FACTOR_DEFAULT
    if scaled_radius > 0.0001:
        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=scaled_radius)
        for f in bm.faces: f.smooth = True
    bm.to_mesh(obj.data)
    bm.free()

def update_torus(obj_name, loc, major, minor, color, show):
    obj = bpy.data.objects.get(obj_name)
    if not obj: return 
    
    obj.hide_viewport = not show
    obj.hide_render = not show
    
    if show:
        apply_torus_bmesh(obj, major, minor)
        obj.location = (loc[0] * SCALE_FACTOR_DEFAULT, loc[1] * SCALE_FACTOR_DEFAULT, loc[2] * SCALE_FACTOR_DEFAULT)
        mat = create_material(f"Mat_{obj_name}", color)
        if not obj.data.materials: obj.data.materials.append(mat)
        else: obj.data.materials[0] = mat

def update_center(obj_name, loc, radius, color, show):
    obj = bpy.data.objects.get(obj_name)
    if not obj: return
    
    obj.hide_viewport = not show
    obj.hide_render = not show
    
    if show:
        apply_center_bmesh(obj, radius)
        obj.location = (loc[0] * SCALE_FACTOR_DEFAULT, loc[1] * SCALE_FACTOR_DEFAULT, loc[2] * SCALE_FACTOR_DEFAULT)
        mat = create_material(f"Mat_{obj_name}", color)
        if not obj.data.materials: obj.data.materials.append(mat)
        else: obj.data.materials[0] = mat

def get_surface_loc(props):
    C = mathutils.Vector(props.cr_crystal_loc)
    R = mathutils.Vector(props.cr_retina_loc)
    r = props.cr_crystal_major
    V = R - C
    dist = V.length
    if dist > 0.0001:
        return C + (V / dist) * r
    return C.copy()

# --- コールバック ---
def cb_update_crystal(self, context):
    props = context.scene
    update_torus("Crystal_Torus", props.cr_crystal_loc, props.cr_crystal_major, props.cr_crystal_minor, props.cr_crystal_color, props.cr_crystal_show_torus)
    update_center("Crystal_Center", props.cr_crystal_loc, props.cr_crystal_center_radius, props.cr_crystal_center_color, props.cr_crystal_show_center)
    # 表面交点の追従更新
    P = get_surface_loc(props)
    update_torus("Surface_Torus", P, props.cr_surface_major, props.cr_surface_minor, props.cr_surface_color, props.cr_surface_show_torus)
    update_center("Surface_Center", P, props.cr_surface_center_radius, props.cr_surface_center_color, props.cr_surface_show_center)

def cb_update_retina(self, context):
    props = context.scene
    update_torus("Retina_Torus", props.cr_retina_loc, props.cr_retina_major, props.cr_retina_minor, props.cr_retina_color, props.cr_retina_show_torus)
    update_center("Retina_Center", props.cr_retina_loc, props.cr_retina_center_radius, props.cr_retina_center_color, props.cr_retina_show_center)
    # 表面交点の追従更新
    P = get_surface_loc(props)
    update_torus("Surface_Torus", P, props.cr_surface_major, props.cr_surface_minor, props.cr_surface_color, props.cr_surface_show_torus)
    update_center("Surface_Center", P, props.cr_surface_center_radius, props.cr_surface_center_color, props.cr_surface_show_center)

def cb_update_surface(self, context):
    props = context.scene
    P = get_surface_loc(props)
    update_torus("Surface_Torus", P, props.cr_surface_major, props.cr_surface_minor, props.cr_surface_color, props.cr_surface_show_torus)
    update_center("Surface_Center", P, props.cr_surface_center_radius, props.cr_surface_center_color, props.cr_surface_show_center)

def cb_update_gamma(self, context):
    props = context.scene
    update_torus("Gamma_Ref_Torus", props.cr_gamma_loc, props.cr_gamma_major, props.cr_gamma_minor, props.cr_gamma_color, props.cr_gamma_show_torus)
    update_center("Gamma_Ref_Center", props.cr_gamma_loc, props.cr_gamma_center_radius, props.cr_gamma_center_color, props.cr_gamma_show_center)

# --- 生成 ---
def create_base_obj(name, col):
    existing = bpy.data.objects.get(name)
    if existing:
        mesh = existing.data
        bpy.data.objects.remove(existing)
        if mesh.users == 0: bpy.data.meshes.remove(mesh)
    mesh = bpy.data.meshes.new(f"Mesh_{name}")
    obj = bpy.data.objects.new(name, mesh)
    col.objects.link(obj)
    return obj

class CRYSTAL_OT_generate(bpy.types.Operator):
    bl_idname = f"{PREFIX}.generate"
    bl_label = "4つのトーラスと中心マーカーを生成"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        col = bpy.data.collections.get(ADDON_TITLE)
        if not col:
            col = bpy.data.collections.new(ADDON_TITLE)
            context.scene.collection.children.link(col)
            
        for name in ["Crystal_Torus", "Crystal_Center", "Retina_Torus", "Retina_Center", 
                     "Surface_Torus", "Surface_Center", "Gamma_Ref_Torus", "Gamma_Ref_Center"]:
            create_base_obj(name, col)

        cb_update_crystal(context.scene, context)
        cb_update_retina(context.scene, context)
        cb_update_surface(context.scene, context)
        cb_update_gamma(context.scene, context)

        self.report({'INFO'}, "要素の生成が完了しました")
        return {'FINISHED'}

class CRYSTAL_OT_detach(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach"
    bl_label = "切り離し (独立化して確定)"
    def execute(self, context):
        names = ["Crystal_Torus", "Crystal_Center", "Retina_Torus", "Retina_Center", 
                 "Surface_Torus", "Surface_Center", "Gamma_Ref_Torus", "Gamma_Ref_Center"]
        count = 0
        for name in names:
            obj = bpy.data.objects.get(name)
            if obj:
                new_name = f"{name}_Baked"
                obj.name = new_name
                obj.data.name = f"Mesh_{new_name}"
                if obj.data.materials:
                    new_mat = obj.data.materials[0].copy()
                    new_mat.name = f"Mat_{new_name}"
                    obj.data.materials[0] = new_mat
                count += 1
                
        if count > 0: self.report({'INFO'}, "現在の状態を切り離して確定しました")
        return {'FINISHED'}

class CRYSTAL_OT_copy_info(bpy.types.Operator):
    bl_idname = f"{PREFIX}.copy_info"
    bl_label = "情報をクリップボードにコピー"
    def execute(self, context):
        props = context.scene
        P = get_surface_loc(props)
        info_text = (
            f"【1.水晶玉】\n  中心: ({props.cr_crystal_loc[0]:.2f}, {props.cr_crystal_loc[1]:.2f}, {props.cr_crystal_loc[2]:.2f})\n"
            f"  半径: {props.cr_crystal_major:.2f} / 太さ: {props.cr_crystal_minor:.2f}\n\n"
            f"【2.網膜点】\n  中心: ({props.cr_retina_loc[0]:.2f}, {props.cr_retina_loc[1]:.2f}, {props.cr_retina_loc[2]:.2f})\n"
            f"  半径: {props.cr_retina_major:.2f} / 太さ: {props.cr_retina_minor:.2f}\n\n"
            f"【3.表面交点】\n  中心: ({P.x:.2f}, {P.y:.2f}, {P.z:.2f})\n"
            f"  半径: {props.cr_surface_major:.2f} / 太さ: {props.cr_surface_minor:.2f}\n\n"
            f"【4.ガンマ基準点】\n  中心: ({props.cr_gamma_loc[0]:.2f}, {props.cr_gamma_loc[1]:.2f}, {props.cr_gamma_loc[2]:.2f})\n"
            f"  半径: {props.cr_gamma_major:.2f} / 太さ: {props.cr_gamma_minor:.2f}\n"
        )
        context.window_manager.clipboard = info_text
        self.report({'INFO'}, "パラメータ情報をコピーしました")
        return {'FINISHED'}

def setup_workspace():
    for window in bpy.context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        space.shading.type = 'MATERIAL'
                        space.show_region_ui = True
    return None

# =========================================================
# 9. パネル構成
# =========================================================
def draw_torus_box(layout, props, title, icon, p_loc, p_major, p_minor, p_color, p_show_torus, p_show_center, p_radius, p_center_color, is_surface=False):
    box = layout.box()
    
    # トーラス自体の表示トグルとタイトル
    row = box.row()
    row.prop(props, p_show_torus, text="", icon='HIDE_OFF' if getattr(props, p_show_torus) else 'HIDE_ON')
    row.label(text=title, icon=icon)
    
    if getattr(props, p_show_torus):
        if is_surface:
            P = get_surface_loc(props)
            box.label(text=f"中心 (自動計算): ({P.x:.3f}, {P.y:.3f}, {P.z:.3f})")
        else:
            box.prop(props, p_loc, text="中心")
            
        row = box.row()
        row.prop(props, p_major, text="半径")
        row.prop(props, p_minor, text="太さ")
        box.prop(props, p_color, text="色")
    
    # センターマーカーの表示トグル
    box.prop(props, p_show_center, text="中心に球体マーカーを表示", toggle=True)
    if getattr(props, p_show_center):
        row = box.row()
        row.prop(props, p_radius, text="球の半径")
        row.prop(props, p_center_color, text="球の色")

class CRYSTAL_PT_main(bpy.types.Panel):
    bl_label = ADDON_TITLE
    bl_idname = f"{PREFIX_VAL}_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        props = context.scene
        layout.label(text="生成後にスライダーでリアルタイム変更", icon='INFO')
        
        draw_torus_box(layout, props, "1. 水晶玉", 'SPHERE', "cr_crystal_loc", "cr_crystal_major", "cr_crystal_minor", "cr_crystal_color", "cr_crystal_show_torus", "cr_crystal_show_center", "cr_crystal_center_radius", "cr_crystal_center_color")
        draw_torus_box(layout, props, "2. 網膜点", 'TRACKING', "cr_retina_loc", "cr_retina_major", "cr_retina_minor", "cr_retina_color", "cr_retina_show_torus", "cr_retina_show_center", "cr_retina_center_radius", "cr_retina_center_color")
        draw_torus_box(layout, props, "3. 表面交点 (線分と球面の交差位置)", 'VERTEXSEL', None, "cr_surface_major", "cr_surface_minor", "cr_surface_color", "cr_surface_show_torus", "cr_surface_show_center", "cr_surface_center_radius", "cr_surface_center_color", is_surface=True)
        draw_torus_box(layout, props, "4. ガンマ基準点", 'EMPTY_DATA', "cr_gamma_loc", "cr_gamma_major", "cr_gamma_minor", "cr_gamma_color", "cr_gamma_show_torus", "cr_gamma_show_center", "cr_gamma_center_radius", "cr_gamma_center_color")

        layout.separator()
        layout.operator(CRYSTAL_OT_generate.bl_idname, icon='PLAY')
        layout.operator(CRYSTAL_OT_detach.bl_idname, icon='UNLINKED')
        layout.operator(CRYSTAL_OT_copy_info.bl_idname, icon='COPYDOWN')

class CRYSTAL_PT_LinksPanel(bpy.types.Panel):
    bl_label = "リンク"
    bl_idname = f"{PREFIX_VAL}_PT_links_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_options = {'DEFAULT_CLOSED'}
    bl_order = 100

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        links = [(LINK_NAME_1, LINK_URL_1, 'HELP'), (LINK_NAME_2, LINK_URL_2, 'URL'), (LINK_NAME_3, LINK_URL_3, 'URL'), (LINK_NAME_4, LINK_URL_4, 'URL')]
        for name, url, icon in links:
            if name and url: box.operator(CRYSTAL_OT_OpenUrl.bl_idname, text=name, icon=icon).url = url

class CRYSTAL_PT_RemovePanel(bpy.types.Panel):
    bl_label = "アドオン削除"
    bl_idname = f"{PREFIX_VAL}_PT_remove_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_options = {'DEFAULT_CLOSED'}
    bl_order = 101

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.operator(CRYSTAL_OT_RemoveAddon.bl_idname, icon='CANCEL')

# =========================================================
# 10. register()
# =========================================================
classes = (
    CRYSTAL_OT_OpenUrl, CRYSTAL_OT_RemoveAddon, CRYSTAL_OT_generate, CRYSTAL_OT_detach, CRYSTAL_OT_copy_info,
    CRYSTAL_PT_main, CRYSTAL_PT_LinksPanel, CRYSTAL_PT_RemovePanel,
)

def register():
    for cls in classes: bpy.utils.register_class(cls)
    Scene = bpy.types.Scene
    
    # 1. 水晶玉
    Scene.cr_crystal_loc = bpy.props.FloatVectorProperty(name="位置", default=CRYSTAL_LOC_DEFAULT, subtype='XYZ', update=cb_update_crystal)
    Scene.cr_crystal_major = bpy.props.FloatProperty(name="半径", default=CRYSTAL_MAJOR_DEFAULT, min=0.0, update=cb_update_crystal)
    Scene.cr_crystal_minor = bpy.props.FloatProperty(name="太さ", default=CRYSTAL_MINOR_DEFAULT, min=0.0, update=cb_update_crystal)
    Scene.cr_crystal_color = bpy.props.FloatVectorProperty(name="色", default=COLOR_CRYSTAL_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_crystal)
    Scene.cr_crystal_show_torus = bpy.props.BoolProperty(name="トーラス表示", default=CRYSTAL_SHOW_TORUS, update=cb_update_crystal)
    Scene.cr_crystal_show_center = bpy.props.BoolProperty(name="マーカー表示", default=CRYSTAL_SHOW_CENTER, update=cb_update_crystal)
    Scene.cr_crystal_center_radius = bpy.props.FloatProperty(name="球半径", default=CRYSTAL_CENTER_RADIUS_DEFAULT, min=0.0, update=cb_update_crystal)
    Scene.cr_crystal_center_color = bpy.props.FloatVectorProperty(name="球色", default=COLOR_CRYSTAL_CENTER_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_crystal)

    # 2. 網膜点
    Scene.cr_retina_loc = bpy.props.FloatVectorProperty(name="位置", default=RETINA_LOC_DEFAULT, subtype='XYZ', update=cb_update_retina)
    Scene.cr_retina_major = bpy.props.FloatProperty(name="半径", default=RETINA_MAJOR_DEFAULT, min=0.0, update=cb_update_retina)
    Scene.cr_retina_minor = bpy.props.FloatProperty(name="太さ", default=RETINA_MINOR_DEFAULT, min=0.0, update=cb_update_retina)
    Scene.cr_retina_color = bpy.props.FloatVectorProperty(name="色", default=COLOR_RETINA_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_retina)
    Scene.cr_retina_show_torus = bpy.props.BoolProperty(name="トーラス表示", default=RETINA_SHOW_TORUS, update=cb_update_retina)
    Scene.cr_retina_show_center = bpy.props.BoolProperty(name="マーカー表示", default=RETINA_SHOW_CENTER, update=cb_update_retina)
    Scene.cr_retina_center_radius = bpy.props.FloatProperty(name="球半径", default=RETINA_CENTER_RADIUS_DEFAULT, min=0.0, update=cb_update_retina)
    Scene.cr_retina_center_color = bpy.props.FloatVectorProperty(name="球色", default=COLOR_RETINA_CENTER_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_retina)

    # 3. 表面交点
    Scene.cr_surface_major = bpy.props.FloatProperty(name="半径", default=SURFACE_MAJOR_DEFAULT, min=0.0, update=cb_update_surface)
    Scene.cr_surface_minor = bpy.props.FloatProperty(name="太さ", default=SURFACE_MINOR_DEFAULT, min=0.0, update=cb_update_surface)
    Scene.cr_surface_color = bpy.props.FloatVectorProperty(name="色", default=COLOR_SURFACE_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_surface)
    Scene.cr_surface_show_torus = bpy.props.BoolProperty(name="トーラス表示", default=SURFACE_SHOW_TORUS, update=cb_update_surface)
    Scene.cr_surface_show_center = bpy.props.BoolProperty(name="マーカー表示", default=SURFACE_SHOW_CENTER, update=cb_update_surface)
    Scene.cr_surface_center_radius = bpy.props.FloatProperty(name="球半径", default=SURFACE_CENTER_RADIUS_DEFAULT, min=0.0, update=cb_update_surface)
    Scene.cr_surface_center_color = bpy.props.FloatVectorProperty(name="球色", default=COLOR_SURFACE_CENTER_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_surface)

    # 4. ガンマ基準点
    Scene.cr_gamma_loc = bpy.props.FloatVectorProperty(name="位置", default=GAMMA_LOC_DEFAULT, subtype='XYZ', update=cb_update_gamma)
    Scene.cr_gamma_major = bpy.props.FloatProperty(name="半径", default=GAMMA_MAJOR_DEFAULT, min=0.0, update=cb_update_gamma)
    Scene.cr_gamma_minor = bpy.props.FloatProperty(name="太さ", default=GAMMA_MINOR_DEFAULT, min=0.0, update=cb_update_gamma)
    Scene.cr_gamma_color = bpy.props.FloatVectorProperty(name="色", default=COLOR_GAMMA_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_gamma)
    Scene.cr_gamma_show_torus = bpy.props.BoolProperty(name="トーラス表示", default=GAMMA_SHOW_TORUS, update=cb_update_gamma)
    Scene.cr_gamma_show_center = bpy.props.BoolProperty(name="マーカー表示", default=GAMMA_SHOW_CENTER, update=cb_update_gamma)
    Scene.cr_gamma_center_radius = bpy.props.FloatProperty(name="球半径", default=GAMMA_CENTER_RADIUS_DEFAULT, min=0.0, update=cb_update_gamma)
    Scene.cr_gamma_center_color = bpy.props.FloatVectorProperty(name="球色", default=COLOR_GAMMA_CENTER_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_gamma)

    bpy.app.timers.register(setup_workspace, first_interval=0.1)

# =========================================================
# 11. unregister()
# =========================================================
def unregister():
    props = [
        "cr_crystal_loc", "cr_crystal_major", "cr_crystal_minor", "cr_crystal_color", "cr_crystal_show_torus", "cr_crystal_show_center", "cr_crystal_center_radius", "cr_crystal_center_color",
        "cr_retina_loc", "cr_retina_major", "cr_retina_minor", "cr_retina_color", "cr_retina_show_torus", "cr_retina_show_center", "cr_retina_center_radius", "cr_retina_center_color",
        "cr_surface_major", "cr_surface_minor", "cr_surface_color", "cr_surface_show_torus", "cr_surface_show_center", "cr_surface_center_radius", "cr_surface_center_color",
        "cr_gamma_loc", "cr_gamma_major", "cr_gamma_minor", "cr_gamma_color", "cr_gamma_show_torus", "cr_gamma_show_center", "cr_gamma_center_radius", "cr_gamma_center_color"
    ]
    for p in props:
        if hasattr(bpy.types.Scene, p): delattr(bpy.types.Scene, p)
    for cls in reversed(classes):
        try: bpy.utils.unregister_class(cls)
        except: pass

if __name__ == "__main__":
    register()
# =========================================================
# 1. bl_info
# =========================================================
bl_info = {
    "name": "Crystal Torus & Center Markers",
    "author": "ChatGPT",
    "version": (1, 7, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > Crystal Torus",
    "description": "表面交点の自動追従、および各トーラス中心の球体マーカーに対応",
    "category": "3D View",
}

# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0723_1610

# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import mathutils
import webbrowser

# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "Crystal Torus System"
PREFIX_VAL = "CRYSTAL_TORUS"
PREFIX = PREFIX_VAL.lower()
ADDON_CATEGORY = "Crystal Torus"

# =========================================================
# 5. リンク定義
# =========================================================
LINK_NAME_1 = "専用マニュアル"
LINK_URL_1 = "<https://example.com/manual>"
LINK_NAME_2 = "コンテナ builder"
LINK_URL_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"
LINK_NAME_3 = "posfie"
LINK_URL_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_4 = "zionadchat"
LINK_URL_4 = "<https://x.com/zionadchat>"
LINK_NAME_5 = ""
LINK_URL_5 = ""

# =========================================================
# 6. 初期設定
# =========================================================
SCALE_FACTOR_DEFAULT = 1.0

CRYSTAL_MAJOR_DEFAULT = 0.10
CRYSTAL_MINOR_DEFAULT = 0.01
CRYSTAL_LOC_DEFAULT = (0.0, 0.0, 0.0)
COLOR_CRYSTAL_DEFAULT = (0.4, 0.8, 1.0, 0.5)

RETINA_MAJOR_DEFAULT = 1.00
RETINA_MINOR_DEFAULT = 0.01
RETINA_LOC_DEFAULT = (1.10, 0.0, 0.0)
COLOR_RETINA_DEFAULT = (1.0, 0.5, 0.5, 1.0)

SURFACE_MAJOR_DEFAULT = 0.05
SURFACE_MINOR_DEFAULT = 0.01
COLOR_SURFACE_DEFAULT = (0.2, 1.0, 0.2, 0.8)

GAMMA_MAJOR_DEFAULT = 1.10
GAMMA_MINOR_DEFAULT = 0.05
GAMMA_LOC_DEFAULT = (0.0, 0.0, 0.0)
COLOR_GAMMA_DEFAULT = (1.0, 0.9, 0.4, 0.5)

CENTER_RADIUS_DEFAULT = 0.02
COLOR_CENTER_DEFAULT = (1.0, 1.0, 1.0, 1.0)

# =========================================================
# 7. 共通クラス
# =========================================================
class CRYSTAL_OT_OpenUrl(bpy.types.Operator):
    bl_idname = f"{PREFIX}.open_url"
    bl_label = "リンクを開く"
    url: bpy.props.StringProperty()
    def execute(self, context):
        if self.url: webbrowser.open(self.url)
        return {'FINISHED'}

class CRYSTAL_OT_RemoveAddon(bpy.types.Operator):
    bl_idname = f"{PREFIX}.remove_addon"
    bl_label = "Unregister Addon"
    bl_description = "このアドオンを無効化し、UIから消去します"
    def execute(self, context):
        bpy.app.timers.register(unregister, first_interval=0.01)
        self.report({'INFO'}, f"{ADDON_TITLE} を UI から消去しました")
        return {'FINISHED'}

# =========================================================
# 8. アドオン固有機能 (リアルタイム・生成・切り離し)
# =========================================================

def create_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name=name)
    mat.use_nodes = True
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    if bsdf:
        bsdf.inputs["Base Color"].default_value = color
        if color[3] < 1.0:
            bsdf.inputs["Alpha"].default_value = color[3]
            mat.blend_method = 'BLEND' if bpy.app.version < (4, 2, 0) else mat.blend_method
        else:
            bsdf.inputs["Alpha"].default_value = 1.0
    return mat

def apply_torus_bmesh(obj, major, minor):
    """ZX平面のトーラスを構築"""
    bm = bmesh.new()
    scaled_major = major * SCALE_FACTOR_DEFAULT
    scaled_minor = minor * SCALE_FACTOR_DEFAULT
    if scaled_major <= 0.0001: scaled_major = 0.0001
    if scaled_minor <= 0.0001: scaled_minor = 0.0001
    
    segments, rings = 48, 24     
    verts = []
    
    for i in range(segments):
        theta = 2.0 * math.pi * i / segments
        cos_theta = math.cos(theta)
        sin_theta = math.sin(theta)
        row = []
        for j in range(rings):
            phi = 2.0 * math.pi * j / rings
            x = (scaled_major + scaled_minor * math.cos(phi)) * cos_theta
            z = (scaled_major + scaled_minor * math.cos(phi)) * sin_theta
            y = scaled_minor * math.sin(phi)
            row.append(bm.verts.new((x, y, z)))
        verts.append(row)
        
    for i in range(segments):
        next_i = (i + 1) % segments
        for j in range(rings):
            next_j = (j + 1) % rings
            f = bm.faces.new((verts[i][j], verts[i][next_j], verts[next_i][next_j], verts[next_i][j]))
            f.smooth = True 

    bm.to_mesh(obj.data)
    bm.free()

def apply_center_bmesh(obj, radius):
    """中心球体のメッシュを構築"""
    bm = bmesh.new()
    scaled_radius = radius * SCALE_FACTOR_DEFAULT
    if scaled_radius > 0.0001:
        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=scaled_radius)
        for f in bm.faces: f.smooth = True
    bm.to_mesh(obj.data)
    bm.free()

def update_torus(obj_name, loc, major, minor, color):
    obj = bpy.data.objects.get(obj_name)
    if not obj: return 
    apply_torus_bmesh(obj, major, minor)
    obj.location = (loc[0] * SCALE_FACTOR_DEFAULT, loc[1] * SCALE_FACTOR_DEFAULT, loc[2] * SCALE_FACTOR_DEFAULT)
    mat = create_material(f"Mat_{obj_name}", color)
    if not obj.data.materials: obj.data.materials.append(mat)
    else: obj.data.materials[0] = mat

def update_center(obj_name, loc, radius, color, show):
    obj = bpy.data.objects.get(obj_name)
    if not obj: return
    obj.hide_viewport = not show
    obj.hide_render = not show
    if show:
        apply_center_bmesh(obj, radius)
        obj.location = (loc[0] * SCALE_FACTOR_DEFAULT, loc[1] * SCALE_FACTOR_DEFAULT, loc[2] * SCALE_FACTOR_DEFAULT)
        mat = create_material(f"Mat_{obj_name}", color)
        if not obj.data.materials: obj.data.materials.append(mat)
        else: obj.data.materials[0] = mat

# 表面交点 (水晶玉中心と網膜点を結ぶ線分と球面の交点) の座標計算
def get_surface_loc(props):
    C = mathutils.Vector(props.cr_crystal_loc)
    R = mathutils.Vector(props.cr_retina_loc)
    r = props.cr_crystal_major
    V = R - C
    dist = V.length
    if dist > 0.0001:
        return C + (V / dist) * r
    return C.copy()

# --- コールバック ---
def cb_update_crystal(self, context):
    props = context.scene
    update_torus("Crystal_Torus", props.cr_crystal_loc, props.cr_crystal_major, props.cr_crystal_minor, props.cr_crystal_color)
    update_center("Crystal_Center", props.cr_crystal_loc, props.cr_crystal_center_radius, props.cr_crystal_center_color, props.cr_crystal_show_center)
    # 表面交点の追従更新
    P = get_surface_loc(props)
    update_torus("Surface_Torus", P, props.cr_surface_major, props.cr_surface_minor, props.cr_surface_color)
    update_center("Surface_Center", P, props.cr_surface_center_radius, props.cr_surface_center_color, props.cr_surface_show_center)

def cb_update_retina(self, context):
    props = context.scene
    update_torus("Retina_Torus", props.cr_retina_loc, props.cr_retina_major, props.cr_retina_minor, props.cr_retina_color)
    update_center("Retina_Center", props.cr_retina_loc, props.cr_retina_center_radius, props.cr_retina_center_color, props.cr_retina_show_center)
    # 表面交点の追従更新
    P = get_surface_loc(props)
    update_torus("Surface_Torus", P, props.cr_surface_major, props.cr_surface_minor, props.cr_surface_color)
    update_center("Surface_Center", P, props.cr_surface_center_radius, props.cr_surface_center_color, props.cr_surface_show_center)

def cb_update_surface(self, context):
    props = context.scene
    P = get_surface_loc(props)
    update_torus("Surface_Torus", P, props.cr_surface_major, props.cr_surface_minor, props.cr_surface_color)
    update_center("Surface_Center", P, props.cr_surface_center_radius, props.cr_surface_center_color, props.cr_surface_show_center)

def cb_update_gamma(self, context):
    props = context.scene
    update_torus("Gamma_Ref_Torus", props.cr_gamma_loc, props.cr_gamma_major, props.cr_gamma_minor, props.cr_gamma_color)
    update_center("Gamma_Ref_Center", props.cr_gamma_loc, props.cr_gamma_center_radius, props.cr_gamma_center_color, props.cr_gamma_show_center)

# --- 生成 ---
def create_base_obj(name, col):
    existing = bpy.data.objects.get(name)
    if existing:
        mesh = existing.data
        bpy.data.objects.remove(existing)
        if mesh.users == 0: bpy.data.meshes.remove(mesh)
    mesh = bpy.data.meshes.new(f"Mesh_{name}")
    obj = bpy.data.objects.new(name, mesh)
    col.objects.link(obj)
    return obj

class CRYSTAL_OT_generate(bpy.types.Operator):
    bl_idname = f"{PREFIX}.generate"
    bl_label = "4つのトーラスと中心マーカーを生成"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        col = bpy.data.collections.get(ADDON_TITLE)
        if not col:
            col = bpy.data.collections.new(ADDON_TITLE)
            context.scene.collection.children.link(col)
            
        # オブジェクトの枠だけ作成
        for name in ["Crystal_Torus", "Crystal_Center", "Retina_Torus", "Retina_Center", 
                     "Surface_Torus", "Surface_Center", "Gamma_Ref_Torus", "Gamma_Ref_Center"]:
            create_base_obj(name, col)

        # 全てを一斉にアップデート(コールバック関数を手動で呼ぶのと同じ効果)
        cb_update_crystal(context.scene, context)
        cb_update_retina(context.scene, context)
        cb_update_surface(context.scene, context)
        cb_update_gamma(context.scene, context)

        self.report({'INFO'}, "要素の生成が完了しました")
        return {'FINISHED'}

class CRYSTAL_OT_detach(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach"
    bl_label = "切り離し (独立化して確定)"
    def execute(self, context):
        names = ["Crystal_Torus", "Crystal_Center", "Retina_Torus", "Retina_Center", 
                 "Surface_Torus", "Surface_Center", "Gamma_Ref_Torus", "Gamma_Ref_Center"]
        count = 0
        for name in names:
            obj = bpy.data.objects.get(name)
            if obj:
                new_name = f"{name}_Baked"
                obj.name = new_name
                obj.data.name = f"Mesh_{new_name}"
                if obj.data.materials:
                    new_mat = obj.data.materials[0].copy()
                    new_mat.name = f"Mat_{new_name}"
                    obj.data.materials[0] = new_mat
                count += 1
                
        if count > 0: self.report({'INFO'}, "現在の状態を切り離して確定しました")
        return {'FINISHED'}

class CRYSTAL_OT_copy_info(bpy.types.Operator):
    bl_idname = f"{PREFIX}.copy_info"
    bl_label = "情報をクリップボードにコピー"
    def execute(self, context):
        props = context.scene
        P = get_surface_loc(props)
        info_text = (
            f"【1.水晶玉】\n  中心: ({props.cr_crystal_loc[0]:.2f}, {props.cr_crystal_loc[1]:.2f}, {props.cr_crystal_loc[2]:.2f})\n"
            f"  半径: {props.cr_crystal_major:.2f} / 太さ: {props.cr_crystal_minor:.2f}\n\n"
            f"【2.網膜点】\n  中心: ({props.cr_retina_loc[0]:.2f}, {props.cr_retina_loc[1]:.2f}, {props.cr_retina_loc[2]:.2f})\n"
            f"  半径: {props.cr_retina_major:.2f} / 太さ: {props.cr_retina_minor:.2f}\n\n"
            f"【3.表面交点】(自動計算)\n  中心: ({P.x:.2f}, {P.y:.2f}, {P.z:.2f})\n"
            f"  半径: {props.cr_surface_major:.2f} / 太さ: {props.cr_surface_minor:.2f}\n\n"
            f"【4.ガンマ基準点】\n  中心: ({props.cr_gamma_loc[0]:.2f}, {props.cr_gamma_loc[1]:.2f}, {props.cr_gamma_loc[2]:.2f})\n"
            f"  半径: {props.cr_gamma_major:.2f} / 太さ: {props.cr_gamma_minor:.2f}\n"
        )
        context.window_manager.clipboard = info_text
        self.report({'INFO'}, "パラメータ情報をコピーしました")
        return {'FINISHED'}

def setup_workspace():
    for window in bpy.context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        space.shading.type = 'MATERIAL'
                        space.show_region_ui = True
    return None

# =========================================================
# 9. パネル構成
# =========================================================
def draw_torus_box(layout, props, title, icon, p_loc, p_major, p_minor, p_color, p_show, p_radius, p_center_color, is_surface=False):
    box = layout.box()
    box.label(text=title, icon=icon)
    
    if is_surface:
        P = get_surface_loc(props)
        box.label(text=f"中心 (自動計算): ({P.x:.3f}, {P.y:.3f}, {P.z:.3f})")
    else:
        box.prop(props, p_loc, text="中心")
        
    row = box.row()
    row.prop(props, p_major, text="半径")
    row.prop(props, p_minor, text="太さ")
    box.prop(props, p_color, text="色")
    
    # センターマーカーの表示
    box.prop(props, p_show, text="中心に球体マーカーを表示", icon='MESH_UVSPHERE')
    if getattr(props, p_show):
        row = box.row()
        row.prop(props, p_radius, text="球の半径")
        row.prop(props, p_center_color, text="球の色")

class CRYSTAL_PT_main(bpy.types.Panel):
    bl_label = ADDON_TITLE
    bl_idname = f"{PREFIX_VAL}_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        props = context.scene
        layout.label(text="生成後にスライダーでリアルタイム変更", icon='INFO')
        
        draw_torus_box(layout, props, "1. 水晶玉", 'SPHERE', "cr_crystal_loc", "cr_crystal_major", "cr_crystal_minor", "cr_crystal_color", "cr_crystal_show_center", "cr_crystal_center_radius", "cr_crystal_center_color")
        draw_torus_box(layout, props, "2. 網膜点", 'TRACKING', "cr_retina_loc", "cr_retina_major", "cr_retina_minor", "cr_retina_color", "cr_retina_show_center", "cr_retina_center_radius", "cr_retina_center_color")
        draw_torus_box(layout, props, "3. 表面交点 (線分と球面の交差位置)", 'VERTEXSEL', None, "cr_surface_major", "cr_surface_minor", "cr_surface_color", "cr_surface_show_center", "cr_surface_center_radius", "cr_surface_center_color", is_surface=True)
        draw_torus_box(layout, props, "4. ガンマ基準点", 'EMPTY_DATA', "cr_gamma_loc", "cr_gamma_major", "cr_gamma_minor", "cr_gamma_color", "cr_gamma_show_center", "cr_gamma_center_radius", "cr_gamma_center_color")

        layout.separator()
        layout.operator(CRYSTAL_OT_generate.bl_idname, icon='PLAY')
        layout.operator(CRYSTAL_OT_detach.bl_idname, icon='UNLINKED')
        layout.operator(CRYSTAL_OT_copy_info.bl_idname, icon='COPYDOWN')

class CRYSTAL_PT_LinksPanel(bpy.types.Panel):
    bl_label = "リンク"
    bl_idname = f"{PREFIX_VAL}_PT_links_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_options = {'DEFAULT_CLOSED'}
    bl_order = 100

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        links = [(LINK_NAME_1, LINK_URL_1, 'HELP'), (LINK_NAME_2, LINK_URL_2, 'URL'), (LINK_NAME_3, LINK_URL_3, 'URL'), (LINK_NAME_4, LINK_URL_4, 'URL')]
        for name, url, icon in links:
            if name and url: box.operator(CRYSTAL_OT_OpenUrl.bl_idname, text=name, icon=icon).url = url

class CRYSTAL_PT_RemovePanel(bpy.types.Panel):
    bl_label = "アドオン削除"
    bl_idname = f"{PREFIX_VAL}_PT_remove_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_options = {'DEFAULT_CLOSED'}
    bl_order = 101

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.operator(CRYSTAL_OT_RemoveAddon.bl_idname, icon='CANCEL')

# =========================================================
# 10. register()
# =========================================================
classes = (
    CRYSTAL_OT_OpenUrl, CRYSTAL_OT_RemoveAddon, CRYSTAL_OT_generate, CRYSTAL_OT_detach, CRYSTAL_OT_copy_info,
    CRYSTAL_PT_main, CRYSTAL_PT_LinksPanel, CRYSTAL_PT_RemovePanel,
)

def register():
    for cls in classes: bpy.utils.register_class(cls)
    Scene = bpy.types.Scene
    
    # 1. 水晶玉
    Scene.cr_crystal_loc = bpy.props.FloatVectorProperty(name="位置", default=CRYSTAL_LOC_DEFAULT, subtype='XYZ', update=cb_update_crystal)
    Scene.cr_crystal_major = bpy.props.FloatProperty(name="半径", default=CRYSTAL_MAJOR_DEFAULT, min=0.0, update=cb_update_crystal)
    Scene.cr_crystal_minor = bpy.props.FloatProperty(name="太さ", default=CRYSTAL_MINOR_DEFAULT, min=0.0, update=cb_update_crystal)
    Scene.cr_crystal_color = bpy.props.FloatVectorProperty(name="色", default=COLOR_CRYSTAL_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_crystal)
    Scene.cr_crystal_show_center = bpy.props.BoolProperty(name="マーカー表示", default=False, update=cb_update_crystal)
    Scene.cr_crystal_center_radius = bpy.props.FloatProperty(name="球半径", default=CENTER_RADIUS_DEFAULT, min=0.0, update=cb_update_crystal)
    Scene.cr_crystal_center_color = bpy.props.FloatVectorProperty(name="球色", default=COLOR_CENTER_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_crystal)

    # 2. 網膜点
    Scene.cr_retina_loc = bpy.props.FloatVectorProperty(name="位置", default=RETINA_LOC_DEFAULT, subtype='XYZ', update=cb_update_retina)
    Scene.cr_retina_major = bpy.props.FloatProperty(name="半径", default=RETINA_MAJOR_DEFAULT, min=0.0, update=cb_update_retina)
    Scene.cr_retina_minor = bpy.props.FloatProperty(name="太さ", default=RETINA_MINOR_DEFAULT, min=0.0, update=cb_update_retina)
    Scene.cr_retina_color = bpy.props.FloatVectorProperty(name="色", default=COLOR_RETINA_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_retina)
    Scene.cr_retina_show_center = bpy.props.BoolProperty(name="マーカー表示", default=False, update=cb_update_retina)
    Scene.cr_retina_center_radius = bpy.props.FloatProperty(name="球半径", default=CENTER_RADIUS_DEFAULT, min=0.0, update=cb_update_retina)
    Scene.cr_retina_center_color = bpy.props.FloatVectorProperty(name="球色", default=COLOR_CENTER_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_retina)

    # 3. 表面交点 (位置のPropertyは不要:自動計算のため)
    Scene.cr_surface_major = bpy.props.FloatProperty(name="半径", default=SURFACE_MAJOR_DEFAULT, min=0.0, update=cb_update_surface)
    Scene.cr_surface_minor = bpy.props.FloatProperty(name="太さ", default=SURFACE_MINOR_DEFAULT, min=0.0, update=cb_update_surface)
    Scene.cr_surface_color = bpy.props.FloatVectorProperty(name="色", default=COLOR_SURFACE_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_surface)
    Scene.cr_surface_show_center = bpy.props.BoolProperty(name="マーカー表示", default=False, update=cb_update_surface)
    Scene.cr_surface_center_radius = bpy.props.FloatProperty(name="球半径", default=CENTER_RADIUS_DEFAULT, min=0.0, update=cb_update_surface)
    Scene.cr_surface_center_color = bpy.props.FloatVectorProperty(name="球色", default=COLOR_CENTER_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_surface)

    # 4. ガンマ基準点
    Scene.cr_gamma_loc = bpy.props.FloatVectorProperty(name="位置", default=GAMMA_LOC_DEFAULT, subtype='XYZ', update=cb_update_gamma)
    Scene.cr_gamma_major = bpy.props.FloatProperty(name="半径", default=GAMMA_MAJOR_DEFAULT, min=0.0, update=cb_update_gamma)
    Scene.cr_gamma_minor = bpy.props.FloatProperty(name="太さ", default=GAMMA_MINOR_DEFAULT, min=0.0, update=cb_update_gamma)
    Scene.cr_gamma_color = bpy.props.FloatVectorProperty(name="色", default=COLOR_GAMMA_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_gamma)
    Scene.cr_gamma_show_center = bpy.props.BoolProperty(name="マーカー表示", default=False, update=cb_update_gamma)
    Scene.cr_gamma_center_radius = bpy.props.FloatProperty(name="球半径", default=CENTER_RADIUS_DEFAULT, min=0.0, update=cb_update_gamma)
    Scene.cr_gamma_center_color = bpy.props.FloatVectorProperty(name="球色", default=COLOR_CENTER_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_gamma)

    bpy.app.timers.register(setup_workspace, first_interval=0.1)

# =========================================================
# 11. unregister()
# =========================================================
def unregister():
    props = [
        "cr_crystal_loc", "cr_crystal_major", "cr_crystal_minor", "cr_crystal_color", "cr_crystal_show_center", "cr_crystal_center_radius", "cr_crystal_center_color",
        "cr_retina_loc", "cr_retina_major", "cr_retina_minor", "cr_retina_color", "cr_retina_show_center", "cr_retina_center_radius", "cr_retina_center_color",
        "cr_surface_major", "cr_surface_minor", "cr_surface_color", "cr_surface_show_center", "cr_surface_center_radius", "cr_surface_center_color",
        "cr_gamma_loc", "cr_gamma_major", "cr_gamma_minor", "cr_gamma_color", "cr_gamma_show_center", "cr_gamma_center_radius", "cr_gamma_center_color"
    ]
    for p in props:
        if hasattr(bpy.types.Scene, p): delattr(bpy.types.Scene, p)
    for cls in reversed(classes):
        try: bpy.utils.unregister_class(cls)
        except: pass

if __name__ == "__main__":
    register()
# =========================================================
# 1. bl_info
# =========================================================
bl_info = {
    "name": "Crystal Torus Realtime System",
    "author": "ChatGPT",
    "version": (1, 6, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > Crystal Torus",
    "description": "リアルタイム変更・情報コピー・切り離し機能付き トーラス生成アドオン",
    "category": "3D View",
}

# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0723_1545

# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import webbrowser

# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "Crystal Torus System"
PREFIX_VAL = "CRYSTAL_TORUS"
PREFIX = PREFIX_VAL.lower()
ADDON_CATEGORY = "Crystal Torus"

# =========================================================
# 5. リンク定義
# =========================================================
LINK_NAME_1 = "専用マニュアル"
LINK_URL_1 = "<https://example.com/manual>"

LINK_NAME_2 = "コンテナ builder"
LINK_URL_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"

LINK_NAME_3 = "posfie"
LINK_URL_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"

LINK_NAME_4 = "zionadchat"
LINK_URL_4 = "<https://x.com/zionadchat>"

LINK_NAME_5 = ""
LINK_URL_5 = ""

# =========================================================
# 6. 初期設定
# =========================================================
SCALE_FACTOR_DEFAULT = 1.0

# 1. 水晶玉
CRYSTAL_MAJOR_DEFAULT = 0.10
CRYSTAL_MINOR_DEFAULT = 0.01
CRYSTAL_LOC_DEFAULT = (0.0, 0.0, 0.0)
COLOR_CRYSTAL_DEFAULT = (0.4, 0.8, 1.0, 0.5)

# 2. 網膜点
RETINA_MAJOR_DEFAULT = 1.00
RETINA_MINOR_DEFAULT = 0.01
RETINA_LOC_DEFAULT = (1.10, 0.0, 0.0)
COLOR_RETINA_DEFAULT = (1.0, 0.5, 0.5, 1.0)

# 3. ガンマ基準点
GAMMA_MAJOR_DEFAULT = 1.10
GAMMA_MINOR_DEFAULT = 0.05
GAMMA_LOC_DEFAULT = (0.0, 0.0, 0.0)
COLOR_GAMMA_DEFAULT = (1.0, 0.9, 0.4, 0.5)

# =========================================================
# 7. 共通クラス
# =========================================================
class CRYSTAL_OT_OpenUrl(bpy.types.Operator):
    bl_idname = f"{PREFIX}.open_url"
    bl_label = "リンクを開く"
    url: bpy.props.StringProperty()
    def execute(self, context):
        if self.url: webbrowser.open(self.url)
        return {'FINISHED'}

class CRYSTAL_OT_RemoveAddon(bpy.types.Operator):
    bl_idname = f"{PREFIX}.remove_addon"
    bl_label = "Unregister Addon"
    bl_description = "このアドオンを無効化し、UIから消去します"
    def execute(self, context):
        bpy.app.timers.register(unregister, first_interval=0.01)
        self.report({'INFO'}, f"{ADDON_TITLE} を UI から消去しました")
        return {'FINISHED'}

# =========================================================
# 8. アドオン固有機能 (リアルタイム・生成・切り離し)
# =========================================================

def create_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name=name)
    mat.use_nodes = True
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    if bsdf:
        bsdf.inputs["Base Color"].default_value = color
        if color[3] < 1.0:
            bsdf.inputs["Alpha"].default_value = color[3]
        else:
            bsdf.inputs["Alpha"].default_value = 1.0
    return mat

def apply_torus_bmesh(obj, major, minor):
    """数学的に頂点を計算してZX平面のトーラスを構築する(エラー回避とリアルタイム対応用)"""
    bm = bmesh.new()
    scaled_major = major * SCALE_FACTOR_DEFAULT
    scaled_minor = minor * SCALE_FACTOR_DEFAULT
    
    segments = 48  
    rings = 24     
    verts = []
    
    # 太さや半径が0の時はクラッシュ防止のため微小値を設定するか、処理をスキップ
    if scaled_major <= 0.0001: scaled_major = 0.0001
    if scaled_minor <= 0.0001: scaled_minor = 0.0001
    
    for i in range(segments):
        theta = 2.0 * math.pi * i / segments
        cos_theta = math.cos(theta)
        sin_theta = math.sin(theta)
        
        row = []
        for j in range(rings):
            phi = 2.0 * math.pi * j / rings
            cos_phi = math.cos(phi)
            sin_phi = math.sin(phi)
            
            x = (scaled_major + scaled_minor * cos_phi) * cos_theta
            z = (scaled_major + scaled_minor * cos_phi) * sin_theta
            y = scaled_minor * sin_phi
            
            v = bm.verts.new((x, y, z))
            row.append(v)
        verts.append(row)
        
    for i in range(segments):
        next_i = (i + 1) % segments
        for j in range(rings):
            next_j = (j + 1) % rings
            v1 = verts[i][j]
            v2 = verts[i][next_j]
            v3 = verts[next_i][next_j]
            v4 = verts[next_i][j]
            f = bm.faces.new((v1, v2, v3, v4))
            f.smooth = True 

    bm.to_mesh(obj.data)
    bm.free()

def update_torus(obj_name, loc, major, minor, color):
    obj = bpy.data.objects.get(obj_name)
    if not obj:
        return 

    apply_torus_bmesh(obj, major, minor)
    
    obj.location = (
        loc[0] * SCALE_FACTOR_DEFAULT,
        loc[1] * SCALE_FACTOR_DEFAULT,
        loc[2] * SCALE_FACTOR_DEFAULT
    )
    
    create_material(f"Mat_{obj_name}", color)

def cb_update_crystal(self, context):
    update_torus("Crystal_Torus", self.cr_crystal_loc, self.cr_crystal_major, self.cr_crystal_minor, self.cr_crystal_color)

def cb_update_retina(self, context):
    update_torus("Retina_Torus", self.cr_retina_loc, self.cr_retina_major, self.cr_retina_minor, self.cr_retina_color)

def cb_update_gamma(self, context):
    update_torus("Gamma_Ref_Torus", self.cr_gamma_loc, self.cr_gamma_major, self.cr_gamma_minor, self.cr_gamma_color)

def create_torus_obj(name, loc, major, minor, color, col):
    existing = bpy.data.objects.get(name)
    if existing:
        mesh = existing.data
        bpy.data.objects.remove(existing)
        if mesh.users == 0:
            bpy.data.meshes.remove(mesh)
            
    mesh = bpy.data.meshes.new(f"Mesh_{name}")
    obj = bpy.data.objects.new(name, mesh)
    col.objects.link(obj)
    
    apply_torus_bmesh(obj, major, minor)
    
    obj.location = (
        loc[0] * SCALE_FACTOR_DEFAULT,
        loc[1] * SCALE_FACTOR_DEFAULT,
        loc[2] * SCALE_FACTOR_DEFAULT
    )
    
    mat = create_material(f"Mat_{name}", color)
    obj.data.materials.append(mat)
    return obj

class CRYSTAL_OT_generate(bpy.types.Operator):
    bl_idname = f"{PREFIX}.generate"
    bl_label = "3つのトーラスを生成"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        props = context.scene
        col = bpy.data.collections.get(ADDON_TITLE)
        if not col:
            col = bpy.data.collections.new(ADDON_TITLE)
            context.scene.collection.children.link(col)
            
        create_torus_obj("Crystal_Torus", props.cr_crystal_loc, props.cr_crystal_major, props.cr_crystal_minor, props.cr_crystal_color, col)
        create_torus_obj("Retina_Torus", props.cr_retina_loc, props.cr_retina_major, props.cr_retina_minor, props.cr_retina_color, col)
        create_torus_obj("Gamma_Ref_Torus", props.cr_gamma_loc, props.cr_gamma_major, props.cr_gamma_minor, props.cr_gamma_color, col)

        self.report({'INFO'}, "ZX平面にトーラスを生成しました(以降スライダーでリアルタイム変更可能)")
        return {'FINISHED'}

class CRYSTAL_OT_detach(bpy.types.Operator):
    bl_idname = f"{PREFIX}.detach"
    bl_label = "切り離し (独立化して確定)"
    bl_description = "現在のトーラスをアドオン管理から切り離し、マテリアルも独立させます"
    
    def execute(self, context):
        names = ["Crystal_Torus", "Retina_Torus", "Gamma_Ref_Torus"]
        detached_count = 0
        
        for name in names:
            obj = bpy.data.objects.get(name)
            if obj:
                new_name = f"{name}_Baked"
                obj.name = new_name
                obj.data.name = f"Mesh_{new_name}"
                
                if obj.data.materials:
                    old_mat = obj.data.materials[0]
                    new_mat = old_mat.copy()
                    new_mat.name = f"Mat_{new_name}"
                    obj.data.materials[0] = new_mat
                    
                detached_count += 1
                
        if detached_count > 0:
            self.report({'INFO'}, f"{detached_count}個のトーラスを切り離しました。(再生成が可能です)")
        else:
            self.report({'WARNING'}, "切り離す対象のトーラスが見つかりません。")
            
        return {'FINISHED'}

class CRYSTAL_OT_copy_info(bpy.types.Operator):
    bl_idname = f"{PREFIX}.copy_info"
    bl_label = "情報をクリップボードにコピー"
    
    def execute(self, context):
        props = context.scene
        info_text = (
            f"【1.水晶玉】\n  中心: ({props.cr_crystal_loc[0]:.2f}, {props.cr_crystal_loc[1]:.2f}, {props.cr_crystal_loc[2]:.2f})\n"
            f"  半径: {props.cr_crystal_major:.2f} / 太さ: {props.cr_crystal_minor:.2f}\n\n"
            f"【2.網膜点】\n  中心: ({props.cr_retina_loc[0]:.2f}, {props.cr_retina_loc[1]:.2f}, {props.cr_retina_loc[2]:.2f})\n"
            f"  半径: {props.cr_retina_major:.2f} / 太さ: {props.cr_retina_minor:.2f}\n\n"
            f"【3.ガンマ基準点】\n  中心: ({props.cr_gamma_loc[0]:.2f}, {props.cr_gamma_loc[1]:.2f}, {props.cr_gamma_loc[2]:.2f})\n"
            f"  半径: {props.cr_gamma_major:.2f} / 太さ: {props.cr_gamma_minor:.2f}\n"
        )
        context.window_manager.clipboard = info_text
        self.report({'INFO'}, "パラメータ情報をコピーしました")
        return {'FINISHED'}

def setup_workspace():
    for window in bpy.context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        space.shading.type = 'MATERIAL'
                        space.show_region_ui = True
    return None

# =========================================================
# 9. パネル構成
# =========================================================
class CRYSTAL_PT_main(bpy.types.Panel):
    bl_label = ADDON_TITLE
    bl_idname = f"{PREFIX_VAL}_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        props = context.scene
        
        layout.label(text="生成後にスライダーでリアルタイム変更", icon='INFO')
        
        box = layout.box()
        box.prop(props, "cr_crystal_loc", text="1.水晶玉 中心")
        row = box.row()
        row.prop(props, "cr_crystal_major", text="半径")
        row.prop(props, "cr_crystal_minor", text="太さ")
        box.prop(props, "cr_crystal_color", text="色")

        box = layout.box()
        box.prop(props, "cr_retina_loc", text="2.網膜点 中心")
        row = box.row()
        row.prop(props, "cr_retina_major", text="半径")
        row.prop(props, "cr_retina_minor", text="太さ")
        box.prop(props, "cr_retina_color", text="色")

        box = layout.box()
        box.prop(props, "cr_gamma_loc", text="3.ガンマ基準点 中心")
        row = box.row()
        row.prop(props, "cr_gamma_major", text="半径")
        row.prop(props, "cr_gamma_minor", text="太さ")
        box.prop(props, "cr_gamma_color", text="色")

        layout.separator()
        layout.operator(CRYSTAL_OT_generate.bl_idname, icon='PLAY', text="トーラスを生成する")
        layout.operator(CRYSTAL_OT_detach.bl_idname, icon='UNLINKED', text="切り離し (独立化して確定)")

class CRYSTAL_PT_info(bpy.types.Panel):
    bl_label = "パラメータ情報"
    bl_idname = f"{PREFIX_VAL}_PT_info"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 1

    def draw(self, context):
        layout = self.layout
        props = context.scene
        
        box = layout.box()
        box.label(text=f"水晶玉 : ({props.cr_crystal_loc[0]:.2f}, {props.cr_crystal_loc[1]:.2f}, {props.cr_crystal_loc[2]:.2f})")
        box.label(text=f" └ 半径: {props.cr_crystal_major:.2f} / 太さ: {props.cr_crystal_minor:.2f}")
        
        box = layout.box()
        box.label(text=f"網膜点 : ({props.cr_retina_loc[0]:.2f}, {props.cr_retina_loc[1]:.2f}, {props.cr_retina_loc[2]:.2f})")
        box.label(text=f" └ 半径: {props.cr_retina_major:.2f} / 太さ: {props.cr_retina_minor:.2f}")
        
        box = layout.box()
        box.label(text=f"ガンマ : ({props.cr_gamma_loc[0]:.2f}, {props.cr_gamma_loc[1]:.2f}, {props.cr_gamma_loc[2]:.2f})")
        box.label(text=f" └ 半径: {props.cr_gamma_major:.2f} / 太さ: {props.cr_gamma_minor:.2f}")
        
        layout.separator()
        layout.operator(CRYSTAL_OT_copy_info.bl_idname, icon='COPYDOWN')

class CRYSTAL_PT_LinksPanel(bpy.types.Panel):
    bl_label = "リンク"
    bl_idname = f"{PREFIX_VAL}_PT_links_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_options = {'DEFAULT_CLOSED'}
    bl_order = 100

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        links = [
            (LINK_NAME_1, LINK_URL_1, 'HELP'),
            (LINK_NAME_2, LINK_URL_2, 'URL'),
            (LINK_NAME_3, LINK_URL_3, 'URL'),
            (LINK_NAME_4, LINK_URL_4, 'URL'),
        ]
        for name, url, icon in links:
            if name and url:
                box.operator(CRYSTAL_OT_OpenUrl.bl_idname, text=name, icon=icon).url = url

class CRYSTAL_PT_RemovePanel(bpy.types.Panel):
    bl_label = "アドオン削除"
    bl_idname = f"{PREFIX_VAL}_PT_remove_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_options = {'DEFAULT_CLOSED'}
    bl_order = 101

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label(text="このアドオンを消去します", icon='ERROR')
        box.operator(CRYSTAL_OT_RemoveAddon.bl_idname, icon='CANCEL')

# =========================================================
# 10. register()
# =========================================================
classes = (
    CRYSTAL_OT_OpenUrl,
    CRYSTAL_OT_RemoveAddon,
    CRYSTAL_OT_generate,
    CRYSTAL_OT_detach,
    CRYSTAL_OT_copy_info,
    CRYSTAL_PT_main,
    CRYSTAL_PT_info,
    CRYSTAL_PT_LinksPanel,
    CRYSTAL_PT_RemovePanel,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
        
    Scene = bpy.types.Scene
    
    # 【制限】 min=0.0 を追加し、太さや半径がマイナスにならないよう設定
    Scene.cr_crystal_loc = bpy.props.FloatVectorProperty(name="水晶玉 中心", default=CRYSTAL_LOC_DEFAULT, subtype='XYZ', update=cb_update_crystal)
    Scene.cr_crystal_major = bpy.props.FloatProperty(name="水晶玉 半径", default=CRYSTAL_MAJOR_DEFAULT, min=0.0, update=cb_update_crystal)
    Scene.cr_crystal_minor = bpy.props.FloatProperty(name="水晶玉 太さ", default=CRYSTAL_MINOR_DEFAULT, min=0.0, update=cb_update_crystal)
    Scene.cr_crystal_color = bpy.props.FloatVectorProperty(name="水晶玉 色", default=COLOR_CRYSTAL_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_crystal)

    Scene.cr_retina_loc = bpy.props.FloatVectorProperty(name="網膜点 中心", default=RETINA_LOC_DEFAULT, subtype='XYZ', update=cb_update_retina)
    Scene.cr_retina_major = bpy.props.FloatProperty(name="網膜点 半径", default=RETINA_MAJOR_DEFAULT, min=0.0, update=cb_update_retina)
    Scene.cr_retina_minor = bpy.props.FloatProperty(name="網膜点 太さ", default=RETINA_MINOR_DEFAULT, min=0.0, update=cb_update_retina)
    Scene.cr_retina_color = bpy.props.FloatVectorProperty(name="網膜点 色", default=COLOR_RETINA_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_retina)

    Scene.cr_gamma_loc = bpy.props.FloatVectorProperty(name="ガンマ基準 中心", default=GAMMA_LOC_DEFAULT, subtype='XYZ', update=cb_update_gamma)
    Scene.cr_gamma_major = bpy.props.FloatProperty(name="ガンマ基準 半径", default=GAMMA_MAJOR_DEFAULT, min=0.0, update=cb_update_gamma)
    Scene.cr_gamma_minor = bpy.props.FloatProperty(name="ガンマ基準 太さ", default=GAMMA_MINOR_DEFAULT, min=0.0, update=cb_update_gamma)
    Scene.cr_gamma_color = bpy.props.FloatVectorProperty(name="ガンマ基準 色", default=COLOR_GAMMA_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_gamma)

    bpy.app.timers.register(setup_workspace, first_interval=0.1)

# =========================================================
# 11. unregister()
# =========================================================
def unregister():
    Scene = bpy.types.Scene
    props = [
        "cr_crystal_loc", "cr_crystal_major", "cr_crystal_minor", "cr_crystal_color",
        "cr_retina_loc", "cr_retina_major", "cr_retina_minor", "cr_retina_color",
        "cr_gamma_loc", "cr_gamma_major", "cr_gamma_minor", "cr_gamma_color"
    ]
    for p in props:
        if hasattr(Scene, p):
            delattr(Scene, p)

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

if __name__ == "__main__":
    register()