blender Million 2026






# Copied: 12:15:00
import bpy
import bmesh
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime

# ==============================================================================
#  設定エリア
# ==============================================================================

PREFIX = "AngledCyl_3Sets20260313"
TAB_NAME = "[ 45° 3-Sets ]"

# ### ZIONAD_SOURCE_ID: ANGLED_CYL_3SETS_20260313 ###

bl_info = {
    "name": "45-Degree Cylinder & Cone Generator (3 Sets)",
    "author": "zionadchat",
    "version": (2, 5, 2),
    "blender": (5, 0, 0), # Blender 5.0以上 専用
    "location": "3D View > Sidebar",
    "description": "Generate 3 sets of 45-degree light cylinders and arbitrary velocity observer cylinders.",
    "category": "3D View",
}

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

ADDON_LINKS = (
    {"label": "z時間軸 45度 20260313", "url": "<https://www.notion.so/20230313-322f5dacaf43806b891efa5002e663e0>"},
    {"label": "Blender Python API", "url": "<https://docs.blender.org/api/current/index.html>"},
)

# ==============================================================================
#  デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_preview": True,
    "active_tab": 'SET3',
    "obs_velocity": -0.5000,
    "time_meet": 10.0000,
    "light_start_x": 10.0000,
    "light_start_y": 0.0000,
    "light_speed": 1.0000,
    "set1_show": True,
    "set1_pt1_xy": (0.0000, 0.0000),
    "set1_pt2_xy": (5.0000, 0.0000),
    "set1_pt3_xy": (10.0000, 0.0000),
    "set1_base_z": 0.0000,
    "set1_show_light": True,
    "set1_radius": 0.5000,
    "set1_cyl_color": (1.0000, 0.7602, 0.0196, 1.0000),
    "set1_show_obs": True,
    "set1_obs_velocity": 0.5000,
    "set1_obs_radius": 0.5000,
    "set1_obs_color": (0.0008, 0.0000, 0.6010, 1.0000),
    "set1_show_cones": True,
    "set1_cone_radius": 10.0000,
    "set1_cone_height": 10.0000,
    "set1_cone_color": (0.2000, 0.6000, 1.0000, 0.0778),
    "set2_show": False,
    "set2_pt1_xy": (10.0000, 0.0000),
    "set2_pt2_xy": (10.0000, 20.0000),
    "set2_pt3_xy": (10.0000, 40.0000),
    "set2_base_z": -10.0000,
    "set2_show_light": True,
    "set2_radius": 0.5000,
    "set2_cyl_color": (0.2000, 1.0000, 0.4000, 1.0000),
    "set2_show_obs": True,
    "set2_obs_velocity": 0.5000,
    "set2_obs_radius": 0.5000,
    "set2_obs_color": (1.0000, 0.8000, 0.1000, 1.0000),
    "set2_show_cones": True,
    "set2_cone_radius": 20.0000,
    "set2_cone_height": 20.0000,
    "set2_cone_color": (0.2000, 1.0000, 0.4000, 0.3000),
    "set3_show": True,
    "set3_pt1_xy": (10.0000, 0.0000),
    "set3_pt2_xy": (5.0000, 0.0000),
    "set3_pt3_xy": (0.0000, 0.0000),
    "set3_base_z": 0.0000,
    "set3_show_light": True,
    "set3_radius": 0.5000,
    "set3_cyl_color": (1.0000, 0.9208, 0.0092, 1.0000),
    "set3_show_obs": True,
    "set3_obs_velocity": 0.5000,
    "set3_obs_radius": 0.5000,
    "set3_obs_color": (1.0000, 0.0179, 0.1817, 1.0000),
    "set3_show_cones": True,
    "set3_cone_radius": 10.0000,
    "set3_cone_height": 10.0000,
    "set3_cone_color": (1.0000, 0.1707, 0.6135, 0.0383),
}
# <END_DICT>

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

def get_or_create_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name)
    
    mat.use_nodes = True
    tree = mat.node_tree
    tree.nodes.clear()
    
    bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
    bsdf.location = (-200, 0)
    bsdf.inputs["Base Color"].default_value = color
    
    if "Alpha" in bsdf.inputs:
        bsdf.inputs["Alpha"].default_value = color[3]
        
    out = tree.nodes.new("ShaderNodeOutputMaterial")
    out.location = (100, 0)
    
    tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
    mat.diffuse_color = color
    return mat

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

def create_single_cylinder(p1, p2, radius, collection, name, mat):
    length = (p2 - p1).length
    if length < 0.0001: return None
        
    mid_point = (p1 + p2) / 2.0
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=radius, radius2=radius, depth=length)
    
    direction = (p2 - p1).normalized()
    rot = Vector((0, 0, 1)).rotation_difference(direction)
    bmesh.ops.rotate(bm, cent=(0,0,0), matrix=rot.to_matrix(), verts=bm.verts)
    bmesh.ops.translate(bm, vec=mid_point, verts=bm.verts)
    
    mesh = bpy.data.meshes.new(name)
    bm.to_mesh(mesh)
    bm.free()
    
    obj = bpy.data.objects.new(name, mesh)
    collection.objects.link(obj)
    obj.data.materials.append(mat)
    return obj

def create_inverted_cone(location, radius, height, collection, name, mat):
    if height < 0.001 or radius < 0.001: return None
    
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=False, cap_tris=True, segments=32, radius1=0.0, radius2=radius, depth=height)
    
    bmesh.ops.translate(bm, vec=(0, 0, height / 2.0), verts=bm.verts)
    bmesh.ops.translate(bm, vec=location, verts=bm.verts)
    
    mesh = bpy.data.meshes.new(name)
    bm.to_mesh(mesh)
    bm.free()
    
    obj = bpy.data.objects.new(name, mesh)
    collection.objects.link(obj)
    obj.data.materials.append(mat)
    return obj

def build_all_sets(props, collection, is_preview=False):
    objs =[]
    for i in range(1, 4):
        prefix = f"set{i}"
        if not getattr(props, f"{prefix}_show"): continue
            
        x1, y1 = getattr(props, f"{prefix}_pt1_xy")
        x2, y2 = getattr(props, f"{prefix}_pt2_xy")
        x3, y3 = getattr(props, f"{prefix}_pt3_xy")
        base_z = getattr(props, f"{prefix}_base_z")
        
        # 光円錐に基づく時間(Z)の計算
        dxy1 = math.hypot(x2 - x1, y2 - y1)
        z2 = base_z + dxy1  
        dxy2 = math.hypot(x3 - x2, y3 - y2)
        z3 = z2 + dxy2
        
        p1 = Vector((x1, y1, base_z))
        p2 = Vector((x2, y2, z2))
        p3 = Vector((x3, y3, z3))
        
        # 1. 光の円柱 (Light Cylinder: v=1.0c)
        if getattr(props, f"{prefix}_show_light"):
            cyl_rad = getattr(props, f"{prefix}_radius")
            cyl_col = getattr(props, f"{prefix}_cyl_color")
            cyl_mat_name = f"Mat_Prev_Light_{i}" if is_preview else f"Mat_Light_{i}_{datetime.now().strftime('%H%M%S')}"
            cyl_mat = get_or_create_material(cyl_mat_name, cyl_col)
            
            name1 = f"Prev_Set{i}_Light1" if is_preview else f"Set{i}_Light1_{datetime.now().strftime('%H%M%S')}"
            name2 = f"Prev_Set{i}_Light2" if is_preview else f"Set{i}_Light2_{datetime.now().strftime('%H%M%S')}"
            
            c1 = create_single_cylinder(p1, p2, cyl_rad, collection, name1, cyl_mat)
            if c1: objs.append(c1)
            c2 = create_single_cylinder(p2, p3, cyl_rad, collection, name2, cyl_mat)
            if c2: objs.append(c2)
            
        # 2. 観測者の円柱 (Observer Cylinder: Velocity v)
        if getattr(props, f"{prefix}_show_obs"):
            v = getattr(props, f"{prefix}_obs_velocity")
            obs_rad = getattr(props, f"{prefix}_obs_radius")
            obs_col = getattr(props, f"{prefix}_obs_color")
            obs_mat_name = f"Mat_Prev_Obs_{i}" if is_preview else f"Mat_Obs_{i}_{datetime.now().strftime('%H%M%S')}"
            obs_mat = get_or_create_material(obs_mat_name, obs_col)
            
            p1_obs = Vector((x1, y1, base_z))
            
            # 同じ時間(Z)で、指定速度(v)分だけXY平面を進む
            p2_x_obs = x1 + (x2 - x1) * v
            p2_y_obs = y1 + (y2 - y1) * v
            p2_obs = Vector((p2_x_obs, p2_y_obs, z2))
            
            p3_x_obs = p2_x_obs + (x3 - x2) * v
            p3_y_obs = p2_y_obs + (y3 - y2) * v
            p3_obs = Vector((p3_x_obs, p3_y_obs, z3))
            
            name1_obs = f"Prev_Set{i}_Obs1" if is_preview else f"Set{i}_Obs1_{datetime.now().strftime('%H%M%S')}"
            name2_obs = f"Prev_Set{i}_Obs2" if is_preview else f"Set{i}_Obs2_{datetime.now().strftime('%H%M%S')}"
            
            c1_obs = create_single_cylinder(p1_obs, p2_obs, obs_rad, collection, name1_obs, obs_mat)
            if c1_obs: objs.append(c1_obs)
            c2_obs = create_single_cylinder(p2_obs, p3_obs, obs_rad, collection, name2_obs, obs_mat)
            if c2_obs: objs.append(c2_obs)
            
        # 3. 逆さ円錐 (Height Cones)
        if getattr(props, f"{prefix}_show_cones"):
            cone_rad = getattr(props, f"{prefix}_cone_radius")
            cone_h = getattr(props, f"{prefix}_cone_height")
            cone_col = getattr(props, f"{prefix}_cone_color")
            
            cone_mat_name = f"Mat_Prev_Cone_{i}" if is_preview else f"Mat_Cone_{i}_{datetime.now().strftime('%H%M%S')}"
            cone_mat = get_or_create_material(cone_mat_name, cone_col)
            
            for idx, p in enumerate([p1, p2, p3]):
                c_name = f"Prev_Set{i}_Cone{idx+1}" if is_preview else f"Set{i}_Cone{idx+1}_{datetime.now().strftime('%H%M%S')}"
                cone_obj = create_inverted_cone(p, cone_rad, cone_h, collection, c_name, cone_mat)
                if cone_obj: objs.append(cone_obj)
                    
    return objs

# ==============================================================================
#  プレビュー用ロジック
# ==============================================================================

PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"

def clear_preview(context):
    col = bpy.data.collections.get(PREVIEW_COL_NAME)
    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("Prev_Set") and m.users == 0:
            bpy.data.meshes.remove(m)

def update_preview(context):
    props = getattr(context.scene, PROPS_NAME, None)
    if not props: return
    clear_preview(context)
    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)
        
    objs = build_all_sets(props, col, is_preview=True)
    for obj in objs: obj.display_type = 'TEXTURED'
        
    context.view_layer.update()

_timer = None
def delayed_update():
    global _timer
    _timer = None
    if bpy.context and bpy.context.scene: update_preview(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_CylinderProps(PropertyGroup):
    show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
    active_tab: EnumProperty(
        items=[('SET1', "Set 1", ""), ('SET2', "Set 2", ""), ('SET3', "Set 3", "")],
        default=CURRENT_DEFAULTS['active_tab']
    )
    
    # --- Calc Panel Props ---
    obs_velocity: FloatProperty(name="Observer Velocity (v)", default=CURRENT_DEFAULTS['obs_velocity'], update=on_update)
    time_meet: FloatProperty(name="Meeting Time (t_meet)", default=CURRENT_DEFAULTS['time_meet'], update=on_update)
    light_start_x: FloatProperty(name="Light Start X", default=CURRENT_DEFAULTS['light_start_x'], update=on_update)
    light_start_y: FloatProperty(name="Light Start Y", default=CURRENT_DEFAULTS['light_start_y'], update=on_update)
    light_speed: FloatProperty(name="Speed of Light (c)", default=CURRENT_DEFAULTS['light_speed'], update=on_update)
    
    # --- SET 1 ---
    set1_show: BoolProperty(default=CURRENT_DEFAULTS['set1_show'], update=on_update)
    set1_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt1_xy'], update=on_update)
    set1_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt2_xy'], update=on_update)
    set1_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt3_xy'], update=on_update)
    set1_base_z: FloatProperty(default=CURRENT_DEFAULTS['set1_base_z'], update=on_update)
    
    set1_show_light: BoolProperty(default=CURRENT_DEFAULTS['set1_show_light'], update=on_update)
    set1_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_radius'], min=0.01, update=on_update)
    set1_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cyl_color'], update=on_update)
    
    set1_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set1_show_obs'], update=on_update)
    set1_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set1_obs_velocity'], update=on_update)
    set1_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_obs_radius'], min=0.01, update=on_update)
    set1_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_obs_color'], update=on_update)
    
    set1_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set1_show_cones'], update=on_update)
    set1_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_radius'], min=0.01, update=on_update)
    set1_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_height'], min=0.01, update=on_update)
    set1_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cone_color'], update=on_update)

    # --- SET 2 ---
    set2_show: BoolProperty(default=CURRENT_DEFAULTS['set2_show'], update=on_update)
    set2_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt1_xy'], update=on_update)
    set2_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt2_xy'], update=on_update)
    set2_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt3_xy'], update=on_update)
    set2_base_z: FloatProperty(default=CURRENT_DEFAULTS['set2_base_z'], update=on_update)
    
    set2_show_light: BoolProperty(default=CURRENT_DEFAULTS['set2_show_light'], update=on_update)
    set2_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_radius'], min=0.01, update=on_update)
    set2_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cyl_color'], update=on_update)
    
    set2_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set2_show_obs'], update=on_update)
    set2_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set2_obs_velocity'], update=on_update)
    set2_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_obs_radius'], min=0.01, update=on_update)
    set2_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_obs_color'], update=on_update)

    set2_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set2_show_cones'], update=on_update)
    set2_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_radius'], min=0.01, update=on_update)
    set2_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_height'], min=0.01, update=on_update)
    set2_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cone_color'], update=on_update)

    # --- SET 3 ---
    set3_show: BoolProperty(default=CURRENT_DEFAULTS['set3_show'], update=on_update)
    set3_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt1_xy'], update=on_update)
    set3_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt2_xy'], update=on_update)
    set3_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt3_xy'], update=on_update)
    set3_base_z: FloatProperty(default=CURRENT_DEFAULTS['set3_base_z'], update=on_update)
    
    set3_show_light: BoolProperty(default=CURRENT_DEFAULTS['set3_show_light'], update=on_update)
    set3_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_radius'], min=0.01, update=on_update)
    set3_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cyl_color'], update=on_update)
    
    set3_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set3_show_obs'], update=on_update)
    set3_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set3_obs_velocity'], update=on_update)
    set3_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_obs_radius'], min=0.01, update=on_update)
    set3_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_obs_color'], update=on_update)

    set3_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set3_show_cones'], update=on_update)
    set3_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_radius'], min=0.01, update=on_update)
    set3_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_height'], min=0.01, update=on_update)
    set3_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cone_color'], update=on_update)

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

class OT_CopyFullScript(Operator):
    bl_idname = f"{OP_PREFIX}.copy_script"
    bl_label = "Copy Script with Current Values"
    
    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 in Text Editor.")
            return {'CANCELLED'}

        code = target_text.as_string()
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_preview": {props.show_preview},\n'
        new_dict += f'    "active_tab": \'{props.active_tab}\',\n'
        
        new_dict += f'    "obs_velocity": {props.obs_velocity:.4f},\n'
        new_dict += f'    "time_meet": {props.time_meet:.4f},\n'
        new_dict += f'    "light_start_x": {props.light_start_x:.4f},\n'
        new_dict += f'    "light_start_y": {props.light_start_y:.4f},\n'
        new_dict += f'    "light_speed": {props.light_speed:.4f},\n'
        
        for i in range(1, 4):
            prefix = f"set{i}"
            show = getattr(props, f"{prefix}_show")
            p1 = getattr(props, f"{prefix}_pt1_xy")
            p2 = getattr(props, f"{prefix}_pt2_xy")
            p3 = getattr(props, f"{prefix}_pt3_xy")
            bz = getattr(props, f"{prefix}_base_z")
            
            sl = getattr(props, f"{prefix}_show_light")
            rad = getattr(props, f"{prefix}_radius")
            ccol = getattr(props, f"{prefix}_cyl_color")
            
            so = getattr(props, f"{prefix}_show_obs")
            ov = getattr(props, f"{prefix}_obs_velocity")
            orad = getattr(props, f"{prefix}_obs_radius")
            ocol = getattr(props, f"{prefix}_obs_color")
            
            scone = getattr(props, f"{prefix}_show_cones")
            crad = getattr(props, f"{prefix}_cone_radius")
            chgt = getattr(props, f"{prefix}_cone_height")
            cocol = getattr(props, f"{prefix}_cone_color")
            
            new_dict += f'    "{prefix}_show": {show},\n'
            new_dict += f'    "{prefix}_pt1_xy": ({p1[0]:.4f}, {p1[1]:.4f}),\n'
            new_dict += f'    "{prefix}_pt2_xy": ({p2[0]:.4f}, {p2[1]:.4f}),\n'
            new_dict += f'    "{prefix}_pt3_xy": ({p3[0]:.4f}, {p3[1]:.4f}),\n'
            new_dict += f'    "{prefix}_base_z": {bz:.4f},\n'
            
            new_dict += f'    "{prefix}_show_light": {sl},\n'
            new_dict += f'    "{prefix}_radius": {rad:.4f},\n'
            new_dict += f'    "{prefix}_cyl_color": ({ccol[0]:.4f}, {ccol[1]:.4f}, {ccol[2]:.4f}, {ccol[3]:.4f}),\n'
            
            new_dict += f'    "{prefix}_show_obs": {so},\n'
            new_dict += f'    "{prefix}_obs_velocity": {ov:.4f},\n'
            new_dict += f'    "{prefix}_obs_radius": {orad:.4f},\n'
            new_dict += f'    "{prefix}_obs_color": ({ocol[0]:.4f}, {ocol[1]:.4f}, {ocol[2]:.4f}, {ocol[3]:.4f}),\n'
            
            new_dict += f'    "{prefix}_show_cones": {scone},\n'
            new_dict += f'    "{prefix}_cone_radius": {crad:.4f},\n'
            new_dict += f'    "{prefix}_cone_height": {chgt:.4f},\n'
            new_dict += f'    "{prefix}_cone_color": ({cocol[0]:.4f}, {cocol[1]:.4f}, {cocol[2]:.4f}, {cocol[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 with current values!")
        except Exception as e:
            self.report({'ERROR'}, f"Copy Failed: {e}")
            return {'CANCELLED'}
        return {'FINISHED'}

class OT_CopyCalcResult(Operator):
    bl_idname = f"{OP_PREFIX}.copy_calc_result"
    bl_label = "Copy Calculation Result"
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        v = props.obs_velocity
        t_m = props.time_meet
        x_start = props.light_start_x
        y_start = props.light_start_y
        c = props.light_speed
        
        x_meet = v * t_m
        y_meet = 0.0
        dx = x_start - x_meet
        dy = y_start - y_meet
        dist = math.sqrt(dx**2 + dy**2)
        t_travel = dist / c if c != 0 else 0
        t_emit = t_m - t_travel
        
        text = (
            f"Observer Velocity (v): {v:.3f}\n"
            f"Meeting Time (t_meet): {t_m:.3f}\n"
            f"Light Start Position: X = {x_start:.3f}, Y = {y_start:.3f}\n"
            f"Speed of Light (c): {c:.3f}\n"
            f"----------------------------------------\n"
            f"[ Calculation Formula & Results ]\n"
            f"1. Meeting Point (Observer Pos): ({x_meet:.3f}, {y_meet:.3f})\n"
            f"     X_meet = {v:.3f} * {t_m:.3f} = {x_meet:.3f}\n\n"
            f"2. Distance (Light Travel Path):\n"
            f"     Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - {y_meet:.3f})²)\n"
            f"          = √({dx:.3f}² + {dy:.3f}²) = {dist:.3f}\n\n"
            f"3. Travel Time (t_travel):\n"
            f"     t_travel = Dist / c = {dist:.3f} / {c:.3f} = {t_travel:.3f}\n\n"
            f"4. Light Emit Time (t):\n"
            f"     t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}\n"
        )
        context.window_manager.clipboard = text
        self.report({'INFO'}, "Calculation results & formulas copied to clipboard!")
        return {'FINISHED'}

class OT_CopyIntersectionCalcResult(Operator):
    bl_idname = f"{OP_PREFIX}.copy_intersection_calc"
    bl_label = "Copy Intersection Result"
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        v = props.obs_velocity
        x_L = props.light_start_x
        y_L = props.light_start_y
        c = props.light_speed
        
        A = c**2 - v**2
        B = 2 * v * x_L
        C = -(x_L**2 + y_L**2)
        
        text = (
            f"Observer Velocity (v): {v:.3f}\n"
            f"Light Start Position: X = {x_L:.3f}, Y = {y_L:.3f}\n"
            f"Speed of Light (c): {c:.3f}\n"
            f"Observer and Light start at the same time (t=0).\n"
            f"----------------------------------------\n"
            f"[ Calculation Formula ]\n"
            f"Distance equation: (v*t - X)² + (0 - Y)² = (c*t)²\n"
            f"Quadratic form: (c² - v²)t² + (2*v*X)t - (X² + Y²) = 0\n"
            f"A = c² - v² = {A:.3f}\n"
            f"B = 2*v*X = {B:.3f}\n"
            f"C = -(X² + Y²) = {C:.3f}\n\n"
        )
        
        t_meet = -1
        if abs(A) < 1e-6:
            if abs(B) > 1e-6:
                t_meet = -C / B
                text += f"A ≈ 0, linear equation: B*t + C = 0 => t = {-C:.3f} / {B:.3f} = {t_meet:.3f}\n"
            else:
                text += f"A ≈ 0 and B ≈ 0, no solution.\n"
        else:
            D = B**2 - 4*A*C
            text += f"D = B² - 4*A*C = {D:.3f}\n"
            if D < 0:
                text += "D < 0, no real solution (they will never meet).\n"
            else:
                t1 = (-B + math.sqrt(D)) / (2*A)
                t2 = (-B - math.sqrt(D)) / (2*A)
                text += f"Roots: t1 = {t1:.3f}, t2 = {t2:.3f}\n"
                
                valid_ts = [t for t in (t1, t2) if t > 0]
                if valid_ts:
                    t_meet = min(valid_ts)
                    text += f"Valid positive time (t_meet): {t_meet:.3f}\n"
                else:
                    text += "No positive time solution (they met in the past).\n"
                    
        if t_meet > 0:
            x_meet = v * t_meet
            text += f"\n[ Results ]\n"
            text += f"Meeting Time (t): {t_meet:.3f}\n"
            text += f"Meeting Position: ({x_meet:.3f}, 0.000)\n"
        else:
            text += f"\n[ Results ]\n"
            text += "No valid intersection.\n"
            
        context.window_manager.clipboard = text
        self.report({'INFO'}, "Intersection calculation copied to clipboard!")
        return {'FINISHED'}

class OT_CreateSets(Operator):
    bl_idname = f"{OP_PREFIX}.create_sets"
    bl_label = "Create Displayed Sets"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        col = context.collection if context.collection else context.scene.collection
        objs = build_all_sets(props, col, is_preview=False)
        
        if objs:
            bpy.ops.object.select_all(action='DESELECT')
            for obj in objs: obj.select_set(True)
            context.view_layer.objects.active = objs[0]
            self.report({'INFO'}, f"Generated {len(objs)} Objects successfully!")
        else:
            self.report({'WARNING'}, "Nothing generated. Check settings.")
        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)
        self.report({'INFO'}, "Addon Removed successfully.")
        return {'FINISHED'}

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

class PT_CalcPanel(Panel):
    bl_label = "Light vs Observer Calc"
    bl_idname = f"{PREFIX}_PT_calc"
    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: return

        layout.prop(props, "obs_velocity")
        layout.prop(props, "time_meet")
        
        # ▼ 光線の出発位置を2行に分割 ▼
        layout.prop(props, "light_start_x")
        layout.prop(props, "light_start_y")
        
        layout.prop(props, "light_speed")
        
        # Calculation logic
        v = props.obs_velocity
        t_m = props.time_meet
        x_start = props.light_start_x
        y_start = props.light_start_y
        c = props.light_speed
        
        x_meet = v * t_m
        y_meet = 0.0
        dx = x_start - x_meet
        dy = y_start - y_meet
        dist = math.sqrt(dx**2 + dy**2)
        t_travel = dist / c if c != 0 else 0
        t_emit = t_m - t_travel
        
        box = layout.box()
        box.label(text="[ Formula & Results ]", icon='INFO')
        box.label(text=f"Meeting Point: ({x_meet:.3f}, {y_meet:.3f})")
        box.label(text=f"Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - 0)²) = {dist:.3f}")
        box.label(text=f"t_travel = {dist:.3f} / {c:.3f} = {t_travel:.3f}")
        box.label(text=f"t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}")
        
        layout.separator()
        layout.operator(OT_CopyCalcResult.bl_idname, icon='COPY_ID', text="Copy Formula & Results")

# ▼ 新規追加: 同時出発(t=0)計算用パネル ▼
class PT_IntersectionCalcPanel(Panel):
    bl_label = "Simultaneous Start (t=0) Calc"
    bl_idname = f"{PREFIX}_PT_intersection"
    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: return

        v = props.obs_velocity
        x_L = props.light_start_x
        y_L = props.light_start_y
        c = props.light_speed
        
        A = c**2 - v**2
        B = 2 * v * x_L
        C = -(x_L**2 + y_L**2)
        
        box = layout.box()
        box.label(text="[ Formula ]", icon='INFO')
        box.label(text="(c² - v²)t² + (2vX)t - (X² + Y²) = 0")
        box.label(text=f"A={A:.3f}, B={B:.3f}, C={C:.3f}")
        
        t_meet = -1
        if abs(A) < 1e-6:
            if abs(B) > 1e-6:
                t_meet = -C / B
        else:
            D = B**2 - 4*A*C
            if D >= 0:
                t1 = (-B + math.sqrt(D)) / (2*A)
                t2 = (-B - math.sqrt(D)) / (2*A)
                valid_ts = [t for t in (t1, t2) if t > 0]
                if valid_ts:
                    t_meet = min(valid_ts)
        
        res_box = layout.box()
        res_box.label(text="[ Results ]", icon='LIGHT')
        if t_meet > 0:
            x_meet = v * t_meet
            res_box.label(text=f"Meet Time (t): {t_meet:.3f}")
            res_box.label(text=f"Meet Pos: ({x_meet:.3f}, 0.000)")
        else:
            res_box.label(text="No valid intersection.")
            
        layout.separator()
        layout.operator(OT_CopyIntersectionCalcResult.bl_idname, icon='COPY_ID', text="Copy Formula & Results")

def draw_set_ui(layout, props, prefix):
    box = layout.box()
    if not getattr(props, f"{prefix}_show"):
        box.label(text="⚠️ This Set is Currently Hidden", icon='INFO')
        
    p_box = box.box()
    p_box.label(text="XY Plane Points", icon='MESH_PLANE')
    p_box.prop(props, f"{prefix}_pt1_xy", text="Point 1 (Start)")
    p_box.prop(props, f"{prefix}_pt2_xy", text="Point 2 (Mid)")
    p_box.prop(props, f"{prefix}_pt3_xy", text="Point 3 (End)")
    p_box.prop(props, f"{prefix}_base_z", text="Base Z (Start Height)")
    
    c_box = box.box()
    c_row = c_box.row()
    c_row.prop(props, f"{prefix}_show_light", text="45° Light Cylinder (v=1.0c)", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_light") else 'HIDE_ON', toggle=True)
    if getattr(props, f"{prefix}_show_light"):
        c_box.prop(props, f"{prefix}_radius", text="Thickness Radius")
        c_box.prop(props, f"{prefix}_cyl_color", text="Color")
        
    o_box = box.box()
    o_row = o_box.row()
    o_row.prop(props, f"{prefix}_show_obs", text="Observer Cylinder (Velocity v)", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_obs") else 'HIDE_ON', toggle=True)
    if getattr(props, f"{prefix}_show_obs"):
        o_box.prop(props, f"{prefix}_obs_velocity", text="Velocity (v, % of c)")
        o_box.prop(props, f"{prefix}_obs_radius", text="Thickness Radius")
        o_box.prop(props, f"{prefix}_obs_color", text="Color")
    
    co_box = box.box()
    co_row = co_box.row()
    co_row.prop(props, f"{prefix}_show_cones", text="Height Cones at P1, P2, P3", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_cones") else 'HIDE_ON', toggle=True)
    if getattr(props, f"{prefix}_show_cones"):
        co_box.prop(props, f"{prefix}_cone_radius", text="Cone Base Radius")
        co_box.prop(props, f"{prefix}_cone_height", text="Cone Height")
        co_box.prop(props, f"{prefix}_cone_color", text="Cone Color (Alpha)")

class PT_MainPanel(Panel):
    bl_label = "45-Deg Cyl & Cone (3 Sets)"
    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="Please Reload Script"); return

        # --- Copy Code Button ---
        row = layout.row()
        row.scale_y = 1.2
        row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Current Values")
        layout.separator()
            
        layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
        layout.separator()
        
        vis_box = layout.box()
        vis_row = vis_box.row(align=True)
        vis_row.prop(props, "set1_show", text="Set 1", icon='HIDE_OFF' if props.set1_show else 'HIDE_ON', toggle=True)
        vis_row.prop(props, "set2_show", text="Set 2", icon='HIDE_OFF' if props.set2_show else 'HIDE_ON', toggle=True)
        vis_row.prop(props, "set3_show", text="Set 3", icon='HIDE_OFF' if props.set3_show else 'HIDE_ON', toggle=True)
        
        layout.separator()
        row = layout.row(align=True)
        row.prop(props, "active_tab", expand=True)
        
        if props.active_tab == 'SET1': draw_set_ui(layout, props, "set1")
        elif props.active_tab == 'SET2': draw_set_ui(layout, props, "set2")
        elif props.active_tab == 'SET3': draw_set_ui(layout, props, "set3")
            
        layout.separator()
        col = layout.column()
        col.scale_y = 1.5
        col.operator(OT_CreateSets.bl_idname, icon='MOD_BUILD', text="Create Mesh Object(s)")

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_CylinderProps, 
    OT_CopyFullScript, 
    OT_CopyCalcResult,
    OT_CopyIntersectionCalcResult, # 追加
    OT_CreateSets, 
    OT_OpenUrl, 
    OT_RemoveAddon, 
    PT_MainPanel, 
    PT_CalcPanel,
    PT_IntersectionCalcPanel,      # 追加
    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(bpy.context)
    return None

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_CylinderProps))
    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()
# Copied: 12:01:14
# Copied: 11:45:32
# Copied: 04:00:00
import bpy
import bmesh
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime

# ==============================================================================
#  設定エリア
# ==============================================================================

PREFIX = "AngledCyl_3Sets20260313"
TAB_NAME = "[ 45° 3-Sets ]"

# ### ZIONAD_SOURCE_ID: ANGLED_CYL_3SETS_20260313 ###

bl_info = {
    "name": "45-Degree Cylinder & Cone Generator (3 Sets)",
    "author": "zionadchat",
    "version": (2, 5, 0),
    "blender": (5, 0, 0), # Blender 5.0以上 専用
    "location": "3D View > Sidebar",
    "description": "Generate 3 sets of 45-degree light cylinders and arbitrary velocity observer cylinders.",
    "category": "3D View",
}

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

ADDON_LINKS = (
    {"label": "z時間軸 45度 20260313", "url": "<https://www.notion.so/20230313-322f5dacaf43806b891efa5002e663e0>"},
    {"label": "Blender Python API", "url": "<https://docs.blender.org/api/current/index.html>"},
)

# ==============================================================================
#  デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_preview": True,
    "active_tab": 'SET3',
    "obs_velocity": -0.5000,
    "time_meet": 10.0000,
    "light_start_x": 10.0000,
    "light_start_y": 0.0000,
    "light_speed": 1.0000,
    "set1_show": True,
    "set1_pt1_xy": (0.0000, 0.0000),
    "set1_pt2_xy": (5.0000, 0.0000),
    "set1_pt3_xy": (10.0000, 0.0000),
    "set1_base_z": 0.0000,
    "set1_show_light": True,
    "set1_radius": 0.5000,
    "set1_cyl_color": (1.0000, 0.7602, 0.0196, 1.0000),
    "set1_show_obs": True,
    "set1_obs_velocity": 0.5000,
    "set1_obs_radius": 0.5000,
    "set1_obs_color": (0.0008, 0.0000, 0.6010, 1.0000),
    "set1_show_cones": True,
    "set1_cone_radius": 10.0000,
    "set1_cone_height": 10.0000,
    "set1_cone_color": (0.2000, 0.6000, 1.0000, 0.0778),
    "set2_show": False,
    "set2_pt1_xy": (10.0000, 0.0000),
    "set2_pt2_xy": (10.0000, 20.0000),
    "set2_pt3_xy": (10.0000, 40.0000),
    "set2_base_z": -10.0000,
    "set2_show_light": True,
    "set2_radius": 0.5000,
    "set2_cyl_color": (0.2000, 1.0000, 0.4000, 1.0000),
    "set2_show_obs": True,
    "set2_obs_velocity": 0.5000,
    "set2_obs_radius": 0.5000,
    "set2_obs_color": (1.0000, 0.8000, 0.1000, 1.0000),
    "set2_show_cones": True,
    "set2_cone_radius": 20.0000,
    "set2_cone_height": 20.0000,
    "set2_cone_color": (0.2000, 1.0000, 0.4000, 0.3000),
    "set3_show": True,
    "set3_pt1_xy": (10.0000, 0.0000),
    "set3_pt2_xy": (5.0000, 0.0000),
    "set3_pt3_xy": (0.0000, 0.0000),
    "set3_base_z": 0.0000,
    "set3_show_light": True,
    "set3_radius": 0.5000,
    "set3_cyl_color": (1.0000, 0.9208, 0.0092, 1.0000),
    "set3_show_obs": True,
    "set3_obs_velocity": 0.5000,
    "set3_obs_radius": 0.5000,
    "set3_obs_color": (1.0000, 0.0179, 0.1817, 1.0000),
    "set3_show_cones": True,
    "set3_cone_radius": 10.0000,
    "set3_cone_height": 10.0000,
    "set3_cone_color": (1.0000, 0.1707, 0.6135, 0.0383),
}
# <END_DICT>

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

def get_or_create_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name)
    
    mat.use_nodes = True
    tree = mat.node_tree
    tree.nodes.clear()
    
    bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
    bsdf.location = (-200, 0)
    bsdf.inputs["Base Color"].default_value = color
    
    if "Alpha" in bsdf.inputs:
        bsdf.inputs["Alpha"].default_value = color[3]
        
    out = tree.nodes.new("ShaderNodeOutputMaterial")
    out.location = (100, 0)
    
    tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
    mat.diffuse_color = color
    return mat

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

def create_single_cylinder(p1, p2, radius, collection, name, mat):
    length = (p2 - p1).length
    if length < 0.0001: return None
        
    mid_point = (p1 + p2) / 2.0
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=radius, radius2=radius, depth=length)
    
    direction = (p2 - p1).normalized()
    rot = Vector((0, 0, 1)).rotation_difference(direction)
    bmesh.ops.rotate(bm, cent=(0,0,0), matrix=rot.to_matrix(), verts=bm.verts)
    bmesh.ops.translate(bm, vec=mid_point, verts=bm.verts)
    
    mesh = bpy.data.meshes.new(name)
    bm.to_mesh(mesh)
    bm.free()
    
    obj = bpy.data.objects.new(name, mesh)
    collection.objects.link(obj)
    obj.data.materials.append(mat)
    return obj

def create_inverted_cone(location, radius, height, collection, name, mat):
    if height < 0.001 or radius < 0.001: return None
    
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=0.0, radius2=radius, depth=height)
    
    bmesh.ops.translate(bm, vec=(0, 0, height / 2.0), verts=bm.verts)
    bmesh.ops.translate(bm, vec=location, verts=bm.verts)
    
    mesh = bpy.data.meshes.new(name)
    bm.to_mesh(mesh)
    bm.free()
    
    obj = bpy.data.objects.new(name, mesh)
    collection.objects.link(obj)
    obj.data.materials.append(mat)
    return obj

def build_all_sets(props, collection, is_preview=False):
    objs =[]
    for i in range(1, 4):
        prefix = f"set{i}"
        if not getattr(props, f"{prefix}_show"): continue
            
        x1, y1 = getattr(props, f"{prefix}_pt1_xy")
        x2, y2 = getattr(props, f"{prefix}_pt2_xy")
        x3, y3 = getattr(props, f"{prefix}_pt3_xy")
        base_z = getattr(props, f"{prefix}_base_z")
        
        # 光円錐に基づく時間(Z)の計算
        dxy1 = math.hypot(x2 - x1, y2 - y1)
        z2 = base_z + dxy1  
        dxy2 = math.hypot(x3 - x2, y3 - y2)
        z3 = z2 + dxy2
        
        p1 = Vector((x1, y1, base_z))
        p2 = Vector((x2, y2, z2))
        p3 = Vector((x3, y3, z3))
        
        # 1. 光の円柱 (Light Cylinder: v=1.0c)
        if getattr(props, f"{prefix}_show_light"):
            cyl_rad = getattr(props, f"{prefix}_radius")
            cyl_col = getattr(props, f"{prefix}_cyl_color")
            cyl_mat_name = f"Mat_Prev_Light_{i}" if is_preview else f"Mat_Light_{i}_{datetime.now().strftime('%H%M%S')}"
            cyl_mat = get_or_create_material(cyl_mat_name, cyl_col)
            
            name1 = f"Prev_Set{i}_Light1" if is_preview else f"Set{i}_Light1_{datetime.now().strftime('%H%M%S')}"
            name2 = f"Prev_Set{i}_Light2" if is_preview else f"Set{i}_Light2_{datetime.now().strftime('%H%M%S')}"
            
            c1 = create_single_cylinder(p1, p2, cyl_rad, collection, name1, cyl_mat)
            if c1: objs.append(c1)
            c2 = create_single_cylinder(p2, p3, cyl_rad, collection, name2, cyl_mat)
            if c2: objs.append(c2)
            
        # 2. 観測者の円柱 (Observer Cylinder: Velocity v)
        if getattr(props, f"{prefix}_show_obs"):
            v = getattr(props, f"{prefix}_obs_velocity")
            obs_rad = getattr(props, f"{prefix}_obs_radius")
            obs_col = getattr(props, f"{prefix}_obs_color")
            obs_mat_name = f"Mat_Prev_Obs_{i}" if is_preview else f"Mat_Obs_{i}_{datetime.now().strftime('%H%M%S')}"
            obs_mat = get_or_create_material(obs_mat_name, obs_col)
            
            p1_obs = Vector((x1, y1, base_z))
            
            # 同じ時間(Z)で、指定速度(v)分だけXY平面を進む
            p2_x_obs = x1 + (x2 - x1) * v
            p2_y_obs = y1 + (y2 - y1) * v
            p2_obs = Vector((p2_x_obs, p2_y_obs, z2))
            
            p3_x_obs = p2_x_obs + (x3 - x2) * v
            p3_y_obs = p2_y_obs + (y3 - y2) * v
            p3_obs = Vector((p3_x_obs, p3_y_obs, z3))
            
            name1_obs = f"Prev_Set{i}_Obs1" if is_preview else f"Set{i}_Obs1_{datetime.now().strftime('%H%M%S')}"
            name2_obs = f"Prev_Set{i}_Obs2" if is_preview else f"Set{i}_Obs2_{datetime.now().strftime('%H%M%S')}"
            
            c1_obs = create_single_cylinder(p1_obs, p2_obs, obs_rad, collection, name1_obs, obs_mat)
            if c1_obs: objs.append(c1_obs)
            c2_obs = create_single_cylinder(p2_obs, p3_obs, obs_rad, collection, name2_obs, obs_mat)
            if c2_obs: objs.append(c2_obs)
            
        # 3. 逆さ円錐 (Height Cones)
        if getattr(props, f"{prefix}_show_cones"):
            cone_rad = getattr(props, f"{prefix}_cone_radius")
            cone_h = getattr(props, f"{prefix}_cone_height")
            cone_col = getattr(props, f"{prefix}_cone_color")
            
            cone_mat_name = f"Mat_Prev_Cone_{i}" if is_preview else f"Mat_Cone_{i}_{datetime.now().strftime('%H%M%S')}"
            cone_mat = get_or_create_material(cone_mat_name, cone_col)
            
            for idx, p in enumerate([p1, p2, p3]):
                c_name = f"Prev_Set{i}_Cone{idx+1}" if is_preview else f"Set{i}_Cone{idx+1}_{datetime.now().strftime('%H%M%S')}"
                cone_obj = create_inverted_cone(p, cone_rad, cone_h, collection, c_name, cone_mat)
                if cone_obj: objs.append(cone_obj)
                    
    return objs

# ==============================================================================
#  プレビュー用ロジック
# ==============================================================================

PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"

def clear_preview(context):
    col = bpy.data.collections.get(PREVIEW_COL_NAME)
    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("Prev_Set") and m.users == 0:
            bpy.data.meshes.remove(m)

def update_preview(context):
    props = getattr(context.scene, PROPS_NAME, None)
    if not props: return
    clear_preview(context)
    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)
        
    objs = build_all_sets(props, col, is_preview=True)
    for obj in objs: obj.display_type = 'TEXTURED'
        
    context.view_layer.update()

_timer = None
def delayed_update():
    global _timer
    _timer = None
    if bpy.context and bpy.context.scene: update_preview(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_CylinderProps(PropertyGroup):
    show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
    active_tab: EnumProperty(
        items=[('SET1', "Set 1", ""), ('SET2', "Set 2", ""), ('SET3', "Set 3", "")],
        default=CURRENT_DEFAULTS['active_tab']
    )
    
    # --- Calc Panel Props ---
    obs_velocity: FloatProperty(name="Observer Velocity (v)", default=CURRENT_DEFAULTS['obs_velocity'], update=on_update)
    time_meet: FloatProperty(name="Meeting Time (t_meet)", default=CURRENT_DEFAULTS['time_meet'], update=on_update)
    light_start_x: FloatProperty(name="Light Start X", default=CURRENT_DEFAULTS['light_start_x'], update=on_update)
    light_start_y: FloatProperty(name="Light Start Y", default=CURRENT_DEFAULTS['light_start_y'], update=on_update)
    light_speed: FloatProperty(name="Speed of Light (c)", default=CURRENT_DEFAULTS['light_speed'], update=on_update)
    
    # --- SET 1 ---
    set1_show: BoolProperty(default=CURRENT_DEFAULTS['set1_show'], update=on_update)
    set1_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt1_xy'], update=on_update)
    set1_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt2_xy'], update=on_update)
    set1_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt3_xy'], update=on_update)
    set1_base_z: FloatProperty(default=CURRENT_DEFAULTS['set1_base_z'], update=on_update)
    
    set1_show_light: BoolProperty(default=CURRENT_DEFAULTS['set1_show_light'], update=on_update)
    set1_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_radius'], min=0.01, update=on_update)
    set1_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cyl_color'], update=on_update)
    
    set1_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set1_show_obs'], update=on_update)
    set1_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set1_obs_velocity'], update=on_update)
    set1_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_obs_radius'], min=0.01, update=on_update)
    set1_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_obs_color'], update=on_update)
    
    set1_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set1_show_cones'], update=on_update)
    set1_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_radius'], min=0.01, update=on_update)
    set1_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_height'], min=0.01, update=on_update)
    set1_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cone_color'], update=on_update)

    # --- SET 2 ---
    set2_show: BoolProperty(default=CURRENT_DEFAULTS['set2_show'], update=on_update)
    set2_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt1_xy'], update=on_update)
    set2_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt2_xy'], update=on_update)
    set2_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt3_xy'], update=on_update)
    set2_base_z: FloatProperty(default=CURRENT_DEFAULTS['set2_base_z'], update=on_update)
    
    set2_show_light: BoolProperty(default=CURRENT_DEFAULTS['set2_show_light'], update=on_update)
    set2_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_radius'], min=0.01, update=on_update)
    set2_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cyl_color'], update=on_update)
    
    set2_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set2_show_obs'], update=on_update)
    set2_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set2_obs_velocity'], update=on_update)
    set2_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_obs_radius'], min=0.01, update=on_update)
    set2_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_obs_color'], update=on_update)

    set2_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set2_show_cones'], update=on_update)
    set2_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_radius'], min=0.01, update=on_update)
    set2_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_height'], min=0.01, update=on_update)
    set2_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cone_color'], update=on_update)

    # --- SET 3 ---
    set3_show: BoolProperty(default=CURRENT_DEFAULTS['set3_show'], update=on_update)
    set3_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt1_xy'], update=on_update)
    set3_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt2_xy'], update=on_update)
    set3_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt3_xy'], update=on_update)
    set3_base_z: FloatProperty(default=CURRENT_DEFAULTS['set3_base_z'], update=on_update)
    
    set3_show_light: BoolProperty(default=CURRENT_DEFAULTS['set3_show_light'], update=on_update)
    set3_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_radius'], min=0.01, update=on_update)
    set3_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cyl_color'], update=on_update)
    
    set3_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set3_show_obs'], update=on_update)
    set3_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set3_obs_velocity'], update=on_update)
    set3_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_obs_radius'], min=0.01, update=on_update)
    set3_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_obs_color'], update=on_update)

    set3_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set3_show_cones'], update=on_update)
    set3_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_radius'], min=0.01, update=on_update)
    set3_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_height'], min=0.01, update=on_update)
    set3_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cone_color'], update=on_update)

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

class OT_CopyFullScript(Operator):
    bl_idname = f"{OP_PREFIX}.copy_script"
    bl_label = "Copy Script with Current Values"
    
    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 in Text Editor.")
            return {'CANCELLED'}

        code = target_text.as_string()
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_preview": {props.show_preview},\n'
        new_dict += f'    "active_tab": \'{props.active_tab}\',\n'
        
        new_dict += f'    "obs_velocity": {props.obs_velocity:.4f},\n'
        new_dict += f'    "time_meet": {props.time_meet:.4f},\n'
        new_dict += f'    "light_start_x": {props.light_start_x:.4f},\n'
        new_dict += f'    "light_start_y": {props.light_start_y:.4f},\n'
        new_dict += f'    "light_speed": {props.light_speed:.4f},\n'
        
        for i in range(1, 4):
            prefix = f"set{i}"
            show = getattr(props, f"{prefix}_show")
            p1 = getattr(props, f"{prefix}_pt1_xy")
            p2 = getattr(props, f"{prefix}_pt2_xy")
            p3 = getattr(props, f"{prefix}_pt3_xy")
            bz = getattr(props, f"{prefix}_base_z")
            
            sl = getattr(props, f"{prefix}_show_light")
            rad = getattr(props, f"{prefix}_radius")
            ccol = getattr(props, f"{prefix}_cyl_color")
            
            so = getattr(props, f"{prefix}_show_obs")
            ov = getattr(props, f"{prefix}_obs_velocity")
            orad = getattr(props, f"{prefix}_obs_radius")
            ocol = getattr(props, f"{prefix}_obs_color")
            
            scone = getattr(props, f"{prefix}_show_cones")
            crad = getattr(props, f"{prefix}_cone_radius")
            chgt = getattr(props, f"{prefix}_cone_height")
            cocol = getattr(props, f"{prefix}_cone_color")
            
            new_dict += f'    "{prefix}_show": {show},\n'
            new_dict += f'    "{prefix}_pt1_xy": ({p1[0]:.4f}, {p1[1]:.4f}),\n'
            new_dict += f'    "{prefix}_pt2_xy": ({p2[0]:.4f}, {p2[1]:.4f}),\n'
            new_dict += f'    "{prefix}_pt3_xy": ({p3[0]:.4f}, {p3[1]:.4f}),\n'
            new_dict += f'    "{prefix}_base_z": {bz:.4f},\n'
            
            new_dict += f'    "{prefix}_show_light": {sl},\n'
            new_dict += f'    "{prefix}_radius": {rad:.4f},\n'
            new_dict += f'    "{prefix}_cyl_color": ({ccol[0]:.4f}, {ccol[1]:.4f}, {ccol[2]:.4f}, {ccol[3]:.4f}),\n'
            
            new_dict += f'    "{prefix}_show_obs": {so},\n'
            new_dict += f'    "{prefix}_obs_velocity": {ov:.4f},\n'
            new_dict += f'    "{prefix}_obs_radius": {orad:.4f},\n'
            new_dict += f'    "{prefix}_obs_color": ({ocol[0]:.4f}, {ocol[1]:.4f}, {ocol[2]:.4f}, {ocol[3]:.4f}),\n'
            
            new_dict += f'    "{prefix}_show_cones": {scone},\n'
            new_dict += f'    "{prefix}_cone_radius": {crad:.4f},\n'
            new_dict += f'    "{prefix}_cone_height": {chgt:.4f},\n'
            new_dict += f'    "{prefix}_cone_color": ({cocol[0]:.4f}, {cocol[1]:.4f}, {cocol[2]:.4f}, {cocol[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 with current values!")
        except Exception as e:
            self.report({'ERROR'}, f"Copy Failed: {e}")
            return {'CANCELLED'}
        return {'FINISHED'}

class OT_CopyCalcResult(Operator):
    bl_idname = f"{OP_PREFIX}.copy_calc_result"
    bl_label = "Copy Calculation Result"
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        v = props.obs_velocity
        t_m = props.time_meet
        x_start = props.light_start_x
        y_start = props.light_start_y
        c = props.light_speed
        
        x_meet = v * t_m
        y_meet = 0.0
        dx = x_start - x_meet
        dy = y_start - y_meet
        dist = math.sqrt(dx**2 + dy**2)
        t_travel = dist / c if c != 0 else 0
        t_emit = t_m - t_travel
        
        text = (
            f"Observer Velocity (v): {v:.3f}\n"
            f"Meeting Time (t_meet): {t_m:.3f}\n"
            f"Light Start Position: X = {x_start:.3f}, Y = {y_start:.3f}\n"
            f"Speed of Light (c): {c:.3f}\n"
            f"----------------------------------------\n"
            f"[ Calculation Formula & Results ]\n"
            f"1. Meeting Point (Observer Pos): ({x_meet:.3f}, {y_meet:.3f})\n"
            f"     X_meet = {v:.3f} * {t_m:.3f} = {x_meet:.3f}\n\n"
            f"2. Distance (Light Travel Path):\n"
            f"     Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - {y_meet:.3f})²)\n"
            f"          = √({dx:.3f}² + {dy:.3f}²) = {dist:.3f}\n\n"
            f"3. Travel Time (t_travel):\n"
            f"     t_travel = Dist / c = {dist:.3f} / {c:.3f} = {t_travel:.3f}\n\n"
            f"4. Light Emit Time (t):\n"
            f"     t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}\n"
        )
        context.window_manager.clipboard = text
        self.report({'INFO'}, "Calculation results & formulas copied to clipboard!")
        return {'FINISHED'}

class OT_CreateSets(Operator):
    bl_idname = f"{OP_PREFIX}.create_sets"
    bl_label = "Create Displayed Sets"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        col = context.collection if context.collection else context.scene.collection
        objs = build_all_sets(props, col, is_preview=False)
        
        if objs:
            bpy.ops.object.select_all(action='DESELECT')
            for obj in objs: obj.select_set(True)
            context.view_layer.objects.active = objs[0]
            self.report({'INFO'}, f"Generated {len(objs)} Objects successfully!")
        else:
            self.report({'WARNING'}, "Nothing generated. Check settings.")
        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)
        self.report({'INFO'}, "Addon Removed successfully.")
        return {'FINISHED'}

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

class PT_CalcPanel(Panel):
    bl_label = "Light vs Observer Calc"
    bl_idname = f"{PREFIX}_PT_calc"
    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: return

        layout.prop(props, "obs_velocity")
        layout.prop(props, "time_meet")
        
        row = layout.row(align=True)
        row.prop(props, "light_start_x")
        row.prop(props, "light_start_y")
        
        layout.prop(props, "light_speed")
        
        # Calculation logic
        v = props.obs_velocity
        t_m = props.time_meet
        x_start = props.light_start_x
        y_start = props.light_start_y
        c = props.light_speed
        
        x_meet = v * t_m
        y_meet = 0.0
        dx = x_start - x_meet
        dy = y_start - y_meet
        dist = math.sqrt(dx**2 + dy**2)
        t_travel = dist / c if c != 0 else 0
        t_emit = t_m - t_travel
        
        box = layout.box()
        box.label(text="[ Formula & Results ]", icon='INFO')
        box.label(text=f"Meeting Point: ({x_meet:.3f}, {y_meet:.3f})")
        box.label(text=f"Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - 0)²) = {dist:.3f}")
        box.label(text=f"t_travel = {dist:.3f} / {c:.3f} = {t_travel:.3f}")
        box.label(text=f"t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}")
        
        layout.separator()
        layout.operator(OT_CopyCalcResult.bl_idname, icon='COPY_ID', text="Copy Formula & Results")

def draw_set_ui(layout, props, prefix):
    box = layout.box()
    if not getattr(props, f"{prefix}_show"):
        box.label(text="⚠️ This Set is Currently Hidden", icon='INFO')
        
    p_box = box.box()
    p_box.label(text="XY Plane Points", icon='MESH_PLANE')
    p_box.prop(props, f"{prefix}_pt1_xy", text="Point 1 (Start)")
    p_box.prop(props, f"{prefix}_pt2_xy", text="Point 2 (Mid)")
    p_box.prop(props, f"{prefix}_pt3_xy", text="Point 3 (End)")
    p_box.prop(props, f"{prefix}_base_z", text="Base Z (Start Height)")
    
    c_box = box.box()
    c_row = c_box.row()
    c_row.prop(props, f"{prefix}_show_light", text="45° Light Cylinder (v=1.0c)", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_light") else 'HIDE_ON', toggle=True)
    if getattr(props, f"{prefix}_show_light"):
        c_box.prop(props, f"{prefix}_radius", text="Thickness Radius")
        c_box.prop(props, f"{prefix}_cyl_color", text="Color")
        
    o_box = box.box()
    o_row = o_box.row()
    o_row.prop(props, f"{prefix}_show_obs", text="Observer Cylinder (Velocity v)", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_obs") else 'HIDE_ON', toggle=True)
    if getattr(props, f"{prefix}_show_obs"):
        o_box.prop(props, f"{prefix}_obs_velocity", text="Velocity (v, % of c)")
        o_box.prop(props, f"{prefix}_obs_radius", text="Thickness Radius")
        o_box.prop(props, f"{prefix}_obs_color", text="Color")
    
    co_box = box.box()
    co_row = co_box.row()
    co_row.prop(props, f"{prefix}_show_cones", text="Height Cones at P1, P2, P3", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_cones") else 'HIDE_ON', toggle=True)
    if getattr(props, f"{prefix}_show_cones"):
        co_box.prop(props, f"{prefix}_cone_radius", text="Cone Base Radius")
        co_box.prop(props, f"{prefix}_cone_height", text="Cone Height")
        co_box.prop(props, f"{prefix}_cone_color", text="Cone Color (Alpha)")

class PT_MainPanel(Panel):
    bl_label = "45-Deg Cyl & Cone (3 Sets)"
    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="Please Reload Script"); return

        # --- Copy Code Button ---
        row = layout.row()
        row.scale_y = 1.2
        row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Current Values")
        layout.separator()
            
        layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
        layout.separator()
        
        vis_box = layout.box()
        vis_row = vis_box.row(align=True)
        vis_row.prop(props, "set1_show", text="Set 1", icon='HIDE_OFF' if props.set1_show else 'HIDE_ON', toggle=True)
        vis_row.prop(props, "set2_show", text="Set 2", icon='HIDE_OFF' if props.set2_show else 'HIDE_ON', toggle=True)
        vis_row.prop(props, "set3_show", text="Set 3", icon='HIDE_OFF' if props.set3_show else 'HIDE_ON', toggle=True)
        
        layout.separator()
        row = layout.row(align=True)
        row.prop(props, "active_tab", expand=True)
        
        if props.active_tab == 'SET1': draw_set_ui(layout, props, "set1")
        elif props.active_tab == 'SET2': draw_set_ui(layout, props, "set2")
        elif props.active_tab == 'SET3': draw_set_ui(layout, props, "set3")
            
        layout.separator()
        col = layout.column()
        col.scale_y = 1.5
        col.operator(OT_CreateSets.bl_idname, icon='MOD_BUILD', text="Create Mesh Object(s)")

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_CylinderProps, 
    OT_CopyFullScript, 
    OT_CopyCalcResult,
    OT_CreateSets, 
    OT_OpenUrl, 
    OT_RemoveAddon, 
    PT_MainPanel, 
    PT_CalcPanel,
    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(bpy.context)
    return None

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_CylinderProps))
    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()
# Copied: 11:45:32
# Copied: 04:00:00
import bpy
import bmesh
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime

# ==============================================================================
#  設定エリア
# ==============================================================================

PREFIX = "AngledCyl_3Sets20260313"
TAB_NAME = "[ 45° 3-Sets ]"

# ### ZIONAD_SOURCE_ID: ANGLED_CYL_3SETS_20260313 ###

bl_info = {
    "name": "45-Degree Cylinder & Cone Generator (3 Sets)",
    "author": "zionadchat",
    "version": (2, 5, 0),
    "blender": (5, 0, 0), # Blender 5.0以上 専用
    "location": "3D View > Sidebar",
    "description": "Generate 3 sets of 45-degree light cylinders and arbitrary velocity observer cylinders.",
    "category": "3D View",
}

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

ADDON_LINKS = (
    {"label": "z時間軸 45度 20260313", "url": "<https://www.notion.so/20230313-322f5dacaf43806b891efa5002e663e0>"},
    {"label": "Blender Python API", "url": "<https://docs.blender.org/api/current/index.html>"},
)

# ==============================================================================
#  デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_preview": True,
    "active_tab": 'SET1',
    "obs_velocity": 0.1000,
    "time_meet": 10.0000,
    "light_start_x": 10.0000,
    "light_start_y": 0.0000,
    "light_speed": 1.0000,
    "set1_show": True,
    "set1_pt1_xy": (0.0000, 0.0000),
    "set1_pt2_xy": (5.0000, 0.0000),
    "set1_pt3_xy": (10.0000, 0.0000),
    "set1_base_z": 0.0000,
    "set1_show_light": True,
    "set1_radius": 0.5000,
    "set1_cyl_color": (0.2000, 0.6000, 1.0000, 1.0000),
    "set1_show_obs": True,
    "set1_obs_velocity": 0.5000,
    "set1_obs_radius": 0.5000,
    "set1_obs_color": (1.0000, 0.8000, 0.1000, 1.0000),
    "set1_show_cones": True,
    "set1_cone_radius": 10.0000,
    "set1_cone_height": 10.0000,
    "set1_cone_color": (0.2000, 0.6000, 1.0000, 0.3000),
    "set2_show": False,
    "set2_pt1_xy": (0.0000, 10.0000),
    "set2_pt2_xy": (5.0000, 15.0000),
    "set2_pt3_xy": (10.0000, 10.0000),
    "set2_base_z": 0.0000,
    "set2_show_light": True,
    "set2_radius": 0.5000,
    "set2_cyl_color": (0.2000, 1.0000, 0.4000, 1.0000),
    "set2_show_obs": False,
    "set2_obs_velocity": 0.5000,
    "set2_obs_radius": 0.5000,
    "set2_obs_color": (1.0000, 0.8000, 0.1000, 1.0000),
    "set2_show_cones": True,
    "set2_cone_radius": 10.0000,
    "set2_cone_height": 10.0000,
    "set2_cone_color": (0.2000, 1.0000, 0.4000, 0.3000),
    "set3_show": False,
    "set3_pt1_xy": (0.0000, -10.0000),
    "set3_pt2_xy": (5.0000, -5.0000),
    "set3_pt3_xy": (10.0000, -10.0000),
    "set3_base_z": 0.0000,
    "set3_show_light": True,
    "set3_radius": 0.5000,
    "set3_cyl_color": (1.0000, 0.3000, 0.2000, 1.0000),
    "set3_show_obs": False,
    "set3_obs_velocity": 0.5000,
    "set3_obs_radius": 0.5000,
    "set3_obs_color": (1.0000, 0.8000, 0.1000, 1.0000),
    "set3_show_cones": True,
    "set3_cone_radius": 10.0000,
    "set3_cone_height": 10.0000,
    "set3_cone_color": (1.0000, 0.3000, 0.2000, 0.3000),
}
# <END_DICT>

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

def get_or_create_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name)
    
    mat.use_nodes = True
    tree = mat.node_tree
    tree.nodes.clear()
    
    bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
    bsdf.location = (-200, 0)
    bsdf.inputs["Base Color"].default_value = color
    
    if "Alpha" in bsdf.inputs:
        bsdf.inputs["Alpha"].default_value = color[3]
        
    out = tree.nodes.new("ShaderNodeOutputMaterial")
    out.location = (100, 0)
    
    tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
    mat.diffuse_color = color
    return mat

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

def create_single_cylinder(p1, p2, radius, collection, name, mat):
    length = (p2 - p1).length
    if length < 0.0001: return None
        
    mid_point = (p1 + p2) / 2.0
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=radius, radius2=radius, depth=length)
    
    direction = (p2 - p1).normalized()
    rot = Vector((0, 0, 1)).rotation_difference(direction)
    bmesh.ops.rotate(bm, cent=(0,0,0), matrix=rot.to_matrix(), verts=bm.verts)
    bmesh.ops.translate(bm, vec=mid_point, verts=bm.verts)
    
    mesh = bpy.data.meshes.new(name)
    bm.to_mesh(mesh)
    bm.free()
    
    obj = bpy.data.objects.new(name, mesh)
    collection.objects.link(obj)
    obj.data.materials.append(mat)
    return obj

def create_inverted_cone(location, radius, height, collection, name, mat):
    if height < 0.001 or radius < 0.001: return None
    
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=0.0, radius2=radius, depth=height)
    
    bmesh.ops.translate(bm, vec=(0, 0, height / 2.0), verts=bm.verts)
    bmesh.ops.translate(bm, vec=location, verts=bm.verts)
    
    mesh = bpy.data.meshes.new(name)
    bm.to_mesh(mesh)
    bm.free()
    
    obj = bpy.data.objects.new(name, mesh)
    collection.objects.link(obj)
    obj.data.materials.append(mat)
    return obj

def build_all_sets(props, collection, is_preview=False):
    objs =[]
    for i in range(1, 4):
        prefix = f"set{i}"
        if not getattr(props, f"{prefix}_show"): continue
            
        x1, y1 = getattr(props, f"{prefix}_pt1_xy")
        x2, y2 = getattr(props, f"{prefix}_pt2_xy")
        x3, y3 = getattr(props, f"{prefix}_pt3_xy")
        base_z = getattr(props, f"{prefix}_base_z")
        
        # 光円錐に基づく時間(Z)の計算
        dxy1 = math.hypot(x2 - x1, y2 - y1)
        z2 = base_z + dxy1  
        dxy2 = math.hypot(x3 - x2, y3 - y2)
        z3 = z2 + dxy2
        
        p1 = Vector((x1, y1, base_z))
        p2 = Vector((x2, y2, z2))
        p3 = Vector((x3, y3, z3))
        
        # 1. 光の円柱 (Light Cylinder: v=1.0c)
        if getattr(props, f"{prefix}_show_light"):
            cyl_rad = getattr(props, f"{prefix}_radius")
            cyl_col = getattr(props, f"{prefix}_cyl_color")
            cyl_mat_name = f"Mat_Prev_Light_{i}" if is_preview else f"Mat_Light_{i}_{datetime.now().strftime('%H%M%S')}"
            cyl_mat = get_or_create_material(cyl_mat_name, cyl_col)
            
            name1 = f"Prev_Set{i}_Light1" if is_preview else f"Set{i}_Light1_{datetime.now().strftime('%H%M%S')}"
            name2 = f"Prev_Set{i}_Light2" if is_preview else f"Set{i}_Light2_{datetime.now().strftime('%H%M%S')}"
            
            c1 = create_single_cylinder(p1, p2, cyl_rad, collection, name1, cyl_mat)
            if c1: objs.append(c1)
            c2 = create_single_cylinder(p2, p3, cyl_rad, collection, name2, cyl_mat)
            if c2: objs.append(c2)
            
        # 2. 観測者の円柱 (Observer Cylinder: Velocity v)
        if getattr(props, f"{prefix}_show_obs"):
            v = getattr(props, f"{prefix}_obs_velocity")
            obs_rad = getattr(props, f"{prefix}_obs_radius")
            obs_col = getattr(props, f"{prefix}_obs_color")
            obs_mat_name = f"Mat_Prev_Obs_{i}" if is_preview else f"Mat_Obs_{i}_{datetime.now().strftime('%H%M%S')}"
            obs_mat = get_or_create_material(obs_mat_name, obs_col)
            
            p1_obs = Vector((x1, y1, base_z))
            
            # 同じ時間(Z)で、指定速度(v)分だけXY平面を進む
            p2_x_obs = x1 + (x2 - x1) * v
            p2_y_obs = y1 + (y2 - y1) * v
            p2_obs = Vector((p2_x_obs, p2_y_obs, z2))
            
            p3_x_obs = p2_x_obs + (x3 - x2) * v
            p3_y_obs = p2_y_obs + (y3 - y2) * v
            p3_obs = Vector((p3_x_obs, p3_y_obs, z3))
            
            name1_obs = f"Prev_Set{i}_Obs1" if is_preview else f"Set{i}_Obs1_{datetime.now().strftime('%H%M%S')}"
            name2_obs = f"Prev_Set{i}_Obs2" if is_preview else f"Set{i}_Obs2_{datetime.now().strftime('%H%M%S')}"
            
            c1_obs = create_single_cylinder(p1_obs, p2_obs, obs_rad, collection, name1_obs, obs_mat)
            if c1_obs: objs.append(c1_obs)
            c2_obs = create_single_cylinder(p2_obs, p3_obs, obs_rad, collection, name2_obs, obs_mat)
            if c2_obs: objs.append(c2_obs)
            
        # 3. 逆さ円錐 (Height Cones)
        if getattr(props, f"{prefix}_show_cones"):
            cone_rad = getattr(props, f"{prefix}_cone_radius")
            cone_h = getattr(props, f"{prefix}_cone_height")
            cone_col = getattr(props, f"{prefix}_cone_color")
            
            cone_mat_name = f"Mat_Prev_Cone_{i}" if is_preview else f"Mat_Cone_{i}_{datetime.now().strftime('%H%M%S')}"
            cone_mat = get_or_create_material(cone_mat_name, cone_col)
            
            for idx, p in enumerate([p1, p2, p3]):
                c_name = f"Prev_Set{i}_Cone{idx+1}" if is_preview else f"Set{i}_Cone{idx+1}_{datetime.now().strftime('%H%M%S')}"
                cone_obj = create_inverted_cone(p, cone_rad, cone_h, collection, c_name, cone_mat)
                if cone_obj: objs.append(cone_obj)
                    
    return objs

# ==============================================================================
#  プレビュー用ロジック
# ==============================================================================

PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"

def clear_preview(context):
    col = bpy.data.collections.get(PREVIEW_COL_NAME)
    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("Prev_Set") and m.users == 0:
            bpy.data.meshes.remove(m)

def update_preview(context):
    props = getattr(context.scene, PROPS_NAME, None)
    if not props: return
    clear_preview(context)
    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)
        
    objs = build_all_sets(props, col, is_preview=True)
    for obj in objs: obj.display_type = 'TEXTURED'
        
    context.view_layer.update()

_timer = None
def delayed_update():
    global _timer
    _timer = None
    if bpy.context and bpy.context.scene: update_preview(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_CylinderProps(PropertyGroup):
    show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
    active_tab: EnumProperty(
        items=[('SET1', "Set 1", ""), ('SET2', "Set 2", ""), ('SET3', "Set 3", "")],
        default=CURRENT_DEFAULTS['active_tab']
    )
    
    # --- Calc Panel Props ---
    obs_velocity: FloatProperty(name="Observer Velocity (v)", default=CURRENT_DEFAULTS['obs_velocity'], update=on_update)
    time_meet: FloatProperty(name="Meeting Time (t_meet)", default=CURRENT_DEFAULTS['time_meet'], update=on_update)
    light_start_x: FloatProperty(name="Light Start X", default=CURRENT_DEFAULTS['light_start_x'], update=on_update)
    light_start_y: FloatProperty(name="Light Start Y", default=CURRENT_DEFAULTS['light_start_y'], update=on_update)
    light_speed: FloatProperty(name="Speed of Light (c)", default=CURRENT_DEFAULTS['light_speed'], update=on_update)
    
    # --- SET 1 ---
    set1_show: BoolProperty(default=CURRENT_DEFAULTS['set1_show'], update=on_update)
    set1_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt1_xy'], update=on_update)
    set1_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt2_xy'], update=on_update)
    set1_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt3_xy'], update=on_update)
    set1_base_z: FloatProperty(default=CURRENT_DEFAULTS['set1_base_z'], update=on_update)
    
    set1_show_light: BoolProperty(default=CURRENT_DEFAULTS['set1_show_light'], update=on_update)
    set1_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_radius'], min=0.01, update=on_update)
    set1_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cyl_color'], update=on_update)
    
    set1_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set1_show_obs'], update=on_update)
    set1_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set1_obs_velocity'], update=on_update)
    set1_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_obs_radius'], min=0.01, update=on_update)
    set1_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_obs_color'], update=on_update)
    
    set1_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set1_show_cones'], update=on_update)
    set1_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_radius'], min=0.01, update=on_update)
    set1_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_height'], min=0.01, update=on_update)
    set1_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cone_color'], update=on_update)

    # --- SET 2 ---
    set2_show: BoolProperty(default=CURRENT_DEFAULTS['set2_show'], update=on_update)
    set2_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt1_xy'], update=on_update)
    set2_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt2_xy'], update=on_update)
    set2_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt3_xy'], update=on_update)
    set2_base_z: FloatProperty(default=CURRENT_DEFAULTS['set2_base_z'], update=on_update)
    
    set2_show_light: BoolProperty(default=CURRENT_DEFAULTS['set2_show_light'], update=on_update)
    set2_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_radius'], min=0.01, update=on_update)
    set2_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cyl_color'], update=on_update)
    
    set2_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set2_show_obs'], update=on_update)
    set2_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set2_obs_velocity'], update=on_update)
    set2_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_obs_radius'], min=0.01, update=on_update)
    set2_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_obs_color'], update=on_update)

    set2_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set2_show_cones'], update=on_update)
    set2_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_radius'], min=0.01, update=on_update)
    set2_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_height'], min=0.01, update=on_update)
    set2_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cone_color'], update=on_update)

    # --- SET 3 ---
    set3_show: BoolProperty(default=CURRENT_DEFAULTS['set3_show'], update=on_update)
    set3_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt1_xy'], update=on_update)
    set3_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt2_xy'], update=on_update)
    set3_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt3_xy'], update=on_update)
    set3_base_z: FloatProperty(default=CURRENT_DEFAULTS['set3_base_z'], update=on_update)
    
    set3_show_light: BoolProperty(default=CURRENT_DEFAULTS['set3_show_light'], update=on_update)
    set3_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_radius'], min=0.01, update=on_update)
    set3_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cyl_color'], update=on_update)
    
    set3_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set3_show_obs'], update=on_update)
    set3_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set3_obs_velocity'], update=on_update)
    set3_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_obs_radius'], min=0.01, update=on_update)
    set3_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_obs_color'], update=on_update)

    set3_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set3_show_cones'], update=on_update)
    set3_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_radius'], min=0.01, update=on_update)
    set3_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_height'], min=0.01, update=on_update)
    set3_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cone_color'], update=on_update)

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

class OT_CopyFullScript(Operator):
    bl_idname = f"{OP_PREFIX}.copy_script"
    bl_label = "Copy Script with Current Values"
    
    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 in Text Editor.")
            return {'CANCELLED'}

        code = target_text.as_string()
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_preview": {props.show_preview},\n'
        new_dict += f'    "active_tab": \'{props.active_tab}\',\n'
        
        new_dict += f'    "obs_velocity": {props.obs_velocity:.4f},\n'
        new_dict += f'    "time_meet": {props.time_meet:.4f},\n'
        new_dict += f'    "light_start_x": {props.light_start_x:.4f},\n'
        new_dict += f'    "light_start_y": {props.light_start_y:.4f},\n'
        new_dict += f'    "light_speed": {props.light_speed:.4f},\n'
        
        for i in range(1, 4):
            prefix = f"set{i}"
            show = getattr(props, f"{prefix}_show")
            p1 = getattr(props, f"{prefix}_pt1_xy")
            p2 = getattr(props, f"{prefix}_pt2_xy")
            p3 = getattr(props, f"{prefix}_pt3_xy")
            bz = getattr(props, f"{prefix}_base_z")
            
            sl = getattr(props, f"{prefix}_show_light")
            rad = getattr(props, f"{prefix}_radius")
            ccol = getattr(props, f"{prefix}_cyl_color")
            
            so = getattr(props, f"{prefix}_show_obs")
            ov = getattr(props, f"{prefix}_obs_velocity")
            orad = getattr(props, f"{prefix}_obs_radius")
            ocol = getattr(props, f"{prefix}_obs_color")
            
            scone = getattr(props, f"{prefix}_show_cones")
            crad = getattr(props, f"{prefix}_cone_radius")
            chgt = getattr(props, f"{prefix}_cone_height")
            cocol = getattr(props, f"{prefix}_cone_color")
            
            new_dict += f'    "{prefix}_show": {show},\n'
            new_dict += f'    "{prefix}_pt1_xy": ({p1[0]:.4f}, {p1[1]:.4f}),\n'
            new_dict += f'    "{prefix}_pt2_xy": ({p2[0]:.4f}, {p2[1]:.4f}),\n'
            new_dict += f'    "{prefix}_pt3_xy": ({p3[0]:.4f}, {p3[1]:.4f}),\n'
            new_dict += f'    "{prefix}_base_z": {bz:.4f},\n'
            
            new_dict += f'    "{prefix}_show_light": {sl},\n'
            new_dict += f'    "{prefix}_radius": {rad:.4f},\n'
            new_dict += f'    "{prefix}_cyl_color": ({ccol[0]:.4f}, {ccol[1]:.4f}, {ccol[2]:.4f}, {ccol[3]:.4f}),\n'
            
            new_dict += f'    "{prefix}_show_obs": {so},\n'
            new_dict += f'    "{prefix}_obs_velocity": {ov:.4f},\n'
            new_dict += f'    "{prefix}_obs_radius": {orad:.4f},\n'
            new_dict += f'    "{prefix}_obs_color": ({ocol[0]:.4f}, {ocol[1]:.4f}, {ocol[2]:.4f}, {ocol[3]:.4f}),\n'
            
            new_dict += f'    "{prefix}_show_cones": {scone},\n'
            new_dict += f'    "{prefix}_cone_radius": {crad:.4f},\n'
            new_dict += f'    "{prefix}_cone_height": {chgt:.4f},\n'
            new_dict += f'    "{prefix}_cone_color": ({cocol[0]:.4f}, {cocol[1]:.4f}, {cocol[2]:.4f}, {cocol[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 with current values!")
        except Exception as e:
            self.report({'ERROR'}, f"Copy Failed: {e}")
            return {'CANCELLED'}
        return {'FINISHED'}

class OT_CopyCalcResult(Operator):
    bl_idname = f"{OP_PREFIX}.copy_calc_result"
    bl_label = "Copy Calculation Result"
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        v = props.obs_velocity
        t_m = props.time_meet
        x_start = props.light_start_x
        y_start = props.light_start_y
        c = props.light_speed
        
        x_meet = v * t_m
        y_meet = 0.0
        dx = x_start - x_meet
        dy = y_start - y_meet
        dist = math.sqrt(dx**2 + dy**2)
        t_travel = dist / c if c != 0 else 0
        t_emit = t_m - t_travel
        
        text = (
            f"Observer Velocity (v): {v:.3f}\n"
            f"Meeting Time (t_meet): {t_m:.3f}\n"
            f"Light Start Position: X = {x_start:.3f}, Y = {y_start:.3f}\n"
            f"Speed of Light (c): {c:.3f}\n"
            f"----------------------------------------\n"
            f"[ Calculation Formula & Results ]\n"
            f"1. Meeting Point (Observer Pos): ({x_meet:.3f}, {y_meet:.3f})\n"
            f"     X_meet = {v:.3f} * {t_m:.3f} = {x_meet:.3f}\n\n"
            f"2. Distance (Light Travel Path):\n"
            f"     Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - {y_meet:.3f})²)\n"
            f"          = √({dx:.3f}² + {dy:.3f}²) = {dist:.3f}\n\n"
            f"3. Travel Time (t_travel):\n"
            f"     t_travel = Dist / c = {dist:.3f} / {c:.3f} = {t_travel:.3f}\n\n"
            f"4. Light Emit Time (t):\n"
            f"     t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}\n"
        )
        context.window_manager.clipboard = text
        self.report({'INFO'}, "Calculation results & formulas copied to clipboard!")
        return {'FINISHED'}

class OT_CreateSets(Operator):
    bl_idname = f"{OP_PREFIX}.create_sets"
    bl_label = "Create Displayed Sets"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        col = context.collection if context.collection else context.scene.collection
        objs = build_all_sets(props, col, is_preview=False)
        
        if objs:
            bpy.ops.object.select_all(action='DESELECT')
            for obj in objs: obj.select_set(True)
            context.view_layer.objects.active = objs[0]
            self.report({'INFO'}, f"Generated {len(objs)} Objects successfully!")
        else:
            self.report({'WARNING'}, "Nothing generated. Check settings.")
        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)
        self.report({'INFO'}, "Addon Removed successfully.")
        return {'FINISHED'}

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

class PT_CalcPanel(Panel):
    bl_label = "Light vs Observer Calc"
    bl_idname = f"{PREFIX}_PT_calc"
    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: return

        layout.prop(props, "obs_velocity")
        layout.prop(props, "time_meet")
        
        row = layout.row(align=True)
        row.prop(props, "light_start_x")
        row.prop(props, "light_start_y")
        
        layout.prop(props, "light_speed")
        
        # Calculation logic
        v = props.obs_velocity
        t_m = props.time_meet
        x_start = props.light_start_x
        y_start = props.light_start_y
        c = props.light_speed
        
        x_meet = v * t_m
        y_meet = 0.0
        dx = x_start - x_meet
        dy = y_start - y_meet
        dist = math.sqrt(dx**2 + dy**2)
        t_travel = dist / c if c != 0 else 0
        t_emit = t_m - t_travel
        
        box = layout.box()
        box.label(text="[ Formula & Results ]", icon='INFO')
        box.label(text=f"Meeting Point: ({x_meet:.3f}, {y_meet:.3f})")
        box.label(text=f"Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - 0)²) = {dist:.3f}")
        box.label(text=f"t_travel = {dist:.3f} / {c:.3f} = {t_travel:.3f}")
        box.label(text=f"t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}")
        
        layout.separator()
        layout.operator(OT_CopyCalcResult.bl_idname, icon='COPY_ID', text="Copy Formula & Results")

def draw_set_ui(layout, props, prefix):
    box = layout.box()
    if not getattr(props, f"{prefix}_show"):
        box.label(text="⚠️ This Set is Currently Hidden", icon='INFO')
        
    p_box = box.box()
    p_box.label(text="XY Plane Points", icon='MESH_PLANE')
    p_box.prop(props, f"{prefix}_pt1_xy", text="Point 1 (Start)")
    p_box.prop(props, f"{prefix}_pt2_xy", text="Point 2 (Mid)")
    p_box.prop(props, f"{prefix}_pt3_xy", text="Point 3 (End)")
    p_box.prop(props, f"{prefix}_base_z", text="Base Z (Start Height)")
    
    c_box = box.box()
    c_row = c_box.row()
    c_row.prop(props, f"{prefix}_show_light", text="45° Light Cylinder (v=1.0c)", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_light") else 'HIDE_ON', toggle=True)
    if getattr(props, f"{prefix}_show_light"):
        c_box.prop(props, f"{prefix}_radius", text="Thickness Radius")
        c_box.prop(props, f"{prefix}_cyl_color", text="Color")
        
    o_box = box.box()
    o_row = o_box.row()
    o_row.prop(props, f"{prefix}_show_obs", text="Observer Cylinder (Velocity v)", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_obs") else 'HIDE_ON', toggle=True)
    if getattr(props, f"{prefix}_show_obs"):
        o_box.prop(props, f"{prefix}_obs_velocity", text="Velocity (v, % of c)")
        o_box.prop(props, f"{prefix}_obs_radius", text="Thickness Radius")
        o_box.prop(props, f"{prefix}_obs_color", text="Color")
    
    co_box = box.box()
    co_row = co_box.row()
    co_row.prop(props, f"{prefix}_show_cones", text="Height Cones at P1, P2, P3", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_cones") else 'HIDE_ON', toggle=True)
    if getattr(props, f"{prefix}_show_cones"):
        co_box.prop(props, f"{prefix}_cone_radius", text="Cone Base Radius")
        co_box.prop(props, f"{prefix}_cone_height", text="Cone Height")
        co_box.prop(props, f"{prefix}_cone_color", text="Cone Color (Alpha)")

class PT_MainPanel(Panel):
    bl_label = "45-Deg Cyl & Cone (3 Sets)"
    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="Please Reload Script"); return

        # --- Copy Code Button ---
        row = layout.row()
        row.scale_y = 1.2
        row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Current Values")
        layout.separator()
            
        layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
        layout.separator()
        
        vis_box = layout.box()
        vis_row = vis_box.row(align=True)
        vis_row.prop(props, "set1_show", text="Set 1", icon='HIDE_OFF' if props.set1_show else 'HIDE_ON', toggle=True)
        vis_row.prop(props, "set2_show", text="Set 2", icon='HIDE_OFF' if props.set2_show else 'HIDE_ON', toggle=True)
        vis_row.prop(props, "set3_show", text="Set 3", icon='HIDE_OFF' if props.set3_show else 'HIDE_ON', toggle=True)
        
        layout.separator()
        row = layout.row(align=True)
        row.prop(props, "active_tab", expand=True)
        
        if props.active_tab == 'SET1': draw_set_ui(layout, props, "set1")
        elif props.active_tab == 'SET2': draw_set_ui(layout, props, "set2")
        elif props.active_tab == 'SET3': draw_set_ui(layout, props, "set3")
            
        layout.separator()
        col = layout.column()
        col.scale_y = 1.5
        col.operator(OT_CreateSets.bl_idname, icon='MOD_BUILD', text="Create Mesh Object(s)")

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_CylinderProps, 
    OT_CopyFullScript, 
    OT_CopyCalcResult,
    OT_CreateSets, 
    OT_OpenUrl, 
    OT_RemoveAddon, 
    PT_MainPanel, 
    PT_CalcPanel,
    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(bpy.context)
    return None

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_CylinderProps))
    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()
# Copied: 04:00:00
import bpy
import bmesh
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime

# ==============================================================================
#  設定エリア
# ==============================================================================

PREFIX = "AngledCyl_3Sets20260313"
TAB_NAME = "[ 45° 3-Sets ]"

# ### ZIONAD_SOURCE_ID: ANGLED_CYL_3SETS_20260313 ###

bl_info = {
    "name": "45-Degree Cylinder & Cone Generator (3 Sets)",
    "author": "zionadchat",
    "version": (2, 5, 0),
    "blender": (5, 0, 0), # Blender 5.0以上 専用
    "location": "3D View > Sidebar",
    "description": "Generate 3 sets of 45-degree light cylinders and arbitrary velocity observer cylinders.",
    "category": "3D View",
}

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

ADDON_LINKS = (
    {"label": "z時間軸 45度 20260313", "url": "<https://www.notion.so/20230313-322f5dacaf43806b891efa5002e663e0>"},
    {"label": "Blender Python API", "url": "<https://docs.blender.org/api/current/index.html>"},
)

# ==============================================================================
#  デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_preview": True,
    "active_tab": 'SET1',
    "obs_velocity": 0.1000,
    "time_meet": 10.0000,
    "light_start_x": 10.0000,
    "light_start_y": 0.0000,
    "light_speed": 1.0000,
    
    "set1_show": True,
    "set1_pt1_xy": (0.0000, 0.0000),
    "set1_pt2_xy": (5.0000, 5.0000),
    "set1_pt3_xy": (10.0000, 0.0000),
    "set1_base_z": 0.0000,
    "set1_show_light": True,
    "set1_radius": 0.5000,
    "set1_cyl_color": (0.2000, 0.6000, 1.0000, 1.0000),
    "set1_show_obs": True,
    "set1_obs_velocity": 0.5000,
    "set1_obs_radius": 0.5000,
    "set1_obs_color": (1.0000, 0.8000, 0.1000, 1.0000),
    "set1_show_cones": True,
    "set1_cone_radius": 10.0000,
    "set1_cone_height": 10.0000,
    "set1_cone_color": (0.2000, 0.6000, 1.0000, 0.3000),
    
    "set2_show": True,
    "set2_pt1_xy": (0.0000, 10.0000),
    "set2_pt2_xy": (5.0000, 15.0000),
    "set2_pt3_xy": (10.0000, 10.0000),
    "set2_base_z": 0.0000,
    "set2_show_light": True,
    "set2_radius": 0.5000,
    "set2_cyl_color": (0.2000, 1.0000, 0.4000, 1.0000),
    "set2_show_obs": False,
    "set2_obs_velocity": 0.5000,
    "set2_obs_radius": 0.5000,
    "set2_obs_color": (1.0000, 0.8000, 0.1000, 1.0000),
    "set2_show_cones": True,
    "set2_cone_radius": 10.0000,
    "set2_cone_height": 10.0000,
    "set2_cone_color": (0.2000, 1.0000, 0.4000, 0.3000),
    
    "set3_show": True,
    "set3_pt1_xy": (0.0000, -10.0000),
    "set3_pt2_xy": (5.0000, -5.0000),
    "set3_pt3_xy": (10.0000, -10.0000),
    "set3_base_z": 0.0000,
    "set3_show_light": True,
    "set3_radius": 0.5000,
    "set3_cyl_color": (1.0000, 0.3000, 0.2000, 1.0000),
    "set3_show_obs": False,
    "set3_obs_velocity": 0.5000,
    "set3_obs_radius": 0.5000,
    "set3_obs_color": (1.0000, 0.8000, 0.1000, 1.0000),
    "set3_show_cones": True,
    "set3_cone_radius": 10.0000,
    "set3_cone_height": 10.0000,
    "set3_cone_color": (1.0000, 0.3000, 0.2000, 0.3000),
}
# <END_DICT>

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

def get_or_create_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name)
    
    mat.use_nodes = True
    tree = mat.node_tree
    tree.nodes.clear()
    
    bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
    bsdf.location = (-200, 0)
    bsdf.inputs["Base Color"].default_value = color
    
    if "Alpha" in bsdf.inputs:
        bsdf.inputs["Alpha"].default_value = color[3]
        
    out = tree.nodes.new("ShaderNodeOutputMaterial")
    out.location = (100, 0)
    
    tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
    mat.diffuse_color = color
    return mat

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

def create_single_cylinder(p1, p2, radius, collection, name, mat):
    length = (p2 - p1).length
    if length < 0.0001: return None
        
    mid_point = (p1 + p2) / 2.0
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=radius, radius2=radius, depth=length)
    
    direction = (p2 - p1).normalized()
    rot = Vector((0, 0, 1)).rotation_difference(direction)
    bmesh.ops.rotate(bm, cent=(0,0,0), matrix=rot.to_matrix(), verts=bm.verts)
    bmesh.ops.translate(bm, vec=mid_point, verts=bm.verts)
    
    mesh = bpy.data.meshes.new(name)
    bm.to_mesh(mesh)
    bm.free()
    
    obj = bpy.data.objects.new(name, mesh)
    collection.objects.link(obj)
    obj.data.materials.append(mat)
    return obj

def create_inverted_cone(location, radius, height, collection, name, mat):
    if height < 0.001 or radius < 0.001: return None
    
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=0.0, radius2=radius, depth=height)
    
    bmesh.ops.translate(bm, vec=(0, 0, height / 2.0), verts=bm.verts)
    bmesh.ops.translate(bm, vec=location, verts=bm.verts)
    
    mesh = bpy.data.meshes.new(name)
    bm.to_mesh(mesh)
    bm.free()
    
    obj = bpy.data.objects.new(name, mesh)
    collection.objects.link(obj)
    obj.data.materials.append(mat)
    return obj

def build_all_sets(props, collection, is_preview=False):
    objs =[]
    for i in range(1, 4):
        prefix = f"set{i}"
        if not getattr(props, f"{prefix}_show"): continue
            
        x1, y1 = getattr(props, f"{prefix}_pt1_xy")
        x2, y2 = getattr(props, f"{prefix}_pt2_xy")
        x3, y3 = getattr(props, f"{prefix}_pt3_xy")
        base_z = getattr(props, f"{prefix}_base_z")
        
        # 光円錐に基づく時間(Z)の計算
        dxy1 = math.hypot(x2 - x1, y2 - y1)
        z2 = base_z + dxy1  
        dxy2 = math.hypot(x3 - x2, y3 - y2)
        z3 = z2 + dxy2
        
        p1 = Vector((x1, y1, base_z))
        p2 = Vector((x2, y2, z2))
        p3 = Vector((x3, y3, z3))
        
        # 1. 光の円柱 (Light Cylinder: v=1.0c)
        if getattr(props, f"{prefix}_show_light"):
            cyl_rad = getattr(props, f"{prefix}_radius")
            cyl_col = getattr(props, f"{prefix}_cyl_color")
            cyl_mat_name = f"Mat_Prev_Light_{i}" if is_preview else f"Mat_Light_{i}_{datetime.now().strftime('%H%M%S')}"
            cyl_mat = get_or_create_material(cyl_mat_name, cyl_col)
            
            name1 = f"Prev_Set{i}_Light1" if is_preview else f"Set{i}_Light1_{datetime.now().strftime('%H%M%S')}"
            name2 = f"Prev_Set{i}_Light2" if is_preview else f"Set{i}_Light2_{datetime.now().strftime('%H%M%S')}"
            
            c1 = create_single_cylinder(p1, p2, cyl_rad, collection, name1, cyl_mat)
            if c1: objs.append(c1)
            c2 = create_single_cylinder(p2, p3, cyl_rad, collection, name2, cyl_mat)
            if c2: objs.append(c2)
            
        # 2. 観測者の円柱 (Observer Cylinder: Velocity v)
        if getattr(props, f"{prefix}_show_obs"):
            v = getattr(props, f"{prefix}_obs_velocity")
            obs_rad = getattr(props, f"{prefix}_obs_radius")
            obs_col = getattr(props, f"{prefix}_obs_color")
            obs_mat_name = f"Mat_Prev_Obs_{i}" if is_preview else f"Mat_Obs_{i}_{datetime.now().strftime('%H%M%S')}"
            obs_mat = get_or_create_material(obs_mat_name, obs_col)
            
            p1_obs = Vector((x1, y1, base_z))
            
            # 同じ時間(Z)で、指定速度(v)分だけXY平面を進む
            p2_x_obs = x1 + (x2 - x1) * v
            p2_y_obs = y1 + (y2 - y1) * v
            p2_obs = Vector((p2_x_obs, p2_y_obs, z2))
            
            p3_x_obs = p2_x_obs + (x3 - x2) * v
            p3_y_obs = p2_y_obs + (y3 - y2) * v
            p3_obs = Vector((p3_x_obs, p3_y_obs, z3))
            
            name1_obs = f"Prev_Set{i}_Obs1" if is_preview else f"Set{i}_Obs1_{datetime.now().strftime('%H%M%S')}"
            name2_obs = f"Prev_Set{i}_Obs2" if is_preview else f"Set{i}_Obs2_{datetime.now().strftime('%H%M%S')}"
            
            c1_obs = create_single_cylinder(p1_obs, p2_obs, obs_rad, collection, name1_obs, obs_mat)
            if c1_obs: objs.append(c1_obs)
            c2_obs = create_single_cylinder(p2_obs, p3_obs, obs_rad, collection, name2_obs, obs_mat)
            if c2_obs: objs.append(c2_obs)
            
        # 3. 逆さ円錐 (Height Cones)
        if getattr(props, f"{prefix}_show_cones"):
            cone_rad = getattr(props, f"{prefix}_cone_radius")
            cone_h = getattr(props, f"{prefix}_cone_height")
            cone_col = getattr(props, f"{prefix}_cone_color")
            
            cone_mat_name = f"Mat_Prev_Cone_{i}" if is_preview else f"Mat_Cone_{i}_{datetime.now().strftime('%H%M%S')}"
            cone_mat = get_or_create_material(cone_mat_name, cone_col)
            
            for idx, p in enumerate([p1, p2, p3]):
                c_name = f"Prev_Set{i}_Cone{idx+1}" if is_preview else f"Set{i}_Cone{idx+1}_{datetime.now().strftime('%H%M%S')}"
                cone_obj = create_inverted_cone(p, cone_rad, cone_h, collection, c_name, cone_mat)
                if cone_obj: objs.append(cone_obj)
                    
    return objs

# ==============================================================================
#  プレビュー用ロジック
# ==============================================================================

PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"

def clear_preview(context):
    col = bpy.data.collections.get(PREVIEW_COL_NAME)
    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("Prev_Set") and m.users == 0:
            bpy.data.meshes.remove(m)

def update_preview(context):
    props = getattr(context.scene, PROPS_NAME, None)
    if not props: return
    clear_preview(context)
    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)
        
    objs = build_all_sets(props, col, is_preview=True)
    for obj in objs: obj.display_type = 'TEXTURED'
        
    context.view_layer.update()

_timer = None
def delayed_update():
    global _timer
    _timer = None
    if bpy.context and bpy.context.scene: update_preview(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_CylinderProps(PropertyGroup):
    show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
    active_tab: EnumProperty(
        items=[('SET1', "Set 1", ""), ('SET2', "Set 2", ""), ('SET3', "Set 3", "")],
        default=CURRENT_DEFAULTS['active_tab']
    )
    
    # --- Calc Panel Props ---
    obs_velocity: FloatProperty(name="Observer Velocity (v)", default=CURRENT_DEFAULTS['obs_velocity'], update=on_update)
    time_meet: FloatProperty(name="Meeting Time (t_meet)", default=CURRENT_DEFAULTS['time_meet'], update=on_update)
    light_start_x: FloatProperty(name="Light Start X", default=CURRENT_DEFAULTS['light_start_x'], update=on_update)
    light_start_y: FloatProperty(name="Light Start Y", default=CURRENT_DEFAULTS['light_start_y'], update=on_update)
    light_speed: FloatProperty(name="Speed of Light (c)", default=CURRENT_DEFAULTS['light_speed'], update=on_update)
    
    # --- SET 1 ---
    set1_show: BoolProperty(default=CURRENT_DEFAULTS['set1_show'], update=on_update)
    set1_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt1_xy'], update=on_update)
    set1_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt2_xy'], update=on_update)
    set1_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt3_xy'], update=on_update)
    set1_base_z: FloatProperty(default=CURRENT_DEFAULTS['set1_base_z'], update=on_update)
    
    set1_show_light: BoolProperty(default=CURRENT_DEFAULTS['set1_show_light'], update=on_update)
    set1_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_radius'], min=0.01, update=on_update)
    set1_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cyl_color'], update=on_update)
    
    set1_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set1_show_obs'], update=on_update)
    set1_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set1_obs_velocity'], update=on_update)
    set1_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_obs_radius'], min=0.01, update=on_update)
    set1_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_obs_color'], update=on_update)
    
    set1_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set1_show_cones'], update=on_update)
    set1_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_radius'], min=0.01, update=on_update)
    set1_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_height'], min=0.01, update=on_update)
    set1_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cone_color'], update=on_update)

    # --- SET 2 ---
    set2_show: BoolProperty(default=CURRENT_DEFAULTS['set2_show'], update=on_update)
    set2_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt1_xy'], update=on_update)
    set2_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt2_xy'], update=on_update)
    set2_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt3_xy'], update=on_update)
    set2_base_z: FloatProperty(default=CURRENT_DEFAULTS['set2_base_z'], update=on_update)
    
    set2_show_light: BoolProperty(default=CURRENT_DEFAULTS['set2_show_light'], update=on_update)
    set2_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_radius'], min=0.01, update=on_update)
    set2_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cyl_color'], update=on_update)
    
    set2_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set2_show_obs'], update=on_update)
    set2_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set2_obs_velocity'], update=on_update)
    set2_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_obs_radius'], min=0.01, update=on_update)
    set2_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_obs_color'], update=on_update)

    set2_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set2_show_cones'], update=on_update)
    set2_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_radius'], min=0.01, update=on_update)
    set2_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_height'], min=0.01, update=on_update)
    set2_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cone_color'], update=on_update)

    # --- SET 3 ---
    set3_show: BoolProperty(default=CURRENT_DEFAULTS['set3_show'], update=on_update)
    set3_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt1_xy'], update=on_update)
    set3_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt2_xy'], update=on_update)
    set3_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt3_xy'], update=on_update)
    set3_base_z: FloatProperty(default=CURRENT_DEFAULTS['set3_base_z'], update=on_update)
    
    set3_show_light: BoolProperty(default=CURRENT_DEFAULTS['set3_show_light'], update=on_update)
    set3_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_radius'], min=0.01, update=on_update)
    set3_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cyl_color'], update=on_update)
    
    set3_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set3_show_obs'], update=on_update)
    set3_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set3_obs_velocity'], update=on_update)
    set3_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_obs_radius'], min=0.01, update=on_update)
    set3_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_obs_color'], update=on_update)

    set3_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set3_show_cones'], update=on_update)
    set3_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_radius'], min=0.01, update=on_update)
    set3_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_height'], min=0.01, update=on_update)
    set3_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cone_color'], update=on_update)

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

class OT_CopyFullScript(Operator):
    bl_idname = f"{OP_PREFIX}.copy_script"
    bl_label = "Copy Script with Current Values"
    
    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 in Text Editor.")
            return {'CANCELLED'}

        code = target_text.as_string()
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_preview": {props.show_preview},\n'
        new_dict += f'    "active_tab": \'{props.active_tab}\',\n'
        
        new_dict += f'    "obs_velocity": {props.obs_velocity:.4f},\n'
        new_dict += f'    "time_meet": {props.time_meet:.4f},\n'
        new_dict += f'    "light_start_x": {props.light_start_x:.4f},\n'
        new_dict += f'    "light_start_y": {props.light_start_y:.4f},\n'
        new_dict += f'    "light_speed": {props.light_speed:.4f},\n'
        
        for i in range(1, 4):
            prefix = f"set{i}"
            show = getattr(props, f"{prefix}_show")
            p1 = getattr(props, f"{prefix}_pt1_xy")
            p2 = getattr(props, f"{prefix}_pt2_xy")
            p3 = getattr(props, f"{prefix}_pt3_xy")
            bz = getattr(props, f"{prefix}_base_z")
            
            sl = getattr(props, f"{prefix}_show_light")
            rad = getattr(props, f"{prefix}_radius")
            ccol = getattr(props, f"{prefix}_cyl_color")
            
            so = getattr(props, f"{prefix}_show_obs")
            ov = getattr(props, f"{prefix}_obs_velocity")
            orad = getattr(props, f"{prefix}_obs_radius")
            ocol = getattr(props, f"{prefix}_obs_color")
            
            scone = getattr(props, f"{prefix}_show_cones")
            crad = getattr(props, f"{prefix}_cone_radius")
            chgt = getattr(props, f"{prefix}_cone_height")
            cocol = getattr(props, f"{prefix}_cone_color")
            
            new_dict += f'    "{prefix}_show": {show},\n'
            new_dict += f'    "{prefix}_pt1_xy": ({p1[0]:.4f}, {p1[1]:.4f}),\n'
            new_dict += f'    "{prefix}_pt2_xy": ({p2[0]:.4f}, {p2[1]:.4f}),\n'
            new_dict += f'    "{prefix}_pt3_xy": ({p3[0]:.4f}, {p3[1]:.4f}),\n'
            new_dict += f'    "{prefix}_base_z": {bz:.4f},\n'
            
            new_dict += f'    "{prefix}_show_light": {sl},\n'
            new_dict += f'    "{prefix}_radius": {rad:.4f},\n'
            new_dict += f'    "{prefix}_cyl_color": ({ccol[0]:.4f}, {ccol[1]:.4f}, {ccol[2]:.4f}, {ccol[3]:.4f}),\n'
            
            new_dict += f'    "{prefix}_show_obs": {so},\n'
            new_dict += f'    "{prefix}_obs_velocity": {ov:.4f},\n'
            new_dict += f'    "{prefix}_obs_radius": {orad:.4f},\n'
            new_dict += f'    "{prefix}_obs_color": ({ocol[0]:.4f}, {ocol[1]:.4f}, {ocol[2]:.4f}, {ocol[3]:.4f}),\n'
            
            new_dict += f'    "{prefix}_show_cones": {scone},\n'
            new_dict += f'    "{prefix}_cone_radius": {crad:.4f},\n'
            new_dict += f'    "{prefix}_cone_height": {chgt:.4f},\n'
            new_dict += f'    "{prefix}_cone_color": ({cocol[0]:.4f}, {cocol[1]:.4f}, {cocol[2]:.4f}, {cocol[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 with current values!")
        except Exception as e:
            self.report({'ERROR'}, f"Copy Failed: {e}")
            return {'CANCELLED'}
        return {'FINISHED'}

class OT_CopyCalcResult(Operator):
    bl_idname = f"{OP_PREFIX}.copy_calc_result"
    bl_label = "Copy Calculation Result"
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        v = props.obs_velocity
        t_m = props.time_meet
        x_start = props.light_start_x
        y_start = props.light_start_y
        c = props.light_speed
        
        x_meet = v * t_m
        y_meet = 0.0
        dx = x_start - x_meet
        dy = y_start - y_meet
        dist = math.sqrt(dx**2 + dy**2)
        t_travel = dist / c if c != 0 else 0
        t_emit = t_m - t_travel
        
        text = (
            f"Observer Velocity (v): {v:.3f}\n"
            f"Meeting Time (t_meet): {t_m:.3f}\n"
            f"Light Start Position: X = {x_start:.3f}, Y = {y_start:.3f}\n"
            f"Speed of Light (c): {c:.3f}\n"
            f"----------------------------------------\n"
            f"[ Calculation Formula & Results ]\n"
            f"1. Meeting Point (Observer Pos): ({x_meet:.3f}, {y_meet:.3f})\n"
            f"     X_meet = {v:.3f} * {t_m:.3f} = {x_meet:.3f}\n\n"
            f"2. Distance (Light Travel Path):\n"
            f"     Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - {y_meet:.3f})²)\n"
            f"          = √({dx:.3f}² + {dy:.3f}²) = {dist:.3f}\n\n"
            f"3. Travel Time (t_travel):\n"
            f"     t_travel = Dist / c = {dist:.3f} / {c:.3f} = {t_travel:.3f}\n\n"
            f"4. Light Emit Time (t):\n"
            f"     t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}\n"
        )
        context.window_manager.clipboard = text
        self.report({'INFO'}, "Calculation results & formulas copied to clipboard!")
        return {'FINISHED'}

class OT_CreateSets(Operator):
    bl_idname = f"{OP_PREFIX}.create_sets"
    bl_label = "Create Displayed Sets"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        col = context.collection if context.collection else context.scene.collection
        objs = build_all_sets(props, col, is_preview=False)
        
        if objs:
            bpy.ops.object.select_all(action='DESELECT')
            for obj in objs: obj.select_set(True)
            context.view_layer.objects.active = objs[0]
            self.report({'INFO'}, f"Generated {len(objs)} Objects successfully!")
        else:
            self.report({'WARNING'}, "Nothing generated. Check settings.")
        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)
        self.report({'INFO'}, "Addon Removed successfully.")
        return {'FINISHED'}

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

class PT_CalcPanel(Panel):
    bl_label = "Light vs Observer Calc"
    bl_idname = f"{PREFIX}_PT_calc"
    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: return

        layout.prop(props, "obs_velocity")
        layout.prop(props, "time_meet")
        
        row = layout.row(align=True)
        row.prop(props, "light_start_x")
        row.prop(props, "light_start_y")
        
        layout.prop(props, "light_speed")
        
        # Calculation logic
        v = props.obs_velocity
        t_m = props.time_meet
        x_start = props.light_start_x
        y_start = props.light_start_y
        c = props.light_speed
        
        x_meet = v * t_m
        y_meet = 0.0
        dx = x_start - x_meet
        dy = y_start - y_meet
        dist = math.sqrt(dx**2 + dy**2)
        t_travel = dist / c if c != 0 else 0
        t_emit = t_m - t_travel
        
        box = layout.box()
        box.label(text="[ Formula & Results ]", icon='INFO')
        box.label(text=f"Meeting Point: ({x_meet:.3f}, {y_meet:.3f})")
        box.label(text=f"Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - 0)²) = {dist:.3f}")
        box.label(text=f"t_travel = {dist:.3f} / {c:.3f} = {t_travel:.3f}")
        box.label(text=f"t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}")
        
        layout.separator()
        layout.operator(OT_CopyCalcResult.bl_idname, icon='COPY_ID', text="Copy Formula & Results")

def draw_set_ui(layout, props, prefix):
    box = layout.box()
    if not getattr(props, f"{prefix}_show"):
        box.label(text="⚠️ This Set is Currently Hidden", icon='INFO')
        
    p_box = box.box()
    p_box.label(text="XY Plane Points", icon='MESH_PLANE')
    p_box.prop(props, f"{prefix}_pt1_xy", text="Point 1 (Start)")
    p_box.prop(props, f"{prefix}_pt2_xy", text="Point 2 (Mid)")
    p_box.prop(props, f"{prefix}_pt3_xy", text="Point 3 (End)")
    p_box.prop(props, f"{prefix}_base_z", text="Base Z (Start Height)")
    
    c_box = box.box()
    c_row = c_box.row()
    c_row.prop(props, f"{prefix}_show_light", text="45° Light Cylinder (v=1.0c)", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_light") else 'HIDE_ON', toggle=True)
    if getattr(props, f"{prefix}_show_light"):
        c_box.prop(props, f"{prefix}_radius", text="Thickness Radius")
        c_box.prop(props, f"{prefix}_cyl_color", text="Color")
        
    o_box = box.box()
    o_row = o_box.row()
    o_row.prop(props, f"{prefix}_show_obs", text="Observer Cylinder (Velocity v)", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_obs") else 'HIDE_ON', toggle=True)
    if getattr(props, f"{prefix}_show_obs"):
        o_box.prop(props, f"{prefix}_obs_velocity", text="Velocity (v, % of c)")
        o_box.prop(props, f"{prefix}_obs_radius", text="Thickness Radius")
        o_box.prop(props, f"{prefix}_obs_color", text="Color")
    
    co_box = box.box()
    co_row = co_box.row()
    co_row.prop(props, f"{prefix}_show_cones", text="Height Cones at P1, P2, P3", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_cones") else 'HIDE_ON', toggle=True)
    if getattr(props, f"{prefix}_show_cones"):
        co_box.prop(props, f"{prefix}_cone_radius", text="Cone Base Radius")
        co_box.prop(props, f"{prefix}_cone_height", text="Cone Height")
        co_box.prop(props, f"{prefix}_cone_color", text="Cone Color (Alpha)")

class PT_MainPanel(Panel):
    bl_label = "45-Deg Cyl & Cone (3 Sets)"
    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="Please Reload Script"); return

        # --- Copy Code Button ---
        row = layout.row()
        row.scale_y = 1.2
        row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Current Values")
        layout.separator()
            
        layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
        layout.separator()
        
        vis_box = layout.box()
        vis_row = vis_box.row(align=True)
        vis_row.prop(props, "set1_show", text="Set 1", icon='HIDE_OFF' if props.set1_show else 'HIDE_ON', toggle=True)
        vis_row.prop(props, "set2_show", text="Set 2", icon='HIDE_OFF' if props.set2_show else 'HIDE_ON', toggle=True)
        vis_row.prop(props, "set3_show", text="Set 3", icon='HIDE_OFF' if props.set3_show else 'HIDE_ON', toggle=True)
        
        layout.separator()
        row = layout.row(align=True)
        row.prop(props, "active_tab", expand=True)
        
        if props.active_tab == 'SET1': draw_set_ui(layout, props, "set1")
        elif props.active_tab == 'SET2': draw_set_ui(layout, props, "set2")
        elif props.active_tab == 'SET3': draw_set_ui(layout, props, "set3")
            
        layout.separator()
        col = layout.column()
        col.scale_y = 1.5
        col.operator(OT_CreateSets.bl_idname, icon='MOD_BUILD', text="Create Mesh Object(s)")

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_CylinderProps, 
    OT_CopyFullScript, 
    OT_CopyCalcResult,
    OT_CreateSets, 
    OT_OpenUrl, 
    OT_RemoveAddon, 
    PT_MainPanel, 
    PT_CalcPanel,
    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(bpy.context)
    return None

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_CylinderProps))
    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()

# Copied: 02:27:00
import bpy
import bmesh
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime

# ==============================================================================
#  設定エリア
# ==============================================================================

PREFIX = "AngledCyl_3Sets20260313"
TAB_NAME = "[ 45° 3-Sets ]"

# ### ZIONAD_SOURCE_ID: ANGLED_CYL_3SETS_20260313 ###

bl_info = {
    "name": "45-Degree Cylinder & Cone Generator (3 Sets)",
    "author": "zionadchat",
    "version": (2, 4, 0),
    "blender": (5, 0, 0), # Blender 5.0以上 専用
    "location": "3D View > Sidebar",
    "description": "Generate 3 sets of 45-degree continuous cylinders with inverted height cones.",
    "category": "3D View",
}

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

ADDON_LINKS = (
    {"label": "z時間軸 45度 20260313", "url": "<https://www.notion.so/20230313-322f5dacaf43806b891efa5002e663e0>"},
    {"label": "Blender Python API", "url": "<https://docs.blender.org/api/current/index.html>"},
)

# ==============================================================================
#  デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_preview": True,
    "active_tab": 'SET1',
    "obs_velocity": 0.1000,
    "time_meet": 10.0000,
    "light_start_x": 10.0000,
    "light_start_y": 0.0000,
    "light_speed": 1.0000,
    "set1_show": True,
    "set1_pt1_xy": (0.0000, 0.0000),
    "set1_pt2_xy": (5.0000, 5.0000),
    "set1_pt3_xy": (10.0000, 0.0000),
    "set1_base_z": 0.0000,
    "set1_radius": 0.5000,
    "set1_cyl_color": (0.2000, 0.6000, 1.0000, 1.0000),
    "set1_show_cones": True,
    "set1_cone_radius": 10.0000,
    "set1_cone_height": 10.0000,
    "set1_cone_color": (0.2000, 0.6000, 1.0000, 0.3000),
    "set2_show": True,
    "set2_pt1_xy": (0.0000, 10.0000),
    "set2_pt2_xy": (5.0000, 15.0000),
    "set2_pt3_xy": (10.0000, 10.0000),
    "set2_base_z": 0.0000,
    "set2_radius": 0.5000,
    "set2_cyl_color": (0.2000, 1.0000, 0.4000, 1.0000),
    "set2_show_cones": True,
    "set2_cone_radius": 10.0000,
    "set2_cone_height": 10.0000,
    "set2_cone_color": (0.2000, 1.0000, 0.4000, 0.3000),
    "set3_show": True,
    "set3_pt1_xy": (0.0000, -10.0000),
    "set3_pt2_xy": (5.0000, -5.0000),
    "set3_pt3_xy": (10.0000, -10.0000),
    "set3_base_z": 0.0000,
    "set3_radius": 0.5000,
    "set3_cyl_color": (1.0000, 0.3000, 0.2000, 1.0000),
    "set3_show_cones": True,
    "set3_cone_radius": 10.0000,
    "set3_cone_height": 10.0000,
    "set3_cone_color": (1.0000, 0.3000, 0.2000, 0.3000),
}
# <END_DICT>

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

def get_or_create_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name)
    
    mat.use_nodes = True
    tree = mat.node_tree
    tree.nodes.clear()
    
    bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
    bsdf.location = (-200, 0)
    bsdf.inputs["Base Color"].default_value = color
    
    if "Alpha" in bsdf.inputs:
        bsdf.inputs["Alpha"].default_value = color[3]
        
    out = tree.nodes.new("ShaderNodeOutputMaterial")
    out.location = (100, 0)
    
    tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
    mat.diffuse_color = color
    return mat

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

def create_single_cylinder(p1, p2, radius, collection, name, mat):
    length = (p2 - p1).length
    if length < 0.0001: return None
        
    mid_point = (p1 + p2) / 2.0
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=radius, radius2=radius, depth=length)
    
    direction = (p2 - p1).normalized()
    rot = Vector((0, 0, 1)).rotation_difference(direction)
    bmesh.ops.rotate(bm, cent=(0,0,0), matrix=rot.to_matrix(), verts=bm.verts)
    bmesh.ops.translate(bm, vec=mid_point, verts=bm.verts)
    
    mesh = bpy.data.meshes.new(name)
    bm.to_mesh(mesh)
    bm.free()
    
    obj = bpy.data.objects.new(name, mesh)
    collection.objects.link(obj)
    obj.data.materials.append(mat)
    return obj

def create_inverted_cone(location, radius, height, collection, name, mat):
    if height < 0.001 or radius < 0.001: return None
    
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=0.0, radius2=radius, depth=height)
    
    bmesh.ops.translate(bm, vec=(0, 0, height / 2.0), verts=bm.verts)
    bmesh.ops.translate(bm, vec=location, verts=bm.verts)
    
    mesh = bpy.data.meshes.new(name)
    bm.to_mesh(mesh)
    bm.free()
    
    obj = bpy.data.objects.new(name, mesh)
    collection.objects.link(obj)
    obj.data.materials.append(mat)
    return obj

def build_all_sets(props, collection, is_preview=False):
    objs =[]
    for i in range(1, 4):
        prefix = f"set{i}"
        if not getattr(props, f"{prefix}_show"): continue
            
        x1, y1 = getattr(props, f"{prefix}_pt1_xy")
        x2, y2 = getattr(props, f"{prefix}_pt2_xy")
        x3, y3 = getattr(props, f"{prefix}_pt3_xy")
        base_z = getattr(props, f"{prefix}_base_z")
        
        dxy1 = math.hypot(x2 - x1, y2 - y1)
        z2 = base_z + dxy1  
        dxy2 = math.hypot(x3 - x2, y3 - y2)
        z3 = z2 + dxy2
        
        p1 = Vector((x1, y1, base_z))
        p2 = Vector((x2, y2, z2))
        p3 = Vector((x3, y3, z3))
        
        cyl_rad = getattr(props, f"{prefix}_radius")
        cyl_col = getattr(props, f"{prefix}_cyl_color")
        cyl_mat_name = f"Mat_Prev_Cyl_{i}" if is_preview else f"Mat_Cyl_{i}_{datetime.now().strftime('%H%M%S')}"
        cyl_mat = get_or_create_material(cyl_mat_name, cyl_col)
        
        name1 = f"Prev_Set{i}_Cyl1" if is_preview else f"Set{i}_Cyl1_{datetime.now().strftime('%H%M%S')}"
        name2 = f"Prev_Set{i}_Cyl2" if is_preview else f"Set{i}_Cyl2_{datetime.now().strftime('%H%M%S')}"
        
        c1 = create_single_cylinder(p1, p2, cyl_rad, collection, name1, cyl_mat)
        if c1: objs.append(c1)
        c2 = create_single_cylinder(p2, p3, cyl_rad, collection, name2, cyl_mat)
        if c2: objs.append(c2)
            
        if getattr(props, f"{prefix}_show_cones"):
            cone_rad = getattr(props, f"{prefix}_cone_radius")
            cone_h = getattr(props, f"{prefix}_cone_height")
            cone_col = getattr(props, f"{prefix}_cone_color")
            
            cone_mat_name = f"Mat_Prev_Cone_{i}" if is_preview else f"Mat_Cone_{i}_{datetime.now().strftime('%H%M%S')}"
            cone_mat = get_or_create_material(cone_mat_name, cone_col)
            
            for idx, p in enumerate([p1, p2, p3]):
                c_name = f"Prev_Set{i}_Cone{idx+1}" if is_preview else f"Set{i}_Cone{idx+1}_{datetime.now().strftime('%H%M%S')}"
                cone_obj = create_inverted_cone(p, cone_rad, cone_h, collection, c_name, cone_mat)
                if cone_obj: objs.append(cone_obj)
                    
    return objs

# ==============================================================================
#  プレビュー用ロジック
# ==============================================================================

PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"

def clear_preview(context):
    col = bpy.data.collections.get(PREVIEW_COL_NAME)
    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("Prev_Set") and m.users == 0:
            bpy.data.meshes.remove(m)

def update_preview(context):
    props = getattr(context.scene, PROPS_NAME, None)
    if not props: return
    clear_preview(context)
    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)
        
    objs = build_all_sets(props, col, is_preview=True)
    for obj in objs: obj.display_type = 'TEXTURED'
        
    context.view_layer.update()

_timer = None
def delayed_update():
    global _timer
    _timer = None
    if bpy.context and bpy.context.scene: update_preview(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_CylinderProps(PropertyGroup):
    show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
    active_tab: EnumProperty(
        items=[('SET1', "Set 1", ""), ('SET2', "Set 2", ""), ('SET3', "Set 3", "")],
        default=CURRENT_DEFAULTS['active_tab']
    )
    
    # --- Calc Panel Props ---
    obs_velocity: FloatProperty(name="Observer Velocity (v)", default=CURRENT_DEFAULTS['obs_velocity'], update=on_update)
    time_meet: FloatProperty(name="Meeting Time (t_meet)", default=CURRENT_DEFAULTS['time_meet'], update=on_update)
    light_start_x: FloatProperty(name="Light Start X", default=CURRENT_DEFAULTS['light_start_x'], update=on_update)
    light_start_y: FloatProperty(name="Light Start Y", default=CURRENT_DEFAULTS['light_start_y'], update=on_update)
    light_speed: FloatProperty(name="Speed of Light (c)", default=CURRENT_DEFAULTS['light_speed'], update=on_update)
    
    # --- SET 1 ---
    set1_show: BoolProperty(default=CURRENT_DEFAULTS['set1_show'], update=on_update)
    set1_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt1_xy'], update=on_update)
    set1_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt2_xy'], update=on_update)
    set1_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt3_xy'], update=on_update)
    set1_base_z: FloatProperty(default=CURRENT_DEFAULTS['set1_base_z'], update=on_update)
    set1_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_radius'], min=0.01, update=on_update)
    set1_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cyl_color'], update=on_update)
    set1_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set1_show_cones'], update=on_update)
    set1_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_radius'], min=0.01, update=on_update)
    set1_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_height'], min=0.01, update=on_update)
    set1_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cone_color'], update=on_update)

    # --- SET 2 ---
    set2_show: BoolProperty(default=CURRENT_DEFAULTS['set2_show'], update=on_update)
    set2_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt1_xy'], update=on_update)
    set2_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt2_xy'], update=on_update)
    set2_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt3_xy'], update=on_update)
    set2_base_z: FloatProperty(default=CURRENT_DEFAULTS['set2_base_z'], update=on_update)
    set2_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_radius'], min=0.01, update=on_update)
    set2_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cyl_color'], update=on_update)
    set2_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set2_show_cones'], update=on_update)
    set2_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_radius'], min=0.01, update=on_update)
    set2_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_height'], min=0.01, update=on_update)
    set2_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cone_color'], update=on_update)

    # --- SET 3 ---
    set3_show: BoolProperty(default=CURRENT_DEFAULTS['set3_show'], update=on_update)
    set3_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt1_xy'], update=on_update)
    set3_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt2_xy'], update=on_update)
    set3_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt3_xy'], update=on_update)
    set3_base_z: FloatProperty(default=CURRENT_DEFAULTS['set3_base_z'], update=on_update)
    set3_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_radius'], min=0.01, update=on_update)
    set3_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cyl_color'], update=on_update)
    set3_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set3_show_cones'], update=on_update)
    set3_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_radius'], min=0.01, update=on_update)
    set3_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_height'], min=0.01, update=on_update)
    set3_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cone_color'], update=on_update)

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

class OT_CopyFullScript(Operator):
    bl_idname = f"{OP_PREFIX}.copy_script"
    bl_label = "Copy Script with Current Values"
    
    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 in Text Editor.")
            return {'CANCELLED'}

        code = target_text.as_string()
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_preview": {props.show_preview},\n'
        new_dict += f'    "active_tab": \'{props.active_tab}\',\n'
        
        new_dict += f'    "obs_velocity": {props.obs_velocity:.4f},\n'
        new_dict += f'    "time_meet": {props.time_meet:.4f},\n'
        new_dict += f'    "light_start_x": {props.light_start_x:.4f},\n'
        new_dict += f'    "light_start_y": {props.light_start_y:.4f},\n'
        new_dict += f'    "light_speed": {props.light_speed:.4f},\n'
        
        for i in range(1, 4):
            prefix = f"set{i}"
            show = getattr(props, f"{prefix}_show")
            p1 = getattr(props, f"{prefix}_pt1_xy")
            p2 = getattr(props, f"{prefix}_pt2_xy")
            p3 = getattr(props, f"{prefix}_pt3_xy")
            bz = getattr(props, f"{prefix}_base_z")
            rad = getattr(props, f"{prefix}_radius")
            ccol = getattr(props, f"{prefix}_cyl_color")
            
            scone = getattr(props, f"{prefix}_show_cones")
            crad = getattr(props, f"{prefix}_cone_radius")
            chgt = getattr(props, f"{prefix}_cone_height")
            cocol = getattr(props, f"{prefix}_cone_color")
            
            new_dict += f'    "{prefix}_show": {show},\n'
            new_dict += f'    "{prefix}_pt1_xy": ({p1[0]:.4f}, {p1[1]:.4f}),\n'
            new_dict += f'    "{prefix}_pt2_xy": ({p2[0]:.4f}, {p2[1]:.4f}),\n'
            new_dict += f'    "{prefix}_pt3_xy": ({p3[0]:.4f}, {p3[1]:.4f}),\n'
            new_dict += f'    "{prefix}_base_z": {bz:.4f},\n'
            new_dict += f'    "{prefix}_radius": {rad:.4f},\n'
            new_dict += f'    "{prefix}_cyl_color": ({ccol[0]:.4f}, {ccol[1]:.4f}, {ccol[2]:.4f}, {ccol[3]:.4f}),\n'
            new_dict += f'    "{prefix}_show_cones": {scone},\n'
            new_dict += f'    "{prefix}_cone_radius": {crad:.4f},\n'
            new_dict += f'    "{prefix}_cone_height": {chgt:.4f},\n'
            new_dict += f'    "{prefix}_cone_color": ({cocol[0]:.4f}, {cocol[1]:.4f}, {cocol[2]:.4f}, {cocol[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 with current values!")
        except Exception as e:
            self.report({'ERROR'}, f"Copy Failed: {e}")
            return {'CANCELLED'}
        return {'FINISHED'}

class OT_CopyCalcResult(Operator):
    bl_idname = f"{OP_PREFIX}.copy_calc_result"
    bl_label = "Copy Calculation Result"
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        v = props.obs_velocity
        t_m = props.time_meet
        x_start = props.light_start_x
        y_start = props.light_start_y
        c = props.light_speed
        
        # Calculation logic
        x_meet = v * t_m
        y_meet = 0.0 # Observer travels on X-axis (Y=0)
        dx = x_start - x_meet
        dy = y_start - y_meet
        dist = math.sqrt(dx**2 + dy**2)
        t_travel = dist / c if c != 0 else 0
        t_emit = t_m - t_travel
        
        text = (
            f"Observer Velocity (v): {v:.3f}\n"
            f"Meeting Time (t_meet): {t_m:.3f}\n"
            f"Light Start Position: X = {x_start:.3f}, Y = {y_start:.3f}\n"
            f"Speed of Light (c): {c:.3f}\n"
            f"----------------------------------------\n"
            f"[ Calculation Formula & Results ]\n"
            f"1. Meeting Point (Observer Pos): ({x_meet:.3f}, {y_meet:.3f})\n"
            f"     X_meet = {v:.3f} * {t_m:.3f} = {x_meet:.3f}\n\n"
            f"2. Distance (Light Travel Path):\n"
            f"     Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - {y_meet:.3f})²)\n"
            f"          = √({dx:.3f}² + {dy:.3f}²) = {dist:.3f}\n\n"
            f"3. Travel Time (t_travel):\n"
            f"     t_travel = Dist / c = {dist:.3f} / {c:.3f} = {t_travel:.3f}\n\n"
            f"4. Light Emit Time (t):\n"
            f"     t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}\n"
        )
        context.window_manager.clipboard = text
        self.report({'INFO'}, "Calculation results & formulas copied to clipboard!")
        return {'FINISHED'}

class OT_CreateSets(Operator):
    bl_idname = f"{OP_PREFIX}.create_sets"
    bl_label = "Create Displayed Sets"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        col = context.collection if context.collection else context.scene.collection
        objs = build_all_sets(props, col, is_preview=False)
        
        if objs:
            bpy.ops.object.select_all(action='DESELECT')
            for obj in objs: obj.select_set(True)
            context.view_layer.objects.active = objs[0]
            self.report({'INFO'}, f"Generated {len(objs)} Objects successfully!")
        else:
            self.report({'WARNING'}, "Nothing generated. Check settings.")
        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)
        self.report({'INFO'}, "Addon Removed successfully.")
        return {'FINISHED'}

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

class PT_CalcPanel(Panel):
    bl_label = "Light vs Observer Calc"
    bl_idname = f"{PREFIX}_PT_calc"
    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: return

        layout.prop(props, "obs_velocity")
        layout.prop(props, "time_meet")
        
        row = layout.row(align=True)
        row.prop(props, "light_start_x")
        row.prop(props, "light_start_y")
        
        layout.prop(props, "light_speed")
        
        # Calculation logic
        v = props.obs_velocity
        t_m = props.time_meet
        x_start = props.light_start_x
        y_start = props.light_start_y
        c = props.light_speed
        
        x_meet = v * t_m
        y_meet = 0.0
        dx = x_start - x_meet
        dy = y_start - y_meet
        dist = math.sqrt(dx**2 + dy**2)
        t_travel = dist / c if c != 0 else 0
        t_emit = t_m - t_travel
        
        box = layout.box()
        box.label(text="[ Formula & Results ]", icon='INFO')
        box.label(text=f"Meeting Point: ({x_meet:.3f}, {y_meet:.3f})")
        box.label(text=f"Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - 0)²) = {dist:.3f}")
        box.label(text=f"t_travel = {dist:.3f} / {c:.3f} = {t_travel:.3f}")
        box.label(text=f"t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}")
        
        layout.separator()
        layout.operator(OT_CopyCalcResult.bl_idname, icon='COPY_ID', text="Copy Formula & Results")

def draw_set_ui(layout, props, prefix):
    box = layout.box()
    if not getattr(props, f"{prefix}_show"):
        box.label(text="⚠️ This Set is Currently Hidden", icon='INFO')
        
    p_box = box.box()
    p_box.label(text="XY Plane Points", icon='MESH_PLANE')
    p_box.prop(props, f"{prefix}_pt1_xy", text="Point 1 (Start)")
    p_box.prop(props, f"{prefix}_pt2_xy", text="Point 2 (Mid)")
    p_box.prop(props, f"{prefix}_pt3_xy", text="Point 3 (End)")
    
    c_box = box.box()
    c_box.label(text="45° Continuous Cylinders", icon='MESH_CYLINDER')
    c_box.prop(props, f"{prefix}_base_z", text="Base Z (Start Height)")
    c_box.prop(props, f"{prefix}_radius", text="Thickness Radius")
    c_box.prop(props, f"{prefix}_cyl_color", text="Color")
    
    co_box = box.box()
    co_row = co_box.row()
    co_row.prop(props, f"{prefix}_show_cones", text="Height Cones at P1, P2, P3", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_cones") else 'HIDE_ON', toggle=True)
    if getattr(props, f"{prefix}_show_cones"):
        co_box.prop(props, f"{prefix}_cone_radius", text="Cone Base Radius")
        co_box.prop(props, f"{prefix}_cone_height", text="Cone Height")
        co_box.prop(props, f"{prefix}_cone_color", text="Cone Color (Alpha)")

class PT_MainPanel(Panel):
    bl_label = "45-Deg Cyl & Cone (3 Sets)"
    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="Please Reload Script"); return

        # --- Copy Code Button ---
        row = layout.row()
        row.scale_y = 1.2
        row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Current Values")
        layout.separator()
            
        layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
        layout.separator()
        
        vis_box = layout.box()
        vis_row = vis_box.row(align=True)
        vis_row.prop(props, "set1_show", text="Set 1", icon='HIDE_OFF' if props.set1_show else 'HIDE_ON', toggle=True)
        vis_row.prop(props, "set2_show", text="Set 2", icon='HIDE_OFF' if props.set2_show else 'HIDE_ON', toggle=True)
        vis_row.prop(props, "set3_show", text="Set 3", icon='HIDE_OFF' if props.set3_show else 'HIDE_ON', toggle=True)
        
        layout.separator()
        row = layout.row(align=True)
        row.prop(props, "active_tab", expand=True)
        
        if props.active_tab == 'SET1': draw_set_ui(layout, props, "set1")
        elif props.active_tab == 'SET2': draw_set_ui(layout, props, "set2")
        elif props.active_tab == 'SET3': draw_set_ui(layout, props, "set3")
            
        layout.separator()
        col = layout.column()
        col.scale_y = 1.5
        col.operator(OT_CreateSets.bl_idname, icon='MOD_BUILD', text="Create Mesh Object(s)")

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_CylinderProps, 
    OT_CopyFullScript, 
    OT_CopyCalcResult,
    OT_CreateSets, 
    OT_OpenUrl, 
    OT_RemoveAddon, 
    PT_MainPanel, 
    PT_CalcPanel,
    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(bpy.context)
    return None

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_CylinderProps))
    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()

import bpy
import bmesh
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime

# ==============================================================================
#  設定エリア
# ==============================================================================

PREFIX = "AngledCyl2026"
TAB_NAME = "[ 45° Cylinder ]"

bl_info = {
    "name": "45-Degree Cylinder Generator",
    "author": "zionadchat",
    "version": (1, 0, 0),
    "blender": (5, 0, 0), # Blender 5.0以上 専用
    "location": "3D View > Sidebar",
    "description": "Generate a 45-degree cylinder from two XY points.",
    "category": "3D View",
}

OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"

ADDON_LINKS = (
    {"label": "Blender Python API", "url": "<https://docs.blender.org/api/current/index.html>"},
    {"label": "Math (Vector) Reference", "url": "<https://docs.blender.org/api/current/mathutils.html>"},
)

CURRENT_DEFAULTS = {
    "show_preview": True,
    "pt1_xy": (0.0000, 0.0000),
    "pt2_xy": (5.0000, 5.0000),
    "base_z": 0.0000,
    "radius": 1.0000,
    "cylinder_color": (0.2000, 0.6000, 0.8000, 1.0000)
}

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

def get_or_create_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name)
    
    mat.use_nodes = True
    tree = mat.node_tree
    tree.nodes.clear()
    
    bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
    bsdf.location = (-200, 0)
    bsdf.inputs["Base Color"].default_value = color
    
    # Blender 5.0の Principled BSDF 仕様対応
    if "Alpha" in bsdf.inputs:
        bsdf.inputs["Alpha"].default_value = color[3]
        
    out = tree.nodes.new("ShaderNodeOutputMaterial")
    out.location = (100, 0)
    
    tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
    mat.diffuse_color = color
    return mat

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

def build_cylinder(props, collection, is_preview=False):
    x1, y1 = props.pt1_xy
    x2, y2 = props.pt2_xy
    z1 = props.base_z
    
    # XY平面上の距離を計算
    dx = x2 - x1
    dy = y2 - y1
    dxy = math.sqrt(dx**2 + dy**2)
    
    # 同一座標等の場合は生成しない
    if dxy < 0.0001:
        return None  
        
    # Z軸に対して45度にするため、高さの差(dZ)をXY平面の距離(dXY)と同一にする
    z2 = z1 + dxy 
    
    p1 = Vector((x1, y1, z1))
    p2 = Vector((x2, y2, z2))
    
    length = (p2 - p1).length
    mid_point = (p1 + p2) / 2.0
    
    # 円柱の生成
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=props.radius, radius2=props.radius, depth=length)
    
    # 向きの設定 (初期Z軸方向を p1 -> p2 への方向ベクトルへ回転)
    direction = (p2 - p1).normalized()
    rot = Vector((0, 0, 1)).rotation_difference(direction)
    bmesh.ops.rotate(bm, cent=(0,0,0), matrix=rot.to_matrix(), verts=bm.verts)
    bmesh.ops.translate(bm, vec=mid_point, verts=bm.verts)
    
    # オブジェクト化
    name = f"Prev_Cylinder_{PREFIX}" if is_preview else f"AngledCyl_{datetime.now().strftime('%H%M%S')}"
    mesh = bpy.data.meshes.new(name)
    bm.to_mesh(mesh)
    bm.free()
    
    obj = bpy.data.objects.new(name, mesh)
    collection.objects.link(obj)
    
    # マテリアルの適用
    mat_name = f"Mat_Prev_{PREFIX}" if is_preview else f"Mat_{name}"
    mat = get_or_create_material(mat_name, props.cylinder_color)
    obj.data.materials.append(mat)
    
    return obj

# ==============================================================================
#  プレビュー用ロジック
# ==============================================================================

PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"

def clear_preview(context):
    col = bpy.data.collections.get(PREVIEW_COL_NAME)
    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_Cylinder_{PREFIX}") and m.users == 0:
            bpy.data.meshes.remove(m)

def update_preview(context):
    props = getattr(context.scene, PROPS_NAME, None)
    if not props: return
    clear_preview(context)
    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)
        
    obj = build_cylinder(props, col, is_preview=True)
    if obj:
        obj.display_type = 'TEXTURED'
    context.view_layer.update()

_timer = None
def delayed_update():
    global _timer
    _timer = None
    if bpy.context and bpy.context.scene: update_preview(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_CylinderProps(PropertyGroup):
    show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
    pt1_xy: FloatVectorProperty(name="Point 1 (Low) XY", size=2, default=CURRENT_DEFAULTS['pt1_xy'], update=on_update)
    pt2_xy: FloatVectorProperty(name="Point 2 (High) XY", size=2, default=CURRENT_DEFAULTS['pt2_xy'], update=on_update)
    base_z: FloatProperty(name="Base Z (Low Z Height)", default=CURRENT_DEFAULTS['base_z'], update=on_update)
    radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['radius'], min=0.01, update=on_update)
    cylinder_color: FloatVectorProperty(name="Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['cylinder_color'], update=on_update)

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

class OT_CreateCylinder(Operator):
    bl_idname = f"{OP_PREFIX}.create_cylinder"
    bl_label = "Create 45-Deg Cylinder"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        col = context.collection if context.collection else context.scene.collection
        
        obj = build_cylinder(props, col, is_preview=False)
        
        if obj:
            bpy.ops.object.select_all(action='DESELECT')
            obj.select_set(True)
            context.view_layer.objects.active = obj
            self.report({'INFO'}, "45-Degree Cylinder Created!")
        else:
            self.report({'WARNING'}, "Points are too close. Generation canceled.")
            
        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)
        self.report({'INFO'}, "Addon Removed successfully.")
        return {'FINISHED'}

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

class PT_MainPanel(Panel):
    bl_label = "45-Deg Cylinder Gen"
    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="Please Reload Script")
            return
            
        layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
        
        box = layout.box()
        box.label(text="XY Plane Points", icon='MESH_PLANE')
        box.prop(props, "pt1_xy")
        box.prop(props, "pt2_xy")
        
        box2 = layout.box()
        box2.label(text="Cylinder Base & Shape", icon='MESH_CYLINDER')
        box2.prop(props, "base_z")
        box2.prop(props, "radius")
        box2.prop(props, "cylinder_color")
        
        # 情報を表示 (プレビューを分かりやすくするため)
        layout.separator()
        x1, y1 = props.pt1_xy
        x2, y2 = props.pt2_xy
        dxy = math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
        dz = dxy # 45度角なのでXY距離とZ高さが同一
        length = math.sqrt(dxy**2 + dz**2)
        
        info_col = layout.column(align=True)
        info_col.label(text=f"Calculated Height (+Z): {dz:.3f}", icon='SORT_ASC')
        info_col.label(text=f"Total Cylinder Length: {length:.3f}", icon='LINCURVE')
        
        layout.separator()
        col = layout.column()
        col.scale_y = 1.5
        col.operator(OT_CreateCylinder.bl_idname, icon='MESH_CYLINDER', text="Create Object")

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_CylinderProps, OT_CreateCylinder, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, 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(bpy.context)
    return None

def register():
    for c in classes:
        bpy.utils.register_class(c)
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_CylinderProps))
    # スクリプト実行後、自動で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()