blender Million 2026

https://posfie.com/@timekagura?sort=0&page=1

https://x.com/zionadchat

作業場

作業場 (1)

rapture_20260312041528.png

image.png

# Copied: 04:02:56
# Copied: 16:00:01
import bpy
import bmesh
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime

# ==============================================================================
#  設定エリア & ID管理
# ==============================================================================

PREFIX = "Interferometer20260312_v10"
TAB_NAME = "[ Interferometer ]   "

# ### ZIONAD_SOURCE_ID: INTERFEROMETER_2026_03_12_V10 ###

bl_info = {
    "name": f"zionad 520 [ Interferometer Gen ] {PREFIX}",
    "author": "zionadchat",
    "version": (5, 1, 1),
    "blender": (5, 0, 0),
    "location": "3D View > Sidebar",
    "description": "5-Part Interferometer Sphere Generator (Auto-Preview Fix)",
    "category": "3D View",
}

OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: INTERFEROMETER_2026_03_12_V10 ###"

ADDON_LINKS = (
    {"label": "あっさり干渉計 20260312版", "url": "<https://www.notion.so/20260312-320f5dacaf438031a63dd9fc00edc049>"},
    {"label": "Code Copy Template", "url": "<https://www.notion.so/Code-copy-20260221>"},
    {"label": "Theory Background", "url": "<https://www.notion.so/Einstein-from-20260119>"},
)

# ==============================================================================
#  デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_preview": True,
    "sphere_loc": (0.0000, 0.0000, 0.0000),
    "sphere_radius": 30.0000,
    "cone_angle": 178.8900,
    "split_offset": 0.0000,
    "show_top_cap": False,
    "cap_top_cap": True,
    "color_front_top_cap": (0.8000, 0.2000, 0.2000, 0.8000),
    "color_back_top_cap": (0.4000, 0.1000, 0.1000, 0.8000),
    "show_top_cone": False,
    "cap_top_cone": True,
    "color_front_top_cone": (0.8000, 0.5000, 0.2000, 0.8000),
    "color_back_top_cone": (0.4000, 0.2500, 0.1000, 0.8000),
    "show_mid": False,
    "cap_mid": True,
    "color_front_mid": (0.2000, 0.8000, 0.2000, 0.8000),
    "color_back_mid": (0.1000, 0.4000, 0.1000, 0.8000),
    "show_bot_cone": False,
    "cap_bot_cone": True,
    "color_front_bot_cone": (0.2000, 0.5000, 0.8000, 0.8000),
    "color_back_bot_cone": (0.1000, 0.2500, 0.4000, 0.8000),
    "show_bot_cap": True,
    "cap_bot_cap": False,
    "color_front_bot_cap": (0.8000, 0.0158, 0.7195, 1.0000),
    "color_back_bot_cap": (0.0379, 0.0173, 0.0576, 1.0000),
    "show_bot_rays": True,
    "ray_count": 22,
    "ray_thickness": 2.6000,
    "ray_offset": 10.0000,
    "color_front_bot_rays": (0.9000, 0.9000, 0.2000, 0.8000),
    "color_back_bot_rays": (0.5000, 0.5000, 0.1000, 0.8000),
}
# <END_DICT>

# ==============================================================================
#  マテリアル作成ロジック (Blender 5.0+ 完全対応版)
# ==============================================================================

def apply_material_color(mat, front_color, back_color):
    mat.use_nodes = True
    f_col = list(front_color)
    b_col = list(back_color)
    
    tree = mat.node_tree
    tree.nodes.clear()

    try:
        bsdf_front = tree.nodes.new("ShaderNodeBsdfPrincipled")
        bsdf_front.location = (-200, 150)
        bsdf_front.inputs["Base Color"].default_value = f_col
        bsdf_front.inputs["Alpha"].default_value = f_col[3]
        
        bsdf_back = tree.nodes.new("ShaderNodeBsdfPrincipled")
        bsdf_back.location = (-200, -150)
        bsdf_back.inputs["Base Color"].default_value = b_col
        bsdf_back.inputs["Alpha"].default_value = b_col[3]
        
        mix_shader = tree.nodes.new("ShaderNodeMixShader")
        mix_shader.location = (100, 0)
        
        geom = tree.nodes.new("ShaderNodeNewGeometry")
        geom.location = (-400, 300)
        
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        out.location = (300, 0)
        
        tree.links.new(geom.outputs["Backfacing"], mix_shader.inputs[0])
        tree.links.new(bsdf_front.outputs["BSDF"], mix_shader.inputs[1])
        tree.links.new(bsdf_back.outputs["BSDF"], mix_shader.inputs[2])
        tree.links.new(mix_shader.outputs["Shader"], out.inputs["Surface"])
    except Exception as e:
        print("Material Error 5.0+:", e)
        
    mat.diffuse_color = f_col

def create_unique_material(front_color, back_color, name_prefix="Mat"):
    timestamp = datetime.now().strftime('%M%S%f')[:5] 
    mat_name = f"{name_prefix}_{timestamp}"
    mat = bpy.data.materials.new(name=mat_name)
    apply_material_color(mat, front_color, back_color)
    return mat

def get_preview_material(front_color, back_color, name="Mat_Prev"):
    mat = bpy.data.materials.get(name)
    if not mat: mat = bpy.data.materials.new(name=name)
    apply_material_color(mat, front_color, back_color)
    return mat

# ==============================================================================
#  ジオメトリ構築ロジック
# ==============================================================================

def build_base_meshes(props, prefix):
    meshes = {}
    R = props.sphere_radius
    theta = math.radians(props.cone_angle / 2)
    H = R * math.cos(theta)
    r_base = R * math.sin(theta)

    def make_mesh(name, bm):
        mesh = bpy.data.meshes.get(name)
        if not mesh: mesh = bpy.data.meshes.new(name)
        bm.to_mesh(mesh)
        bm.free()
        return mesh

    # Base Spheres & Cones
    for key in ['base_top', 'base_mid', 'base_bot']:
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=R)
        meshes[key] = make_mesh(f"{prefix}_{key}", bm)

    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=props.cap_top_cone, cap_tris=True, segments=64, radius1=0.0, radius2=r_base, depth=H)
    bmesh.ops.translate(bm, vec=(0, 0, H/2), verts=bm.verts)
    meshes['cone_top'] = make_mesh(f"{prefix}_ConeTop", bm)

    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=props.cap_bot_cone, cap_tris=True, segments=64, radius1=r_base, radius2=0.0, depth=H)
    bmesh.ops.translate(bm, vec=(0, 0, -H/2), verts=bm.verts)
    meshes['cone_bot'] = make_mesh(f"{prefix}_ConeBot", bm)

    bm = bmesh.new()
    bmesh.ops.create_cube(bm, size=1.0)
    bmesh.ops.scale(bm, vec=(R*4, R*4, R*4), verts=bm.verts)
    bmesh.ops.translate(bm, vec=(0, 0, H + 2*R), verts=bm.verts)
    meshes['box_top'] = make_mesh(f"{prefix}_BoxTop", bm)

    bm = bmesh.new()
    bmesh.ops.create_cube(bm, size=1.0)
    bmesh.ops.scale(bm, vec=(R*4, R*4, R*4), verts=bm.verts)
    bmesh.ops.translate(bm, vec=(0, 0, -H - 2*R), verts=bm.verts)
    meshes['box_bot'] = make_mesh(f"{prefix}_BoxBot", bm)

    # --- Bottom Rays Generation (Fibonacci Lattice) ---
    bm_rays = bmesh.new()
    count = props.ray_count
    offset = props.ray_offset
    thickness = props.ray_thickness
    
    z_min = -R
    z_max = -H
    golden_ratio = (1 + 5 ** 0.5) / 2
    
    for i in range(count):
        # 等面積になるようにZ軸を分割
        z = z_min + (z_max - z_min) * ((i + 0.5) / count) if count > 0 else z_min
        r = math.sqrt(max(0, R**2 - z**2))
        phi = 2 * math.pi * i / golden_ratio
        
        x = r * math.cos(phi)
        y = r * math.sin(phi)
        u = Vector((x, y, z)).normalized()
        
        P = u * R        # 底面中心 (球体表面)
        A = u * offset   # 頂点 (中心からのオフセット)
        depth = R - offset
        
        if depth <= 0.1: continue
        
        geom = bmesh.ops.create_cone(bm_rays, cap_ends=True, cap_tris=True, segments=12, radius1=thickness, radius2=0, depth=depth)
        verts = [v for v in geom['verts'] if isinstance(v, bmesh.types.BMVert)]
        
        rot = Vector((0,0,1)).rotation_difference(-u)
        bmesh.ops.rotate(bm_rays, cent=(0,0,0), matrix=rot.to_matrix(), verts=verts)
        
        mid = (P + A) / 2
        bmesh.ops.translate(bm_rays, vec=mid, verts=verts)

    meshes['bot_rays'] = make_mesh(f"{prefix}_BotRays", bm_rays)

    return meshes

def setup_cutters(meshes, loc, offset_v, collection, is_preview=False):
    cutters = {}
    names = [('box_top', "Cut_BoxTop"), ('box_bot', "Cut_BoxBot")]
              
    for key, name in names:
        obj = bpy.data.objects.new(name, meshes[key])
        obj.display_type = 'BOUNDS'
        obj.hide_viewport = True; obj.hide_render = True
        if is_preview: obj[PREVIEW_TAG] = True
        collection.objects.link(obj)
        cutters[key] = obj
        
    cutters['box_top'].location = loc + Vector((0, 0, offset_v * 2))
    cutters['box_bot'].location = loc + Vector((0, 0, -offset_v * 2))
    return cutters

def setup_parts(meshes, cutters, loc, offset_v, collection, is_preview=False):
    parts = {}
    parts['top_cap'] = bpy.data.objects.new("Part_TopCap", meshes['base_top'])
    parts['top_cone'] = bpy.data.objects.new("Part_TopCone", meshes['cone_top'])
    parts['mid'] = bpy.data.objects.new("Part_Middle", meshes['base_mid'])
    parts['bot_cone'] = bpy.data.objects.new("Part_BotCone", meshes['cone_bot'])
    parts['bot_cap'] = bpy.data.objects.new("Part_BotCap", meshes['base_bot'])
    parts['bot_rays'] = bpy.data.objects.new("Part_BotRays", meshes['bot_rays'])
    
    for obj in parts.values():
        if is_preview: obj[PREVIEW_TAG] = True
        collection.objects.link(obj)

    parts['top_cap'].location = loc + Vector((0, 0, offset_v * 2))
    parts['top_cone'].location = loc + Vector((0, 0, offset_v * 1))
    parts['mid'].location = loc
    parts['bot_cone'].location = loc + Vector((0, 0, -offset_v * 1))
    parts['bot_cap'].location = loc + Vector((0, 0, -offset_v * 2))
    parts['bot_rays'].location = loc + Vector((0, 0, -offset_v * 2)) # Bottom Capと同じ座標系
    
    mod = parts['top_cap'].modifiers.new("Bool", 'BOOLEAN')
    mod.operation = 'INTERSECT'; mod.object = cutters['box_top']; mod.solver = 'EXACT'
    
    mod = parts['bot_cap'].modifiers.new("Bool", 'BOOLEAN')
    mod.operation = 'INTERSECT'; mod.object = cutters['box_bot']; mod.solver = 'EXACT'
    
    mod = parts['mid'].modifiers.new("Bool1", 'BOOLEAN')
    mod.operation = 'DIFFERENCE'; mod.object = cutters['box_top']; mod.solver = 'EXACT'
    mod = parts['mid'].modifiers.new("Bool2", 'BOOLEAN')
    mod.operation = 'DIFFERENCE'; mod.object = cutters['box_bot']; mod.solver = 'EXACT'
        
    return parts

def remove_flat_faces(mesh, normal_z_targets, threshold=0.01):
    bm = bmesh.new()
    bm.from_mesh(mesh)
    faces_to_remove = []
    for f in bm.faces:
        for nz in normal_z_targets:
            if abs(f.normal.z - nz) < threshold:
                faces_to_remove.append(f)
                break
    if faces_to_remove:
        bmesh.ops.delete(bm, geom=faces_to_remove, context='FACES')
        bm.to_mesh(mesh)
    bm.free()

def apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=False):
    context.view_layer.update()
    dg = context.evaluated_depsgraph_get()
    
    for key in ['top_cap', 'mid', 'bot_cap']:
        obj = parts.get(key)
        if not obj: continue
        try:
            eval_obj = obj.evaluated_get(dg)
            new_mesh = bpy.data.meshes.new_from_object(eval_obj)
            if is_preview: new_mesh.name = f"Prev_Applied_{key}"
            
            old_mesh = obj.data
            obj.modifiers.clear()
            obj.data = new_mesh
            if old_mesh.users == 0: bpy.data.meshes.remove(old_mesh)
                
            if key == 'top_cap' and not getattr(props, 'cap_top_cap', True):
                remove_flat_faces(new_mesh, [-1.0])
            elif key == 'bot_cap' and not getattr(props, 'cap_bot_cap', True):
                remove_flat_faces(new_mesh, [1.0])
            elif key == 'mid' and not getattr(props, 'cap_mid', True):
                remove_flat_faces(new_mesh, [1.0, -1.0])
        except Exception as e:
            print(f"Mod Apply Error [{key}]:", e)
            
    for obj in cutters.values():
        m = obj.data
        try:
            bpy.data.objects.remove(obj, do_unlink=True)
            if m and m.users == 0: bpy.data.meshes.remove(m)
        except: pass

# ==============================================================================
#  プレビュー用ロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_TAG = f"{PREFIX}_preview_tag"

def clear_preview_data(prefix):
    col = bpy.data.collections.get(f"{prefix}_Preview_Zone")
    if col:
        for o in list(col.objects):
            m = o.data
            bpy.data.objects.remove(o, do_unlink=True)
            if m and getattr(m, "users", 0) == 0: bpy.data.meshes.remove(m)
    for m in list(bpy.data.meshes):
        if (m.name.startswith(f"Prev_{prefix}_") or m.name.startswith("Prev_Applied_")) and m.users == 0:
            bpy.data.meshes.remove(m)

def update_preview_geometry(context):
    props = getattr(context.scene, PROPS_NAME, None)
    if not props: return
    clear_preview_data(PREFIX)
    if not props.show_preview: 
        context.view_layer.update(); return

    col = bpy.data.collections.get(PREVIEW_COL_NAME)
    if not col:
        col = bpy.data.collections.new(PREVIEW_COL_NAME)
        context.scene.collection.children.link(col)

    meshes = build_base_meshes(props, f"Prev_{PREFIX}")
    loc = Vector(props.sphere_loc)
    offset_v = props.split_offset
    
    cutters = setup_cutters(meshes, loc, offset_v, col, is_preview=True)
    parts = setup_parts(meshes, cutters, loc, offset_v, col, is_preview=True)
    apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=True)
    
    parts_config = [
        ('top_cap', props.show_top_cap, props.color_front_top_cap, props.color_back_top_cap, "TCap"),
        ('top_cone', props.show_top_cone, props.color_front_top_cone, props.color_back_top_cone, "TCone"),
        ('mid', props.show_mid, props.color_front_mid, props.color_back_mid, "Mid"),
        ('bot_cone', props.show_bot_cone, props.color_front_bot_cone, props.color_back_bot_cone, "BCone"),
        ('bot_cap', props.show_bot_cap, props.color_front_bot_cap, props.color_back_bot_cap, "BCap"),
        ('bot_rays', props.show_bot_rays, props.color_front_bot_rays, props.color_back_bot_rays, "BRays"),
    ]
    
    for key, is_show, c_front, c_back, mat_name in parts_config:
        obj = parts.get(key)
        if not obj: continue
        try:
            if is_show:
                mat = get_preview_material(c_front, c_back, f"Mat_Prev_{mat_name}")
                if not obj.data.materials: obj.data.materials.append(mat)
                else: obj.data.materials[0] = mat
            else:
                m = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if m and m.users == 0: bpy.data.meshes.remove(m)
        except Exception as e: print(f"Visibility Error[{key}]:", e)

    context.view_layer.update()

_timer = None
def delayed_update():
    global _timer
    _timer = None
    if bpy.context and bpy.context.scene: update_preview_geometry(bpy.context)
    return None

def on_update(self, context):
    global _timer
    if _timer: 
        try: bpy.app.timers.unregister(_timer)
        except: pass
    _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)

# ==============================================================================
#  PROPERTIES
# ==============================================================================

class PG_SphereProps(PropertyGroup):
    show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
    sphere_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['sphere_loc'], update=on_update)
    sphere_radius: FloatProperty(name="Sphere Radius", default=CURRENT_DEFAULTS['sphere_radius'], min=0.01, update=on_update)
    cone_angle: FloatProperty(name="Cone Angle", default=CURRENT_DEFAULTS['cone_angle'], min=1.0, max=179.0, update=on_update)
    split_offset: FloatProperty(name="Split Z-Offset", default=CURRENT_DEFAULTS['split_offset'], min=0.0, update=on_update)

    show_top_cap: BoolProperty(name="Top Cap", default=CURRENT_DEFAULTS['show_top_cap'], update=on_update)
    cap_top_cap: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_top_cap'], update=on_update)
    color_front_top_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_top_cap'], update=on_update)
    color_back_top_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_top_cap'], update=on_update)
    
    show_top_cone: BoolProperty(name="Top Cone", default=CURRENT_DEFAULTS['show_top_cone'], update=on_update)
    cap_top_cone: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_top_cone'], update=on_update)
    color_front_top_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_top_cone'], update=on_update)
    color_back_top_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_top_cone'], update=on_update)
    
    show_mid: BoolProperty(name="Middle Sphere", default=CURRENT_DEFAULTS['show_mid'], update=on_update)
    cap_mid: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_mid'], update=on_update)
    color_front_mid: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_mid'], update=on_update)
    color_back_mid: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_mid'], update=on_update)
    
    show_bot_cone: BoolProperty(name="Bottom Cone", default=CURRENT_DEFAULTS['show_bot_cone'], update=on_update)
    cap_bot_cone: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_bot_cone'], update=on_update)
    color_front_bot_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_bot_cone'], update=on_update)
    color_back_bot_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_bot_cone'], update=on_update)
    
    show_bot_cap: BoolProperty(name="Bottom Cap", default=CURRENT_DEFAULTS['show_bot_cap'], update=on_update)
    cap_bot_cap: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_bot_cap'], update=on_update)
    color_front_bot_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_bot_cap'], update=on_update)
    color_back_bot_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_bot_cap'], update=on_update)

    # --- New Rays Properties ---
    show_bot_rays: BoolProperty(name="Bottom Rays", default=CURRENT_DEFAULTS['show_bot_rays'], update=on_update)
    ray_count: IntProperty(name="Ray Count", default=CURRENT_DEFAULTS['ray_count'], min=1, max=72, update=on_update)
    ray_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['ray_thickness'], min=0.01, update=on_update)
    ray_offset: FloatProperty(name="Apex Offset", default=CURRENT_DEFAULTS['ray_offset'], min=0.0, update=on_update)
    color_front_bot_rays: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_bot_rays'], update=on_update)
    color_back_bot_rays: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_bot_rays'], update=on_update)

# ==============================================================================
#  OPERATORS
# ==============================================================================

class OT_CreateSphere(Operator):
    bl_idname = f"{OP_PREFIX}.create_sphere"
    bl_label = "Create Selected Parts"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        timestamp = datetime.now().strftime('%H%M%S')
        
        meshes = build_base_meshes(props, f"Temp_{timestamp}")
        loc = Vector(props.sphere_loc)
        offset_v = props.split_offset
        col = context.collection if context.collection else context.scene.collection
        
        cutters = setup_cutters(meshes, loc, offset_v, col, is_preview=False)
        parts = setup_parts(meshes, cutters, loc, offset_v, col, is_preview=False)
        apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=False)
        
        parts_config = [
            ('top_cap', props.show_top_cap, props.color_front_top_cap, props.color_back_top_cap, "TopCap"),
            ('top_cone', props.show_top_cone, props.color_front_top_cone, props.color_back_top_cone, "TopCone"),
            ('mid', props.show_mid, props.color_front_mid, props.color_back_mid, "Middle"),
            ('bot_cone', props.show_bot_cone, props.color_front_bot_cone, props.color_back_bot_cone, "BotCone"),
            ('bot_cap', props.show_bot_cap, props.color_front_bot_cap, props.color_back_bot_cap, "BotCap"),
            ('bot_rays', props.show_bot_rays, props.color_front_bot_rays, props.color_back_bot_rays, "BotRays"),
        ]
        
        active_obj = None
        bpy.ops.object.select_all(action='DESELECT')
        
        for key, is_show, c_front, c_back, mat_name in parts_config:
            obj = parts.get(key)
            if not obj: continue
            
            if is_show:
                obj.data.materials.append(create_unique_material(c_front, c_back, f"Mat_{mat_name}"))
                obj.name = f"Sphere_{mat_name}_{timestamp}"
                obj.select_set(True)
                active_obj = obj
            else:
                m = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if m and m.users == 0: bpy.data.meshes.remove(m)
                    
        for k, m in list(meshes.items()):
            if m and m.users == 0: bpy.data.meshes.remove(m)
                    
        if active_obj: context.view_layer.objects.active = active_obj
        self.report({'INFO'}, "Created Interferometer Parts")
        return {'FINISHED'}

class OT_CopyFullScript(Operator):
    bl_idname = f"{OP_PREFIX}.copy_script"
    bl_label = "Copy Script"
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        target_text = None
        for t in bpy.data.texts:
            if SOURCE_ID_TAG in t.as_string():
                target_text = t; break
        
        if not target_text:
            self.report({'ERROR'}, "Script source not found.")
            return {'CANCELLED'}

        code = target_text.as_string()
        l = props.sphere_loc
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_preview": {props.show_preview},\n'
        new_dict += f'    "sphere_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
        new_dict += f'    "sphere_radius": {props.sphere_radius:.4f},\n'
        new_dict += f'    "cone_angle": {props.cone_angle:.4f},\n'
        new_dict += f'    "split_offset": {props.split_offset:.4f},\n'
        
        for key in ["top_cap", "top_cone", "mid", "bot_cone", "bot_cap", "bot_rays"]:
            s = getattr(props, f"show_{key}")
            cf = getattr(props, f"color_front_{key}")
            cb = getattr(props, f"color_back_{key}")
            
            new_dict += f'    "show_{key}": {s},\n'
            if hasattr(props, f"cap_{key}"):
                cap_s = getattr(props, f"cap_{key}")
                new_dict += f'    "cap_{key}": {cap_s},\n'
            
            if key == "bot_rays":
                new_dict += f'    "ray_count": {props.ray_count},\n'
                new_dict += f'    "ray_thickness": {props.ray_thickness:.4f},\n'
                new_dict += f'    "ray_offset": {props.ray_offset:.4f},\n'

            new_dict += f'    "color_front_{key}": ({cf[0]:.4f}, {cf[1]:.4f}, {cf[2]:.4f}, {cf[3]:.4f}),\n'
            new_dict += f'    "color_back_{key}": ({cb[0]:.4f}, {cb[1]:.4f}, {cb[2]:.4f}, {cb[3]:.4f}),\n'
        
        new_dict += "}\n"

        try:
            start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
            pre, post = code.split(start)[0], code.split(end)[1]
            final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
            context.window_manager.clipboard = final
            self.report({'INFO'}, "Code copied!")
        except: return {'CANCELLED'}
        return {'FINISHED'}

class OT_Reset(Operator):
    bl_idname = f"{OP_PREFIX}.reset"
    bl_label = "Reset"
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME)
        p.sphere_loc = (0,0,0)
        p.sphere_radius = 30.0; p.cone_angle = 90.0; p.split_offset = 5.0
        
        p.show_top_cap = True; p.cap_top_cap = True; p.color_front_top_cap = (0.8, 0.2, 0.2, 0.8); p.color_back_top_cap = (0.4, 0.1, 0.1, 0.8)
        p.show_top_cone = True; p.cap_top_cone = True; p.color_front_top_cone = (0.8, 0.5, 0.2, 0.8); p.color_back_top_cone = (0.4, 0.25, 0.1, 0.8)
        p.show_mid = True; p.cap_mid = True; p.color_front_mid = (0.2, 0.8, 0.2, 0.8); p.color_back_mid = (0.1, 0.4, 0.1, 0.8)
        p.show_bot_cone = True; p.cap_bot_cone = True; p.color_front_bot_cone = (0.2, 0.5, 0.8, 0.8); p.color_back_bot_cone = (0.1, 0.25, 0.4, 0.8)
        p.show_bot_cap = True; p.cap_bot_cap = True; p.color_front_bot_cap = (0.5, 0.2, 0.8, 0.8); p.color_back_bot_cap = (0.25, 0.1, 0.4, 0.8)
        
        p.show_bot_rays = True; p.ray_count = 36; p.ray_thickness = 0.5; p.ray_offset = 10.0
        p.color_front_bot_rays = (0.9, 0.9, 0.2, 0.8); p.color_back_bot_rays = (0.5, 0.5, 0.1, 0.8)
        return {'FINISHED'}

class OT_OpenUrl(Operator):
    bl_idname = f"{OP_PREFIX}.open_url"; bl_label = "Open URL"; url: StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

class OT_RemoveAddon(Operator):
    bl_idname = f"{OP_PREFIX}.remove_addon"; bl_label = "Remove Addon"
    def execute(self, context):
        bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
        return {'FINISHED'}

# ==============================================================================
#  PANELS
# ==============================================================================

class PT_MainPanel(Panel):
    bl_label = "Interferometer Generator"
    bl_idname = f"{PREFIX}_PT_main"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: layout.label(text="Reload Script"); return

        row = layout.row()
        row.scale_y = 1.2
        row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
        layout.separator()

        layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
        
        box = layout.box()
        if not props.show_preview: box.label(text="Preview is Hidden", icon='INFO')
        box.prop(props, "sphere_loc")
        box.prop(props, "sphere_radius")
        box.prop(props, "cone_angle")
        box.prop(props, "split_offset")
        
        layout.separator()
        
        parts_box = layout.box()
        parts_box.label(text="Parts Selection & Colors", icon='MATERIAL')
        
        parts_ui = [
            ("top_cap", "Top Cap", "cap_top_cap"),
            ("top_cone", "Top Cone", "cap_top_cone"),
            ("mid", "Middle Sphere", "cap_mid"),
            ("bot_cone", "Bottom Cone", "cap_bot_cone"),
            ("bot_cap", "Bottom Cap", "cap_bot_cap"),
        ]
        
        for key, label, cap_prop in parts_ui:
            is_show = getattr(props, f"show_{key}")
            icon = 'RESTRICT_VIEW_OFF' if is_show else 'RESTRICT_VIEW_ON'
            
            p_box = parts_box.box()
            row = p_box.row(align=True)
            row.prop(props, f"show_{key}", icon=icon, text=label)
            if cap_prop: row.prop(props, cap_prop, text="Base", toggle=True)
            
            if is_show:
                col = p_box.column(align=True)
                col.prop(props, f"color_front_{key}", text="Front Color")
                col.prop(props, f"color_back_{key}", text="Back Color")

        layout.separator()
        layout.operator(OT_Reset.bl_idname, icon='LOOP_BACK', text="Reset Values")
        layout.separator()
        
        col = layout.column()
        col.scale_y = 1.5
        col.operator(OT_CreateSphere.bl_idname, icon='MESH_UVSPHERE', text="Create Selected Parts")

class PT_RaysPanel(Panel):
    bl_label = "Bottom Rays Settings"
    bl_idname = f"{PREFIX}_PT_rays"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return
        
        box = layout.box()
        icon = 'RESTRICT_VIEW_OFF' if props.show_bot_rays else 'RESTRICT_VIEW_ON'
        box.prop(props, "show_bot_rays", icon=icon, text="Enable Bottom Rays")
        
        if props.show_bot_rays:
            col = box.column(align=True)
            col.prop(props, "ray_count")
            col.prop(props, "ray_thickness")
            col.prop(props, "ray_offset")
            
            c_box = box.box()
            c_box.prop(props, "color_front_bot_rays", text="Front Color")
            c_box.prop(props, "color_back_bot_rays", text="Back Color")

class PT_LinksPanel(Panel):
    bl_label = "Links"; bl_idname = f"{PREFIX}_PT_links"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        for l in ADDON_LINKS: self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"]).url = l["url"]

class PT_RemovePanel(Panel):
    bl_label = "System"; bl_idname = f"{PREFIX}_PT_remove"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): self.layout.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Remove Addon")

# ==============================================================================
#  REGISTER
# ==============================================================================

classes = (PG_SphereProps, OT_CreateSphere, OT_CopyFullScript, OT_Reset, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, PT_RaysPanel, PT_LinksPanel, PT_RemovePanel)

# 初期化用の関数を追加
def init_preview():
    if bpy.context and hasattr(bpy.context, 'scene'):
        props = getattr(bpy.context.scene, PROPS_NAME, None)
        if props and props.show_preview:
            update_preview_geometry(bpy.context)
    return None

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_SphereProps))
    # スクリプト実行後、自動で1回描画を走らせるタイマー (修正ポイント)
    bpy.app.timers.register(init_preview, first_interval=0.2)

def unregister():
    if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
    for c in reversed(classes): bpy.utils.unregister_class(c)

if __name__ == "__main__": register()