blender Million 2026

ローレンツ氏の 光速 予算配分 進化版 20260319






# Copied: 20260319 19:52:40
# Copied: 19:50:42
# Copied: 19:46:40
# Copied: 19:42:18
# Copied: 15:00:01
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, Matrix
from datetime import datetime

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

# ★ PREFIXに大文字が含まれていても、内部で自動的に小文字に変換して処理します
PREFIX = "YZRays20260318_V50"
TAB_NAME = "   [ YZ Rays ]   "

# ★ このスクリプト自身のID (コピー機能で使用)
# ### ZIONAD_SOURCE_ID: YZ_RAYS_V50_FIXED ###

bl_info = {
    "name": f"zionad 520 [ YZ Rays ] {PREFIX}",
    "author": "zionadchat",
    "version": (1, 50, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": "YZ Plane Rays with If-100% Extension",
    "category": "3D View",
}

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

ADDON_LINKS = (
    {"label": "Theory Background", "url": "<https://www.notion.so/>"},
    {"label": "Blender Guide", "url": "<https://www.notion.so/>"},
)

# ==============================================================================
#  デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_circle": True,
    "show_train_rays": True,
    "show_cone_rays": True,
    "show_if_train_rays": True,
    "ray_color": (0.6396, 0.0172, 0.5576, 1.0000),
    "cone_color": (0.8000, 0.5848, 0.0000, 0.6000),
    "circle_color": (0.0082, 0.0098, 0.8000, 0.1000),
    "if_train_color": (0.0112, 0.5279, 0.0418, 1.0000),
    "emission_x": 0.0000,
    "speed_v": 0.6900,
    "time_t": 10.0000,
    "circle_depth": 10.0000,
    "circle_solid": 2.0000,
    "ray_thickness": 0.3000,
    "cone_thickness": 0.1000,
    "if_train_thickness": 0.5000,
}
# <END_DICT>

# ==============================================================================
#  物理情報計算ロジック
# ==============================================================================
def get_physics_info(p):
    c = 1.0
    v = min(0.9999, max(0.0, p.speed_v))
    t = p.time_t
    
    # 実際の座標と半径
    x_pos = p.emission_x + (v * t)
    val = (c * t)**2 - (v * t)**2
    r_rays = math.sqrt(max(0, val))
    
    # 角度 (X軸に対する角度 / ZX平面での角度)
    angle_rad = math.acos(v / c)
    angle_deg = math.degrees(angle_rad)
    
    # If 100% (そのままの角度で、半径が光速100%相当 t になるまで延長した場合の交点)
    gamma = 1.0 / math.sqrt(1.0 - v**2) if v < 0.9999 else 1.0
    if_r_rays = c * t
    if_x_pos = p.emission_x + (v * t * gamma)
    
    return {
        "speed_pct": v * 100.0,
        "x_pos": x_pos,
        "r_rays": r_rays,
        "angle_deg": angle_deg,
        "if_x_pos": if_x_pos,
        "if_r_rays": if_r_rays
    }

# ==============================================================================
#  マテリアル作成ロジック
# ==============================================================================
def create_unique_material(color, name_prefix="Mat"):
    # 無限増殖を防ぐためにPREFIXを使用
    mat_name = f"{name_prefix}_{PREFIX}"
    
    mat = bpy.data.materials.get(mat_name)
    if not mat: mat = bpy.data.materials.new(name=mat_name)
        
    mat.use_nodes = True
    mat.blend_method = 'BLEND'
    if hasattr(mat, "shadow_method"): mat.shadow_method = 'NONE'
    
    if mat.use_nodes:
        tree = mat.node_tree
        tree.nodes.clear()
        
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        bsdf.location = (0, 0)
        
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        out.location = (300, 0)
        
        tree.links.new(bsdf.outputs[0], out.inputs[0])
        
        if "Base Color" in bsdf.inputs: bsdf.inputs['Base Color'].default_value = color
        if "Alpha" in bsdf.inputs: bsdf.inputs['Alpha'].default_value = color[3]
        if "Emission Strength" in bsdf.inputs: bsdf.inputs["Emission Strength"].default_value = 0.0
            
    mat.diffuse_color = color
    return mat

# ==============================================================================
#  プレビュー用(ジオメトリ)ロジック
# ==============================================================================
OUTPUT_COL_NAME = f"{PREFIX}_Output"
TAG_C, TAG_T, TAG_O, TAG_I = f"{PREFIX}_c", f"{PREFIX}_t", f"{PREFIX}_o", f"{PREFIX}_i"

def create_arrow_bm(bm, start, end, thick):
    vec = end - start
    length = vec.length
    if length < 0.001: return
    s_len, h_len = length * 0.9, length * 0.1
    s = bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=thick, radius2=thick, depth=s_len)
    bmesh.ops.translate(bm, verts=s['verts'], vec=Vector((0, 0, s_len/2)))
    h = bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=thick*2, radius2=0, depth=h_len)
    bmesh.ops.translate(bm, verts=h['verts'], vec=Vector((0, 0, s_len + h_len/2)))
    rot = Vector((0, 0, 1)).rotation_difference(vec.normalized())
    bmesh.ops.rotate(bm, verts=list(s['verts']) + list(h['verts']), cent=(0,0,0), matrix=rot.to_matrix().to_4x4())
    bmesh.ops.translate(bm, verts=list(s['verts']) + list(h['verts']), vec=start)

def update_preview_geometry(context):
    p = getattr(context.scene, PROPS_NAME, None)
    if not p: return

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

    def sync_obj(name, tag):
        obj = next((o for o in col.objects if o.get(tag)), None)
        if obj: 
            obj.data.clear_geometry()
            return obj, obj.data
        mesh = bpy.data.meshes.new(name)
        obj = bpy.data.objects.new(name, mesh)
        obj[tag] = True
        col.objects.link(obj)
        return obj, mesh

    info = get_physics_info(p)
    R_circle = 1.0 * p.time_t
    R_rays = info["r_rays"]
    
    cone_origin = Vector((p.emission_x, 0, 0))
    train_origin = Vector((info["x_pos"], 0, 0))

    # 1. Circle
    o_c, m_c = sync_obj(f"YZ_Circle_{PREFIX}", TAG_C)
    o_c.hide_viewport = not p.show_circle
    bm = bmesh.new()
    res_c = bmesh.ops.create_cone(bm, cap_ends=False, segments=96, radius1=R_circle, radius2=R_circle, depth=p.circle_depth)
    
    circle_faces = [f for f in res_c.get('geom', res_c.get('faces', [])) if isinstance(f, bmesh.types.BMFace)]
    if not circle_faces: circle_faces = bm.faces[:]
    
    if p.circle_solid > 0:
        bmesh.ops.solidify(bm, geom=circle_faces, thickness=p.circle_solid)
        
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    bmesh.ops.rotate(bm, verts=bm.verts, cent=(0,0,0), matrix=Matrix.Rotation(math.radians(90), 4, 'Y'))
    bmesh.ops.translate(bm, verts=bm.verts, vec=train_origin)
    
    bm.to_mesh(m_c); bm.free()
    o_c.data.materials.clear()
    o_c.data.materials.append(create_unique_material(p.circle_color, "Mat_Circle"))

    # 2. Train Rays
    o_t, m_t = sync_obj(f"YZ_Train_Rays_{PREFIX}", TAG_T)
    o_t.hide_viewport = not p.show_train_rays
    bm = bmesh.new()
    if R_rays > 0.001:
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, R_rays * math.cos(ang), R_rays * math.sin(ang)))
            create_arrow_bm(bm, train_origin, train_origin + tip_offset, p.ray_thickness)
    bm.to_mesh(m_t); bm.free()
    o_t.data.materials.clear()
    o_t.data.materials.append(create_unique_material(p.ray_color, "Mat_Train"))

    # 3. Cone Rays
    o_o, m_o = sync_obj(f"YZ_Cone_Rays_{PREFIX}", TAG_O)
    o_o.hide_viewport = not p.show_cone_rays
    bm = bmesh.new()
    if R_rays > 0.001:
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, R_rays * math.cos(ang), R_rays * math.sin(ang)))
            create_arrow_bm(bm, cone_origin, train_origin + tip_offset, p.cone_thickness)
    bm.to_mesh(m_o); bm.free()
    o_o.data.materials.clear()
    o_o.data.materials.append(create_unique_material(p.cone_color, "Mat_Cone"))

    # 4. If Train Rays (100% Extension)
    o_i, m_i = sync_obj(f"YZ_If_Train_{PREFIX}", TAG_I)
    o_i.hide_viewport = not p.show_if_train_rays
    bm = bmesh.new()
    if info["if_r_rays"] > 0.001:
        train_if_origin = Vector((info["if_x_pos"], 0, 0))
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, info["if_r_rays"] * math.cos(ang), info["if_r_rays"] * math.sin(ang)))
            create_arrow_bm(bm, train_if_origin, train_if_origin + tip_offset, p.if_train_thickness)
    bm.to_mesh(m_i); bm.free()
    o_i.data.materials.clear()
    o_i.data.materials.append(create_unique_material(p.if_train_color, "Mat_If_Train"))

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

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

# ==============================================================================
#  PROPERTIES
# ==============================================================================
class PG_YZProps(PropertyGroup):
    show_circle: BoolProperty(name="Show Circle", default=CURRENT_DEFAULTS['show_circle'], update=on_update)
    show_train_rays: BoolProperty(name="Show Train Rays", default=CURRENT_DEFAULTS['show_train_rays'], update=on_update)
    show_cone_rays: BoolProperty(name="Show Cone Rays", default=CURRENT_DEFAULTS['show_cone_rays'], update=on_update)
    show_if_train_rays: BoolProperty(name="Show If Train Rays", default=CURRENT_DEFAULTS['show_if_train_rays'], update=on_update)
    
    ray_color: FloatVectorProperty(name="Train Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['ray_color'], update=on_update)
    cone_color: FloatVectorProperty(name="Cone Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['cone_color'], update=on_update)
    circle_color: FloatVectorProperty(name="Circle Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['circle_color'], update=on_update)
    if_train_color: FloatVectorProperty(name="If Train Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['if_train_color'], update=on_update)
    
    emission_x: FloatProperty(name="Cone Emission X", default=CURRENT_DEFAULTS['emission_x'], update=on_update)
    speed_v: FloatProperty(name="Velocity (v/c)", default=CURRENT_DEFAULTS['speed_v'], min=0.0, max=0.99, update=on_update)
    time_t: FloatProperty(name="Time (t)", default=CURRENT_DEFAULTS['time_t'], min=0.1, update=on_update)
    
    circle_depth: FloatProperty(name="Axial Width", default=CURRENT_DEFAULTS['circle_depth'], min=0.0, max=10.0, update=on_update)
    circle_solid: FloatProperty(name="Face Solidify", default=CURRENT_DEFAULTS['circle_solid'], min=0.0, max=2.0, update=on_update)
    ray_thickness: FloatProperty(name="Train Ray Thick", default=CURRENT_DEFAULTS['ray_thickness'], min=0.01, max=10.0, update=on_update)
    cone_thickness: FloatProperty(name="Cone Ray Thick", default=CURRENT_DEFAULTS['cone_thickness'], min=0.01, max=10.0, update=on_update)
    if_train_thickness: FloatProperty(name="If Train Thick", default=CURRENT_DEFAULTS['if_train_thickness'], min=0.01, max=10.0, update=on_update)

# ==============================================================================
#  OPERATORS
# ==============================================================================
class OT_ExecuteDraw(Operator):
    bl_idname = f"{OP_PREFIX}.execute_draw"
    bl_label = "Force Execute Draw"
    def execute(self, context):
        update_preview_geometry(context)
        return {'FINISHED'}

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

        code = target_text.as_string()
        
        # 安全な辞書構築 (エラーが起きないよう展開)
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_circle": {props.show_circle},\n'
        new_dict += f'    "show_train_rays": {props.show_train_rays},\n'
        new_dict += f'    "show_cone_rays": {props.show_cone_rays},\n'
        new_dict += f'    "show_if_train_rays": {props.show_if_train_rays},\n'
        
        rc, cc = props.ray_color, props.cone_color
        crc, itc = props.circle_color, props.if_train_color
        
        new_dict += f'    "ray_color": ({rc[0]:.4f}, {rc[1]:.4f}, {rc[2]:.4f}, {rc[3]:.4f}),\n'
        new_dict += f'    "cone_color": ({cc[0]:.4f}, {cc[1]:.4f}, {cc[2]:.4f}, {cc[3]:.4f}),\n'
        new_dict += f'    "circle_color": ({crc[0]:.4f}, {crc[1]:.4f}, {crc[2]:.4f}, {crc[3]:.4f}),\n'
        new_dict += f'    "if_train_color": ({itc[0]:.4f}, {itc[1]:.4f}, {itc[2]:.4f}, {itc[3]:.4f}),\n'
        
        new_dict += f'    "emission_x": {props.emission_x:.4f},\n'
        new_dict += f'    "speed_v": {props.speed_v:.4f},\n'
        new_dict += f'    "time_t": {props.time_t:.4f},\n'
        new_dict += f'    "circle_depth": {props.circle_depth:.4f},\n'
        new_dict += f'    "circle_solid": {props.circle_solid:.4f},\n'
        new_dict += f'    "ray_thickness": {props.ray_thickness:.4f},\n'
        new_dict += f'    "cone_thickness": {props.cone_thickness:.4f},\n'
        new_dict += f'    "if_train_thickness": {props.if_train_thickness:.4f},\n'
        new_dict += "}\n"

        # Sphereと同じ確実な文字列分割ロジック
        try:
            start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
            pre, post = code.split(start)[0], code.split(end)[1]
            final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
            context.window_manager.clipboard = final
            self.report({'INFO'}, "Code copied!")
        except Exception as e:
            self.report({'ERROR'}, f"Copy Failed: {str(e)}")
            return {'CANCELLED'}
            
        return {'FINISHED'}

class OT_CopyInfo(Operator):
    bl_idname = f"{OP_PREFIX}.copy_info"
    bl_label = "Copy Physics Info"
    
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME, None)
        if not p: return {'CANCELLED'}
        
        info = get_physics_info(p)
        text = (
            f"[ YZ Rays Physics Info ]\n"
            f"Speed (v/c)       : {info['speed_pct']:.1f} %  ({p.speed_v:.2f}c)\n"
            f"Time (t)          : {p.time_t:.4f}\n"
            f"Ray Angle to X    : {info['angle_deg']:.2f} deg\n"
            f"---------------------------------\n"
            f"Real Reach X Pos  : {info['x_pos']:.4f}\n"
            f"Real Wave Radius  : {info['r_rays']:.4f}\n"
            f"---------------------------------\n"
            f"If 100% Reach X   : {info['if_x_pos']:.4f}\n"
            f"If 100% Radius    : {info['if_r_rays']:.4f}\n"
        )
        context.window_manager.clipboard = text
        self.report({'INFO'}, "Physics Info Copied!")
        return {'FINISHED'}

class OT_Reset(Operator):
    bl_idname = f"{OP_PREFIX}.reset"
    bl_label = "Reset View"
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME)
        p.emission_x = 0.0; p.speed_v = 0.6; p.time_t = 100.0
        return {'FINISHED'}

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

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

# ==============================================================================
#  PANELS
# ==============================================================================
class PT_MainPanel(Panel):
    bl_label = "YZ Plane Rays (V50)"
    bl_idname = f"{PREFIX}_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

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

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

        # UI: Physics Properties
        phys = layout.box()
        phys.label(text="Physics Parameters", icon='PHYSICS')
        phys.prop(props, "emission_x")
        phys.prop(props, "speed_v")
        phys.prop(props, "time_t")
        phys.operator(OT_Reset.bl_idname, icon='LOOP_BACK', text="Reset Params")

        # UI: Realtime Info
        info_box = layout.box()
        info_box.label(text="Realtime Info", icon='INFO')
        info = get_physics_info(props)
        info_box.label(text=f"Speed: {info['speed_pct']:.1f} % ({props.speed_v:.2f}c)")
        info_box.label(text=f"Angle to X: {info['angle_deg']:.2f} °")
        info_box.label(text=f"[Real] X: {info['x_pos']:.2f} | R: {info['r_rays']:.2f}")
        info_box.label(text=f"[If 100%] X: {info['if_x_pos']:.2f} | R: {info['if_r_rays']:.2f}")
        info_box.operator(OT_CopyInfo.bl_idname, icon='COPYDOWN', text="Copy Info to Clipboard")
        layout.separator()

        # UI: Visuals
        col = layout.column()
        col.operator(OT_ExecuteDraw.bl_idname, icon='PLAY', text="Force Update Draw")

        box = layout.box()
        box.prop(props, "show_circle")
        if props.show_circle:
            box.prop(props, "circle_color", text="")
            box.prop(props, "circle_depth")
            box.prop(props, "circle_solid")

        box = layout.box()
        box.prop(props, "show_train_rays")
        if props.show_train_rays:
            box.prop(props, "ray_color", text="")
            box.prop(props, "ray_thickness")

        box = layout.box()
        box.prop(props, "show_cone_rays")
        if props.show_cone_rays:
            box.prop(props, "cone_color", text="")
            box.prop(props, "cone_thickness")

        box = layout.box()
        box.prop(props, "show_if_train_rays")
        if props.show_if_train_rays:
            box.prop(props, "if_train_color", text="")
            box.prop(props, "if_train_thickness")

class PT_LinksPanel(Panel):
    bl_label = "Theory 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"], icon='WORLD').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_YZProps, 
    OT_ExecuteDraw, 
    OT_CopyFullScript, 
    OT_CopyInfo,
    OT_Reset,
    OT_OpenUrl, 
    OT_RemoveAddon, 
    PT_MainPanel, 
    PT_LinksPanel, 
    PT_RemovePanel
)

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_YZProps))

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()
[ YZ Rays Physics Info ]
Speed (v/c)       : 69.0 %  (0.69c)
Time (t)          : 10.0000
Ray Angle to X    : 46.37 deg
---------------------------------
Real Reach X Pos  : 6.9000
Real Wave Radius  : 7.2381
---------------------------------
If 100% Reach X   : 9.5329
If 100% Radius    : 10.0000

# Copied: 19:50:42
# Copied: 19:46:40
# Copied: 19:42:18
# Copied: 15:00:01
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, Matrix
from datetime import datetime

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

# ★ PREFIXに大文字が含まれていても、内部で自動的に小文字に変換して処理します
PREFIX = "YZRays20260318_V50"
TAB_NAME = "   [ YZ Rays ]   "

# ★ このスクリプト自身のID (コピー機能で使用)
# ### ZIONAD_SOURCE_ID: YZ_RAYS_V50_FIXED ###

bl_info = {
    "name": f"zionad 520 [ YZ Rays ] {PREFIX}",
    "author": "zionadchat",
    "version": (1, 50, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": "YZ Plane Rays with If-100% Extension",
    "category": "3D View",
}

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

ADDON_LINKS = (
    {"label": "Theory Background", "url": "<https://www.notion.so/>"},
    {"label": "Blender Guide", "url": "<https://www.notion.so/>"},
)

# ==============================================================================
#  デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_circle": True,
    "show_train_rays": True,
    "show_cone_rays": True,
    "show_if_train_rays": True,
    "ray_color": (0.6396, 0.0172, 0.5576, 1.0000),
    "cone_color": (0.8000, 0.5848, 0.0000, 0.6000),
    "circle_color": (0.0082, 0.0098, 0.8000, 0.1000),
    "if_train_color": (0.0112, 0.5279, 0.0418, 1.0000),
    "emission_x": 0.0000,
    "speed_v": 0.6900,
    "time_t": 10.0000,
    "circle_depth": 10.0000,
    "circle_solid": 2.0000,
    "ray_thickness": 0.3000,
    "cone_thickness": 0.1000,
    "if_train_thickness": 0.5000,
}
# <END_DICT>

# ==============================================================================
#  物理情報計算ロジック
# ==============================================================================
def get_physics_info(p):
    c = 1.0
    v = min(0.9999, max(0.0, p.speed_v))
    t = p.time_t
    
    # 実際の座標と半径
    x_pos = p.emission_x + (v * t)
    val = (c * t)**2 - (v * t)**2
    r_rays = math.sqrt(max(0, val))
    
    # 角度 (X軸に対する角度 / ZX平面での角度)
    angle_rad = math.acos(v / c)
    angle_deg = math.degrees(angle_rad)
    
    # If 100% (そのままの角度で、半径が光速100%相当 t になるまで延長した場合の交点)
    gamma = 1.0 / math.sqrt(1.0 - v**2) if v < 0.9999 else 1.0
    if_r_rays = c * t
    if_x_pos = p.emission_x + (v * t * gamma)
    
    return {
        "speed_pct": v * 100.0,
        "x_pos": x_pos,
        "r_rays": r_rays,
        "angle_deg": angle_deg,
        "if_x_pos": if_x_pos,
        "if_r_rays": if_r_rays
    }

# ==============================================================================
#  マテリアル作成ロジック
# ==============================================================================
def create_unique_material(color, name_prefix="Mat"):
    # 無限増殖を防ぐためにPREFIXを使用
    mat_name = f"{name_prefix}_{PREFIX}"
    
    mat = bpy.data.materials.get(mat_name)
    if not mat: mat = bpy.data.materials.new(name=mat_name)
        
    mat.use_nodes = True
    mat.blend_method = 'BLEND'
    if hasattr(mat, "shadow_method"): mat.shadow_method = 'NONE'
    
    if mat.use_nodes:
        tree = mat.node_tree
        tree.nodes.clear()
        
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        bsdf.location = (0, 0)
        
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        out.location = (300, 0)
        
        tree.links.new(bsdf.outputs[0], out.inputs[0])
        
        if "Base Color" in bsdf.inputs: bsdf.inputs['Base Color'].default_value = color
        if "Alpha" in bsdf.inputs: bsdf.inputs['Alpha'].default_value = color[3]
        if "Emission Strength" in bsdf.inputs: bsdf.inputs["Emission Strength"].default_value = 0.0
            
    mat.diffuse_color = color
    return mat

# ==============================================================================
#  プレビュー用(ジオメトリ)ロジック
# ==============================================================================
OUTPUT_COL_NAME = f"{PREFIX}_Output"
TAG_C, TAG_T, TAG_O, TAG_I = f"{PREFIX}_c", f"{PREFIX}_t", f"{PREFIX}_o", f"{PREFIX}_i"

def create_arrow_bm(bm, start, end, thick):
    vec = end - start
    length = vec.length
    if length < 0.001: return
    s_len, h_len = length * 0.9, length * 0.1
    s = bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=thick, radius2=thick, depth=s_len)
    bmesh.ops.translate(bm, verts=s['verts'], vec=Vector((0, 0, s_len/2)))
    h = bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=thick*2, radius2=0, depth=h_len)
    bmesh.ops.translate(bm, verts=h['verts'], vec=Vector((0, 0, s_len + h_len/2)))
    rot = Vector((0, 0, 1)).rotation_difference(vec.normalized())
    bmesh.ops.rotate(bm, verts=list(s['verts']) + list(h['verts']), cent=(0,0,0), matrix=rot.to_matrix().to_4x4())
    bmesh.ops.translate(bm, verts=list(s['verts']) + list(h['verts']), vec=start)

def update_preview_geometry(context):
    p = getattr(context.scene, PROPS_NAME, None)
    if not p: return

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

    def sync_obj(name, tag):
        obj = next((o for o in col.objects if o.get(tag)), None)
        if obj: 
            obj.data.clear_geometry()
            return obj, obj.data
        mesh = bpy.data.meshes.new(name)
        obj = bpy.data.objects.new(name, mesh)
        obj[tag] = True
        col.objects.link(obj)
        return obj, mesh

    info = get_physics_info(p)
    R_circle = 1.0 * p.time_t
    R_rays = info["r_rays"]
    
    cone_origin = Vector((p.emission_x, 0, 0))
    train_origin = Vector((info["x_pos"], 0, 0))

    # 1. Circle
    o_c, m_c = sync_obj(f"YZ_Circle_{PREFIX}", TAG_C)
    o_c.hide_viewport = not p.show_circle
    bm = bmesh.new()
    res_c = bmesh.ops.create_cone(bm, cap_ends=False, segments=96, radius1=R_circle, radius2=R_circle, depth=p.circle_depth)
    
    circle_faces = [f for f in res_c.get('geom', res_c.get('faces', [])) if isinstance(f, bmesh.types.BMFace)]
    if not circle_faces: circle_faces = bm.faces[:]
    
    if p.circle_solid > 0:
        bmesh.ops.solidify(bm, geom=circle_faces, thickness=p.circle_solid)
        
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    bmesh.ops.rotate(bm, verts=bm.verts, cent=(0,0,0), matrix=Matrix.Rotation(math.radians(90), 4, 'Y'))
    bmesh.ops.translate(bm, verts=bm.verts, vec=train_origin)
    
    bm.to_mesh(m_c); bm.free()
    o_c.data.materials.clear()
    o_c.data.materials.append(create_unique_material(p.circle_color, "Mat_Circle"))

    # 2. Train Rays
    o_t, m_t = sync_obj(f"YZ_Train_Rays_{PREFIX}", TAG_T)
    o_t.hide_viewport = not p.show_train_rays
    bm = bmesh.new()
    if R_rays > 0.001:
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, R_rays * math.cos(ang), R_rays * math.sin(ang)))
            create_arrow_bm(bm, train_origin, train_origin + tip_offset, p.ray_thickness)
    bm.to_mesh(m_t); bm.free()
    o_t.data.materials.clear()
    o_t.data.materials.append(create_unique_material(p.ray_color, "Mat_Train"))

    # 3. Cone Rays
    o_o, m_o = sync_obj(f"YZ_Cone_Rays_{PREFIX}", TAG_O)
    o_o.hide_viewport = not p.show_cone_rays
    bm = bmesh.new()
    if R_rays > 0.001:
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, R_rays * math.cos(ang), R_rays * math.sin(ang)))
            create_arrow_bm(bm, cone_origin, train_origin + tip_offset, p.cone_thickness)
    bm.to_mesh(m_o); bm.free()
    o_o.data.materials.clear()
    o_o.data.materials.append(create_unique_material(p.cone_color, "Mat_Cone"))

    # 4. If Train Rays (100% Extension)
    o_i, m_i = sync_obj(f"YZ_If_Train_{PREFIX}", TAG_I)
    o_i.hide_viewport = not p.show_if_train_rays
    bm = bmesh.new()
    if info["if_r_rays"] > 0.001:
        train_if_origin = Vector((info["if_x_pos"], 0, 0))
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, info["if_r_rays"] * math.cos(ang), info["if_r_rays"] * math.sin(ang)))
            create_arrow_bm(bm, train_if_origin, train_if_origin + tip_offset, p.if_train_thickness)
    bm.to_mesh(m_i); bm.free()
    o_i.data.materials.clear()
    o_i.data.materials.append(create_unique_material(p.if_train_color, "Mat_If_Train"))

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

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

# ==============================================================================
#  PROPERTIES
# ==============================================================================
class PG_YZProps(PropertyGroup):
    show_circle: BoolProperty(name="Show Circle", default=CURRENT_DEFAULTS['show_circle'], update=on_update)
    show_train_rays: BoolProperty(name="Show Train Rays", default=CURRENT_DEFAULTS['show_train_rays'], update=on_update)
    show_cone_rays: BoolProperty(name="Show Cone Rays", default=CURRENT_DEFAULTS['show_cone_rays'], update=on_update)
    show_if_train_rays: BoolProperty(name="Show If Train Rays", default=CURRENT_DEFAULTS['show_if_train_rays'], update=on_update)
    
    ray_color: FloatVectorProperty(name="Train Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['ray_color'], update=on_update)
    cone_color: FloatVectorProperty(name="Cone Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['cone_color'], update=on_update)
    circle_color: FloatVectorProperty(name="Circle Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['circle_color'], update=on_update)
    if_train_color: FloatVectorProperty(name="If Train Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['if_train_color'], update=on_update)
    
    emission_x: FloatProperty(name="Cone Emission X", default=CURRENT_DEFAULTS['emission_x'], update=on_update)
    speed_v: FloatProperty(name="Velocity (v/c)", default=CURRENT_DEFAULTS['speed_v'], min=0.0, max=0.99, update=on_update)
    time_t: FloatProperty(name="Time (t)", default=CURRENT_DEFAULTS['time_t'], min=0.1, update=on_update)
    
    circle_depth: FloatProperty(name="Axial Width", default=CURRENT_DEFAULTS['circle_depth'], min=0.0, max=10.0, update=on_update)
    circle_solid: FloatProperty(name="Face Solidify", default=CURRENT_DEFAULTS['circle_solid'], min=0.0, max=2.0, update=on_update)
    ray_thickness: FloatProperty(name="Train Ray Thick", default=CURRENT_DEFAULTS['ray_thickness'], min=0.01, max=10.0, update=on_update)
    cone_thickness: FloatProperty(name="Cone Ray Thick", default=CURRENT_DEFAULTS['cone_thickness'], min=0.01, max=10.0, update=on_update)
    if_train_thickness: FloatProperty(name="If Train Thick", default=CURRENT_DEFAULTS['if_train_thickness'], min=0.01, max=10.0, update=on_update)

# ==============================================================================
#  OPERATORS
# ==============================================================================
class OT_ExecuteDraw(Operator):
    bl_idname = f"{OP_PREFIX}.execute_draw"
    bl_label = "Force Execute Draw"
    def execute(self, context):
        update_preview_geometry(context)
        return {'FINISHED'}

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

        code = target_text.as_string()
        
        # 安全な辞書構築 (エラーが起きないよう展開)
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_circle": {props.show_circle},\n'
        new_dict += f'    "show_train_rays": {props.show_train_rays},\n'
        new_dict += f'    "show_cone_rays": {props.show_cone_rays},\n'
        new_dict += f'    "show_if_train_rays": {props.show_if_train_rays},\n'
        
        rc, cc = props.ray_color, props.cone_color
        crc, itc = props.circle_color, props.if_train_color
        
        new_dict += f'    "ray_color": ({rc[0]:.4f}, {rc[1]:.4f}, {rc[2]:.4f}, {rc[3]:.4f}),\n'
        new_dict += f'    "cone_color": ({cc[0]:.4f}, {cc[1]:.4f}, {cc[2]:.4f}, {cc[3]:.4f}),\n'
        new_dict += f'    "circle_color": ({crc[0]:.4f}, {crc[1]:.4f}, {crc[2]:.4f}, {crc[3]:.4f}),\n'
        new_dict += f'    "if_train_color": ({itc[0]:.4f}, {itc[1]:.4f}, {itc[2]:.4f}, {itc[3]:.4f}),\n'
        
        new_dict += f'    "emission_x": {props.emission_x:.4f},\n'
        new_dict += f'    "speed_v": {props.speed_v:.4f},\n'
        new_dict += f'    "time_t": {props.time_t:.4f},\n'
        new_dict += f'    "circle_depth": {props.circle_depth:.4f},\n'
        new_dict += f'    "circle_solid": {props.circle_solid:.4f},\n'
        new_dict += f'    "ray_thickness": {props.ray_thickness:.4f},\n'
        new_dict += f'    "cone_thickness": {props.cone_thickness:.4f},\n'
        new_dict += f'    "if_train_thickness": {props.if_train_thickness:.4f},\n'
        new_dict += "}\n"

        # Sphereと同じ確実な文字列分割ロジック
        try:
            start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
            pre, post = code.split(start)[0], code.split(end)[1]
            final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
            context.window_manager.clipboard = final
            self.report({'INFO'}, "Code copied!")
        except Exception as e:
            self.report({'ERROR'}, f"Copy Failed: {str(e)}")
            return {'CANCELLED'}
            
        return {'FINISHED'}

class OT_CopyInfo(Operator):
    bl_idname = f"{OP_PREFIX}.copy_info"
    bl_label = "Copy Physics Info"
    
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME, None)
        if not p: return {'CANCELLED'}
        
        info = get_physics_info(p)
        text = (
            f"[ YZ Rays Physics Info ]\n"
            f"Speed (v/c)       : {info['speed_pct']:.1f} %  ({p.speed_v:.2f}c)\n"
            f"Time (t)          : {p.time_t:.4f}\n"
            f"Ray Angle to X    : {info['angle_deg']:.2f} deg\n"
            f"---------------------------------\n"
            f"Real Reach X Pos  : {info['x_pos']:.4f}\n"
            f"Real Wave Radius  : {info['r_rays']:.4f}\n"
            f"---------------------------------\n"
            f"If 100% Reach X   : {info['if_x_pos']:.4f}\n"
            f"If 100% Radius    : {info['if_r_rays']:.4f}\n"
        )
        context.window_manager.clipboard = text
        self.report({'INFO'}, "Physics Info Copied!")
        return {'FINISHED'}

class OT_Reset(Operator):
    bl_idname = f"{OP_PREFIX}.reset"
    bl_label = "Reset View"
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME)
        p.emission_x = 0.0; p.speed_v = 0.6; p.time_t = 100.0
        return {'FINISHED'}

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

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

# ==============================================================================
#  PANELS
# ==============================================================================
class PT_MainPanel(Panel):
    bl_label = "YZ Plane Rays (V50)"
    bl_idname = f"{PREFIX}_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

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

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

        # UI: Physics Properties
        phys = layout.box()
        phys.label(text="Physics Parameters", icon='PHYSICS')
        phys.prop(props, "emission_x")
        phys.prop(props, "speed_v")
        phys.prop(props, "time_t")
        phys.operator(OT_Reset.bl_idname, icon='LOOP_BACK', text="Reset Params")

        # UI: Realtime Info
        info_box = layout.box()
        info_box.label(text="Realtime Info", icon='INFO')
        info = get_physics_info(props)
        info_box.label(text=f"Speed: {info['speed_pct']:.1f} % ({props.speed_v:.2f}c)")
        info_box.label(text=f"Angle to X: {info['angle_deg']:.2f} °")
        info_box.label(text=f"[Real] X: {info['x_pos']:.2f} | R: {info['r_rays']:.2f}")
        info_box.label(text=f"[If 100%] X: {info['if_x_pos']:.2f} | R: {info['if_r_rays']:.2f}")
        info_box.operator(OT_CopyInfo.bl_idname, icon='COPYDOWN', text="Copy Info to Clipboard")
        layout.separator()

        # UI: Visuals
        col = layout.column()
        col.operator(OT_ExecuteDraw.bl_idname, icon='PLAY', text="Force Update Draw")

        box = layout.box()
        box.prop(props, "show_circle")
        if props.show_circle:
            box.prop(props, "circle_color", text="")
            box.prop(props, "circle_depth")
            box.prop(props, "circle_solid")

        box = layout.box()
        box.prop(props, "show_train_rays")
        if props.show_train_rays:
            box.prop(props, "ray_color", text="")
            box.prop(props, "ray_thickness")

        box = layout.box()
        box.prop(props, "show_cone_rays")
        if props.show_cone_rays:
            box.prop(props, "cone_color", text="")
            box.prop(props, "cone_thickness")

        box = layout.box()
        box.prop(props, "show_if_train_rays")
        if props.show_if_train_rays:
            box.prop(props, "if_train_color", text="")
            box.prop(props, "if_train_thickness")

class PT_LinksPanel(Panel):
    bl_label = "Theory 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"], icon='WORLD').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_YZProps, 
    OT_ExecuteDraw, 
    OT_CopyFullScript, 
    OT_CopyInfo,
    OT_Reset,
    OT_OpenUrl, 
    OT_RemoveAddon, 
    PT_MainPanel, 
    PT_LinksPanel, 
    PT_RemovePanel
)

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_YZProps))

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: 19:46:40
# Copied: 19:42:18
# Copied: 15:00:01
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, Matrix
from datetime import datetime

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

# ★ PREFIXに大文字が含まれていても、内部で自動的に小文字に変換して処理します
PREFIX = "YZRays20260318_V50"
TAB_NAME = "   [ YZ Rays ]   "

# ★ このスクリプト自身のID (コピー機能で使用)
# ### ZIONAD_SOURCE_ID: YZ_RAYS_V50_FIXED ###

bl_info = {
    "name": f"zionad 520 [ YZ Rays ] {PREFIX}",
    "author": "zionadchat",
    "version": (1, 50, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": "YZ Plane Rays with If-100% Extension",
    "category": "3D View",
}

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

ADDON_LINKS = (
    {"label": "Theory Background", "url": "<https://www.notion.so/>"},
    {"label": "Blender Guide", "url": "<https://www.notion.so/>"},
)

# ==============================================================================
#  デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_circle": True,
    "show_train_rays": True,
    "show_cone_rays": True,
    "show_if_train_rays": True,
    "ray_color": (0.8000, 0.8000, 0.1000, 1.0000),
    "cone_color": (0.8000, 0.2000, 0.1000, 0.6000),
    "circle_color": (0.0082, 0.0098, 0.8000, 0.1000),
    "if_train_color": (0.1000, 0.8000, 0.5000, 0.5000),
    "emission_x": 0.0000,
    "speed_v": 0.9600,
    "time_t": 10.0000,
    "circle_depth": 10.0000,
    "circle_solid": 2.0000,
    "ray_thickness": 0.2000,
    "cone_thickness": 0.2000,
    "if_train_thickness": 0.5000,
}
# <END_DICT>

# ==============================================================================
#  物理情報計算ロジック
# ==============================================================================
def get_physics_info(p):
    c = 1.0
    v = min(0.9999, max(0.0, p.speed_v))
    t = p.time_t
    
    # 実際の座標と半径
    x_pos = p.emission_x + (v * t)
    val = (c * t)**2 - (v * t)**2
    r_rays = math.sqrt(max(0, val))
    
    # 角度 (X軸に対する角度 / ZX平面での角度)
    angle_rad = math.acos(v / c)
    angle_deg = math.degrees(angle_rad)
    
    # If 100% (そのままの角度で、半径が光速100%相当 t になるまで延長した場合の交点)
    gamma = 1.0 / math.sqrt(1.0 - v**2) if v < 0.9999 else 1.0
    if_r_rays = c * t
    if_x_pos = p.emission_x + (v * t * gamma)
    
    return {
        "speed_pct": v * 100.0,
        "x_pos": x_pos,
        "r_rays": r_rays,
        "angle_deg": angle_deg,
        "if_x_pos": if_x_pos,
        "if_r_rays": if_r_rays
    }

# ==============================================================================
#  マテリアル作成ロジック
# ==============================================================================
def create_unique_material(color, name_prefix="Mat"):
    # 無限増殖を防ぐためにPREFIXを使用
    mat_name = f"{name_prefix}_{PREFIX}"
    
    mat = bpy.data.materials.get(mat_name)
    if not mat: mat = bpy.data.materials.new(name=mat_name)
        
    mat.use_nodes = True
    mat.blend_method = 'BLEND'
    if hasattr(mat, "shadow_method"): mat.shadow_method = 'NONE'
    
    if mat.use_nodes:
        tree = mat.node_tree
        tree.nodes.clear()
        
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        bsdf.location = (0, 0)
        
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        out.location = (300, 0)
        
        tree.links.new(bsdf.outputs[0], out.inputs[0])
        
        if "Base Color" in bsdf.inputs: bsdf.inputs['Base Color'].default_value = color
        if "Alpha" in bsdf.inputs: bsdf.inputs['Alpha'].default_value = color[3]
        if "Emission Strength" in bsdf.inputs: bsdf.inputs["Emission Strength"].default_value = 0.0
            
    mat.diffuse_color = color
    return mat

# ==============================================================================
#  プレビュー用(ジオメトリ)ロジック
# ==============================================================================
OUTPUT_COL_NAME = f"{PREFIX}_Output"
TAG_C, TAG_T, TAG_O, TAG_I = f"{PREFIX}_c", f"{PREFIX}_t", f"{PREFIX}_o", f"{PREFIX}_i"

def create_arrow_bm(bm, start, end, thick):
    vec = end - start
    length = vec.length
    if length < 0.001: return
    s_len, h_len = length * 0.9, length * 0.1
    s = bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=thick, radius2=thick, depth=s_len)
    bmesh.ops.translate(bm, verts=s['verts'], vec=Vector((0, 0, s_len/2)))
    h = bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=thick*2, radius2=0, depth=h_len)
    bmesh.ops.translate(bm, verts=h['verts'], vec=Vector((0, 0, s_len + h_len/2)))
    rot = Vector((0, 0, 1)).rotation_difference(vec.normalized())
    bmesh.ops.rotate(bm, verts=list(s['verts']) + list(h['verts']), cent=(0,0,0), matrix=rot.to_matrix().to_4x4())
    bmesh.ops.translate(bm, verts=list(s['verts']) + list(h['verts']), vec=start)

def update_preview_geometry(context):
    p = getattr(context.scene, PROPS_NAME, None)
    if not p: return

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

    def sync_obj(name, tag):
        obj = next((o for o in col.objects if o.get(tag)), None)
        if obj: 
            obj.data.clear_geometry()
            return obj, obj.data
        mesh = bpy.data.meshes.new(name)
        obj = bpy.data.objects.new(name, mesh)
        obj[tag] = True
        col.objects.link(obj)
        return obj, mesh

    info = get_physics_info(p)
    R_circle = 1.0 * p.time_t
    R_rays = info["r_rays"]
    
    cone_origin = Vector((p.emission_x, 0, 0))
    train_origin = Vector((info["x_pos"], 0, 0))

    # 1. Circle
    o_c, m_c = sync_obj(f"YZ_Circle_{PREFIX}", TAG_C)
    o_c.hide_viewport = not p.show_circle
    bm = bmesh.new()
    res_c = bmesh.ops.create_cone(bm, cap_ends=False, segments=96, radius1=R_circle, radius2=R_circle, depth=p.circle_depth)
    
    circle_faces = [f for f in res_c.get('geom', res_c.get('faces', [])) if isinstance(f, bmesh.types.BMFace)]
    if not circle_faces: circle_faces = bm.faces[:]
    
    if p.circle_solid > 0:
        bmesh.ops.solidify(bm, geom=circle_faces, thickness=p.circle_solid)
        
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    bmesh.ops.rotate(bm, verts=bm.verts, cent=(0,0,0), matrix=Matrix.Rotation(math.radians(90), 4, 'Y'))
    bmesh.ops.translate(bm, verts=bm.verts, vec=train_origin)
    
    bm.to_mesh(m_c); bm.free()
    o_c.data.materials.clear()
    o_c.data.materials.append(create_unique_material(p.circle_color, "Mat_Circle"))

    # 2. Train Rays
    o_t, m_t = sync_obj(f"YZ_Train_Rays_{PREFIX}", TAG_T)
    o_t.hide_viewport = not p.show_train_rays
    bm = bmesh.new()
    if R_rays > 0.001:
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, R_rays * math.cos(ang), R_rays * math.sin(ang)))
            create_arrow_bm(bm, train_origin, train_origin + tip_offset, p.ray_thickness)
    bm.to_mesh(m_t); bm.free()
    o_t.data.materials.clear()
    o_t.data.materials.append(create_unique_material(p.ray_color, "Mat_Train"))

    # 3. Cone Rays
    o_o, m_o = sync_obj(f"YZ_Cone_Rays_{PREFIX}", TAG_O)
    o_o.hide_viewport = not p.show_cone_rays
    bm = bmesh.new()
    if R_rays > 0.001:
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, R_rays * math.cos(ang), R_rays * math.sin(ang)))
            create_arrow_bm(bm, cone_origin, train_origin + tip_offset, p.cone_thickness)
    bm.to_mesh(m_o); bm.free()
    o_o.data.materials.clear()
    o_o.data.materials.append(create_unique_material(p.cone_color, "Mat_Cone"))

    # 4. If Train Rays (100% Extension)
    o_i, m_i = sync_obj(f"YZ_If_Train_{PREFIX}", TAG_I)
    o_i.hide_viewport = not p.show_if_train_rays
    bm = bmesh.new()
    if info["if_r_rays"] > 0.001:
        train_if_origin = Vector((info["if_x_pos"], 0, 0))
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, info["if_r_rays"] * math.cos(ang), info["if_r_rays"] * math.sin(ang)))
            create_arrow_bm(bm, train_if_origin, train_if_origin + tip_offset, p.if_train_thickness)
    bm.to_mesh(m_i); bm.free()
    o_i.data.materials.clear()
    o_i.data.materials.append(create_unique_material(p.if_train_color, "Mat_If_Train"))

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

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

# ==============================================================================
#  PROPERTIES
# ==============================================================================
class PG_YZProps(PropertyGroup):
    show_circle: BoolProperty(name="Show Circle", default=CURRENT_DEFAULTS['show_circle'], update=on_update)
    show_train_rays: BoolProperty(name="Show Train Rays", default=CURRENT_DEFAULTS['show_train_rays'], update=on_update)
    show_cone_rays: BoolProperty(name="Show Cone Rays", default=CURRENT_DEFAULTS['show_cone_rays'], update=on_update)
    show_if_train_rays: BoolProperty(name="Show If Train Rays", default=CURRENT_DEFAULTS['show_if_train_rays'], update=on_update)
    
    ray_color: FloatVectorProperty(name="Train Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['ray_color'], update=on_update)
    cone_color: FloatVectorProperty(name="Cone Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['cone_color'], update=on_update)
    circle_color: FloatVectorProperty(name="Circle Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['circle_color'], update=on_update)
    if_train_color: FloatVectorProperty(name="If Train Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['if_train_color'], update=on_update)
    
    emission_x: FloatProperty(name="Cone Emission X", default=CURRENT_DEFAULTS['emission_x'], update=on_update)
    speed_v: FloatProperty(name="Velocity (v/c)", default=CURRENT_DEFAULTS['speed_v'], min=0.0, max=0.99, update=on_update)
    time_t: FloatProperty(name="Time (t)", default=CURRENT_DEFAULTS['time_t'], min=0.1, update=on_update)
    
    circle_depth: FloatProperty(name="Axial Width", default=CURRENT_DEFAULTS['circle_depth'], min=0.0, max=10.0, update=on_update)
    circle_solid: FloatProperty(name="Face Solidify", default=CURRENT_DEFAULTS['circle_solid'], min=0.0, max=2.0, update=on_update)
    ray_thickness: FloatProperty(name="Train Ray Thick", default=CURRENT_DEFAULTS['ray_thickness'], min=0.01, max=10.0, update=on_update)
    cone_thickness: FloatProperty(name="Cone Ray Thick", default=CURRENT_DEFAULTS['cone_thickness'], min=0.01, max=10.0, update=on_update)
    if_train_thickness: FloatProperty(name="If Train Thick", default=CURRENT_DEFAULTS['if_train_thickness'], min=0.01, max=10.0, update=on_update)

# ==============================================================================
#  OPERATORS
# ==============================================================================
class OT_ExecuteDraw(Operator):
    bl_idname = f"{OP_PREFIX}.execute_draw"
    bl_label = "Force Execute Draw"
    def execute(self, context):
        update_preview_geometry(context)
        return {'FINISHED'}

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

        code = target_text.as_string()
        
        # 安全な辞書構築 (エラーが起きないよう展開)
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_circle": {props.show_circle},\n'
        new_dict += f'    "show_train_rays": {props.show_train_rays},\n'
        new_dict += f'    "show_cone_rays": {props.show_cone_rays},\n'
        new_dict += f'    "show_if_train_rays": {props.show_if_train_rays},\n'
        
        rc, cc = props.ray_color, props.cone_color
        crc, itc = props.circle_color, props.if_train_color
        
        new_dict += f'    "ray_color": ({rc[0]:.4f}, {rc[1]:.4f}, {rc[2]:.4f}, {rc[3]:.4f}),\n'
        new_dict += f'    "cone_color": ({cc[0]:.4f}, {cc[1]:.4f}, {cc[2]:.4f}, {cc[3]:.4f}),\n'
        new_dict += f'    "circle_color": ({crc[0]:.4f}, {crc[1]:.4f}, {crc[2]:.4f}, {crc[3]:.4f}),\n'
        new_dict += f'    "if_train_color": ({itc[0]:.4f}, {itc[1]:.4f}, {itc[2]:.4f}, {itc[3]:.4f}),\n'
        
        new_dict += f'    "emission_x": {props.emission_x:.4f},\n'
        new_dict += f'    "speed_v": {props.speed_v:.4f},\n'
        new_dict += f'    "time_t": {props.time_t:.4f},\n'
        new_dict += f'    "circle_depth": {props.circle_depth:.4f},\n'
        new_dict += f'    "circle_solid": {props.circle_solid:.4f},\n'
        new_dict += f'    "ray_thickness": {props.ray_thickness:.4f},\n'
        new_dict += f'    "cone_thickness": {props.cone_thickness:.4f},\n'
        new_dict += f'    "if_train_thickness": {props.if_train_thickness:.4f},\n'
        new_dict += "}\n"

        # Sphereと同じ確実な文字列分割ロジック
        try:
            start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
            pre, post = code.split(start)[0], code.split(end)[1]
            final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
            context.window_manager.clipboard = final
            self.report({'INFO'}, "Code copied!")
        except Exception as e:
            self.report({'ERROR'}, f"Copy Failed: {str(e)}")
            return {'CANCELLED'}
            
        return {'FINISHED'}

class OT_CopyInfo(Operator):
    bl_idname = f"{OP_PREFIX}.copy_info"
    bl_label = "Copy Physics Info"
    
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME, None)
        if not p: return {'CANCELLED'}
        
        info = get_physics_info(p)
        text = (
            f"[ YZ Rays Physics Info ]\n"
            f"Speed (v/c)       : {info['speed_pct']:.1f} %  ({p.speed_v:.2f}c)\n"
            f"Time (t)          : {p.time_t:.4f}\n"
            f"Ray Angle to X    : {info['angle_deg']:.2f} deg\n"
            f"---------------------------------\n"
            f"Real Reach X Pos  : {info['x_pos']:.4f}\n"
            f"Real Wave Radius  : {info['r_rays']:.4f}\n"
            f"---------------------------------\n"
            f"If 100% Reach X   : {info['if_x_pos']:.4f}\n"
            f"If 100% Radius    : {info['if_r_rays']:.4f}\n"
        )
        context.window_manager.clipboard = text
        self.report({'INFO'}, "Physics Info Copied!")
        return {'FINISHED'}

class OT_Reset(Operator):
    bl_idname = f"{OP_PREFIX}.reset"
    bl_label = "Reset View"
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME)
        p.emission_x = 0.0; p.speed_v = 0.6; p.time_t = 100.0
        return {'FINISHED'}

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

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

# ==============================================================================
#  PANELS
# ==============================================================================
class PT_MainPanel(Panel):
    bl_label = "YZ Plane Rays (V50)"
    bl_idname = f"{PREFIX}_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

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

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

        # UI: Physics Properties
        phys = layout.box()
        phys.label(text="Physics Parameters", icon='PHYSICS')
        phys.prop(props, "emission_x")
        phys.prop(props, "speed_v")
        phys.prop(props, "time_t")
        phys.operator(OT_Reset.bl_idname, icon='LOOP_BACK', text="Reset Params")

        # UI: Realtime Info
        info_box = layout.box()
        info_box.label(text="Realtime Info", icon='INFO')
        info = get_physics_info(props)
        info_box.label(text=f"Speed: {info['speed_pct']:.1f} % ({props.speed_v:.2f}c)")
        info_box.label(text=f"Angle to X: {info['angle_deg']:.2f} °")
        info_box.label(text=f"[Real] X: {info['x_pos']:.2f} | R: {info['r_rays']:.2f}")
        info_box.label(text=f"[If 100%] X: {info['if_x_pos']:.2f} | R: {info['if_r_rays']:.2f}")
        info_box.operator(OT_CopyInfo.bl_idname, icon='COPYDOWN', text="Copy Info to Clipboard")
        layout.separator()

        # UI: Visuals
        col = layout.column()
        col.operator(OT_ExecuteDraw.bl_idname, icon='PLAY', text="Force Update Draw")

        box = layout.box()
        box.prop(props, "show_circle")
        if props.show_circle:
            box.prop(props, "circle_color", text="")
            box.prop(props, "circle_depth")
            box.prop(props, "circle_solid")

        box = layout.box()
        box.prop(props, "show_train_rays")
        if props.show_train_rays:
            box.prop(props, "ray_color", text="")
            box.prop(props, "ray_thickness")

        box = layout.box()
        box.prop(props, "show_cone_rays")
        if props.show_cone_rays:
            box.prop(props, "cone_color", text="")
            box.prop(props, "cone_thickness")

        box = layout.box()
        box.prop(props, "show_if_train_rays")
        if props.show_if_train_rays:
            box.prop(props, "if_train_color", text="")
            box.prop(props, "if_train_thickness")

class PT_LinksPanel(Panel):
    bl_label = "Theory 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"], icon='WORLD').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_YZProps, 
    OT_ExecuteDraw, 
    OT_CopyFullScript, 
    OT_CopyInfo,
    OT_Reset,
    OT_OpenUrl, 
    OT_RemoveAddon, 
    PT_MainPanel, 
    PT_LinksPanel, 
    PT_RemovePanel
)

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_YZProps))

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: 19:42:18
# Copied: 15:00:01
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, Matrix
from datetime import datetime

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

# ★ PREFIXに大文字が含まれていても、内部で自動的に小文字に変換して処理します
PREFIX = "YZRays20260318_V50"
TAB_NAME = "   [ YZ Rays ]   "

# ★ このスクリプト自身のID (コピー機能で使用)
# ### ZIONAD_SOURCE_ID: YZ_RAYS_V50_FIXED ###

bl_info = {
    "name": f"zionad 520 [ YZ Rays ] {PREFIX}",
    "author": "zionadchat",
    "version": (1, 50, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": "YZ Plane Rays with If-100% Extension",
    "category": "3D View",
}

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

ADDON_LINKS = (
    {"label": "Theory Background", "url": "<https://www.notion.so/>"},
    {"label": "Blender Guide", "url": "<https://www.notion.so/>"},
)

# ==============================================================================
#  デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_circle": True,
    "show_train_rays": True,
    "show_cone_rays": True,
    "show_if_train_rays": True,
    "ray_color": (0.8000, 0.8000, 0.1000, 1.0000),
    "cone_color": (0.8000, 0.2000, 0.1000, 0.6000),
    "circle_color": (0.0082, 0.0098, 0.8000, 0.1000),
    "if_train_color": (0.1000, 0.8000, 0.5000, 0.5000),
    "emission_x": 0.0000,
    "speed_v": 0.6000,
    "time_t": 100.0000,
    "circle_depth": 10.0000,
    "circle_solid": 2.0000,
    "ray_thickness": 1.0000,
    "cone_thickness": 1.0000,
    "if_train_thickness": 2.0000,
}
# <END_DICT>

# ==============================================================================
#  物理情報計算ロジック
# ==============================================================================
def get_physics_info(p):
    c = 1.0
    v = min(0.9999, max(0.0, p.speed_v))
    t = p.time_t
    
    # 実際の座標と半径
    x_pos = p.emission_x + (v * t)
    val = (c * t)**2 - (v * t)**2
    r_rays = math.sqrt(max(0, val))
    
    # 角度 (X軸に対する角度 / ZX平面での角度)
    angle_rad = math.acos(v / c)
    angle_deg = math.degrees(angle_rad)
    
    # If 100% (そのままの角度で、半径が光速100%相当 t になるまで延長した場合の交点)
    gamma = 1.0 / math.sqrt(1.0 - v**2) if v < 0.9999 else 1.0
    if_r_rays = c * t
    if_x_pos = p.emission_x + (v * t * gamma)
    
    return {
        "speed_pct": v * 100.0,
        "x_pos": x_pos,
        "r_rays": r_rays,
        "angle_deg": angle_deg,
        "if_x_pos": if_x_pos,
        "if_r_rays": if_r_rays
    }

# ==============================================================================
#  マテリアル作成ロジック
# ==============================================================================
def create_unique_material(color, name_prefix="Mat"):
    # 無限増殖を防ぐためにPREFIXを使用
    mat_name = f"{name_prefix}_{PREFIX}"
    
    mat = bpy.data.materials.get(mat_name)
    if not mat: mat = bpy.data.materials.new(name=mat_name)
        
    mat.use_nodes = True
    mat.blend_method = 'BLEND'
    if hasattr(mat, "shadow_method"): mat.shadow_method = 'NONE'
    
    if mat.use_nodes:
        tree = mat.node_tree
        tree.nodes.clear()
        
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        bsdf.location = (0, 0)
        
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        out.location = (300, 0)
        
        tree.links.new(bsdf.outputs[0], out.inputs[0])
        
        if "Base Color" in bsdf.inputs: bsdf.inputs['Base Color'].default_value = color
        if "Alpha" in bsdf.inputs: bsdf.inputs['Alpha'].default_value = color[3]
        if "Emission Strength" in bsdf.inputs: bsdf.inputs["Emission Strength"].default_value = 0.0
            
    mat.diffuse_color = color
    return mat

# ==============================================================================
#  プレビュー用(ジオメトリ)ロジック
# ==============================================================================
OUTPUT_COL_NAME = f"{PREFIX}_Output"
TAG_C, TAG_T, TAG_O, TAG_I = f"{PREFIX}_c", f"{PREFIX}_t", f"{PREFIX}_o", f"{PREFIX}_i"

def create_arrow_bm(bm, start, end, thick):
    vec = end - start
    length = vec.length
    if length < 0.001: return
    s_len, h_len = length * 0.9, length * 0.1
    s = bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=thick, radius2=thick, depth=s_len)
    bmesh.ops.translate(bm, verts=s['verts'], vec=Vector((0, 0, s_len/2)))
    h = bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=thick*2, radius2=0, depth=h_len)
    bmesh.ops.translate(bm, verts=h['verts'], vec=Vector((0, 0, s_len + h_len/2)))
    rot = Vector((0, 0, 1)).rotation_difference(vec.normalized())
    bmesh.ops.rotate(bm, verts=list(s['verts']) + list(h['verts']), cent=(0,0,0), matrix=rot.to_matrix().to_4x4())
    bmesh.ops.translate(bm, verts=list(s['verts']) + list(h['verts']), vec=start)

def update_preview_geometry(context):
    p = getattr(context.scene, PROPS_NAME, None)
    if not p: return

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

    def sync_obj(name, tag):
        obj = next((o for o in col.objects if o.get(tag)), None)
        if obj: 
            obj.data.clear_geometry()
            return obj, obj.data
        mesh = bpy.data.meshes.new(name)
        obj = bpy.data.objects.new(name, mesh)
        obj[tag] = True
        col.objects.link(obj)
        return obj, mesh

    info = get_physics_info(p)
    R_circle = 1.0 * p.time_t
    R_rays = info["r_rays"]
    
    cone_origin = Vector((p.emission_x, 0, 0))
    train_origin = Vector((info["x_pos"], 0, 0))

    # 1. Circle
    o_c, m_c = sync_obj(f"YZ_Circle_{PREFIX}", TAG_C)
    o_c.hide_viewport = not p.show_circle
    bm = bmesh.new()
    res_c = bmesh.ops.create_cone(bm, cap_ends=False, segments=96, radius1=R_circle, radius2=R_circle, depth=p.circle_depth)
    
    circle_faces = [f for f in res_c.get('geom', res_c.get('faces', [])) if isinstance(f, bmesh.types.BMFace)]
    if not circle_faces: circle_faces = bm.faces[:]
    
    if p.circle_solid > 0:
        bmesh.ops.solidify(bm, geom=circle_faces, thickness=p.circle_solid)
        
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    bmesh.ops.rotate(bm, verts=bm.verts, cent=(0,0,0), matrix=Matrix.Rotation(math.radians(90), 4, 'Y'))
    bmesh.ops.translate(bm, verts=bm.verts, vec=train_origin)
    
    bm.to_mesh(m_c); bm.free()
    o_c.data.materials.clear()
    o_c.data.materials.append(create_unique_material(p.circle_color, "Mat_Circle"))

    # 2. Train Rays
    o_t, m_t = sync_obj(f"YZ_Train_Rays_{PREFIX}", TAG_T)
    o_t.hide_viewport = not p.show_train_rays
    bm = bmesh.new()
    if R_rays > 0.001:
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, R_rays * math.cos(ang), R_rays * math.sin(ang)))
            create_arrow_bm(bm, train_origin, train_origin + tip_offset, p.ray_thickness)
    bm.to_mesh(m_t); bm.free()
    o_t.data.materials.clear()
    o_t.data.materials.append(create_unique_material(p.ray_color, "Mat_Train"))

    # 3. Cone Rays
    o_o, m_o = sync_obj(f"YZ_Cone_Rays_{PREFIX}", TAG_O)
    o_o.hide_viewport = not p.show_cone_rays
    bm = bmesh.new()
    if R_rays > 0.001:
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, R_rays * math.cos(ang), R_rays * math.sin(ang)))
            create_arrow_bm(bm, cone_origin, train_origin + tip_offset, p.cone_thickness)
    bm.to_mesh(m_o); bm.free()
    o_o.data.materials.clear()
    o_o.data.materials.append(create_unique_material(p.cone_color, "Mat_Cone"))

    # 4. If Train Rays (100% Extension)
    o_i, m_i = sync_obj(f"YZ_If_Train_{PREFIX}", TAG_I)
    o_i.hide_viewport = not p.show_if_train_rays
    bm = bmesh.new()
    if info["if_r_rays"] > 0.001:
        train_if_origin = Vector((info["if_x_pos"], 0, 0))
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, info["if_r_rays"] * math.cos(ang), info["if_r_rays"] * math.sin(ang)))
            create_arrow_bm(bm, train_if_origin, train_if_origin + tip_offset, p.if_train_thickness)
    bm.to_mesh(m_i); bm.free()
    o_i.data.materials.clear()
    o_i.data.materials.append(create_unique_material(p.if_train_color, "Mat_If_Train"))

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

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

# ==============================================================================
#  PROPERTIES
# ==============================================================================
class PG_YZProps(PropertyGroup):
    show_circle: BoolProperty(name="Show Circle", default=CURRENT_DEFAULTS['show_circle'], update=on_update)
    show_train_rays: BoolProperty(name="Show Train Rays", default=CURRENT_DEFAULTS['show_train_rays'], update=on_update)
    show_cone_rays: BoolProperty(name="Show Cone Rays", default=CURRENT_DEFAULTS['show_cone_rays'], update=on_update)
    show_if_train_rays: BoolProperty(name="Show If Train Rays", default=CURRENT_DEFAULTS['show_if_train_rays'], update=on_update)
    
    ray_color: FloatVectorProperty(name="Train Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['ray_color'], update=on_update)
    cone_color: FloatVectorProperty(name="Cone Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['cone_color'], update=on_update)
    circle_color: FloatVectorProperty(name="Circle Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['circle_color'], update=on_update)
    if_train_color: FloatVectorProperty(name="If Train Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['if_train_color'], update=on_update)
    
    emission_x: FloatProperty(name="Cone Emission X", default=CURRENT_DEFAULTS['emission_x'], update=on_update)
    speed_v: FloatProperty(name="Velocity (v/c)", default=CURRENT_DEFAULTS['speed_v'], min=0.0, max=0.99, update=on_update)
    time_t: FloatProperty(name="Time (t)", default=CURRENT_DEFAULTS['time_t'], min=0.1, update=on_update)
    
    circle_depth: FloatProperty(name="Axial Width", default=CURRENT_DEFAULTS['circle_depth'], min=0.0, max=10.0, update=on_update)
    circle_solid: FloatProperty(name="Face Solidify", default=CURRENT_DEFAULTS['circle_solid'], min=0.0, max=2.0, update=on_update)
    ray_thickness: FloatProperty(name="Train Ray Thick", default=CURRENT_DEFAULTS['ray_thickness'], min=0.01, max=10.0, update=on_update)
    cone_thickness: FloatProperty(name="Cone Ray Thick", default=CURRENT_DEFAULTS['cone_thickness'], min=0.01, max=10.0, update=on_update)
    if_train_thickness: FloatProperty(name="If Train Thick", default=CURRENT_DEFAULTS['if_train_thickness'], min=0.01, max=10.0, update=on_update)

# ==============================================================================
#  OPERATORS
# ==============================================================================
class OT_ExecuteDraw(Operator):
    bl_idname = f"{OP_PREFIX}.execute_draw"
    bl_label = "Force Execute Draw"
    def execute(self, context):
        update_preview_geometry(context)
        return {'FINISHED'}

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

        code = target_text.as_string()
        
        # 安全な辞書構築 (エラーが起きないよう展開)
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_circle": {props.show_circle},\n'
        new_dict += f'    "show_train_rays": {props.show_train_rays},\n'
        new_dict += f'    "show_cone_rays": {props.show_cone_rays},\n'
        new_dict += f'    "show_if_train_rays": {props.show_if_train_rays},\n'
        
        rc, cc = props.ray_color, props.cone_color
        crc, itc = props.circle_color, props.if_train_color
        
        new_dict += f'    "ray_color": ({rc[0]:.4f}, {rc[1]:.4f}, {rc[2]:.4f}, {rc[3]:.4f}),\n'
        new_dict += f'    "cone_color": ({cc[0]:.4f}, {cc[1]:.4f}, {cc[2]:.4f}, {cc[3]:.4f}),\n'
        new_dict += f'    "circle_color": ({crc[0]:.4f}, {crc[1]:.4f}, {crc[2]:.4f}, {crc[3]:.4f}),\n'
        new_dict += f'    "if_train_color": ({itc[0]:.4f}, {itc[1]:.4f}, {itc[2]:.4f}, {itc[3]:.4f}),\n'
        
        new_dict += f'    "emission_x": {props.emission_x:.4f},\n'
        new_dict += f'    "speed_v": {props.speed_v:.4f},\n'
        new_dict += f'    "time_t": {props.time_t:.4f},\n'
        new_dict += f'    "circle_depth": {props.circle_depth:.4f},\n'
        new_dict += f'    "circle_solid": {props.circle_solid:.4f},\n'
        new_dict += f'    "ray_thickness": {props.ray_thickness:.4f},\n'
        new_dict += f'    "cone_thickness": {props.cone_thickness:.4f},\n'
        new_dict += f'    "if_train_thickness": {props.if_train_thickness:.4f},\n'
        new_dict += "}\n"

        # Sphereと同じ確実な文字列分割ロジック
        try:
            start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
            pre, post = code.split(start)[0], code.split(end)[1]
            final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
            context.window_manager.clipboard = final
            self.report({'INFO'}, "Code copied!")
        except Exception as e:
            self.report({'ERROR'}, f"Copy Failed: {str(e)}")
            return {'CANCELLED'}
            
        return {'FINISHED'}

class OT_CopyInfo(Operator):
    bl_idname = f"{OP_PREFIX}.copy_info"
    bl_label = "Copy Physics Info"
    
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME, None)
        if not p: return {'CANCELLED'}
        
        info = get_physics_info(p)
        text = (
            f"[ YZ Rays Physics Info ]\n"
            f"Speed (v/c)       : {info['speed_pct']:.1f} %  ({p.speed_v:.2f}c)\n"
            f"Time (t)          : {p.time_t:.4f}\n"
            f"Ray Angle to X    : {info['angle_deg']:.2f} deg\n"
            f"---------------------------------\n"
            f"Real Reach X Pos  : {info['x_pos']:.4f}\n"
            f"Real Wave Radius  : {info['r_rays']:.4f}\n"
            f"---------------------------------\n"
            f"If 100% Reach X   : {info['if_x_pos']:.4f}\n"
            f"If 100% Radius    : {info['if_r_rays']:.4f}\n"
        )
        context.window_manager.clipboard = text
        self.report({'INFO'}, "Physics Info Copied!")
        return {'FINISHED'}

class OT_Reset(Operator):
    bl_idname = f"{OP_PREFIX}.reset"
    bl_label = "Reset View"
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME)
        p.emission_x = 0.0; p.speed_v = 0.6; p.time_t = 100.0
        return {'FINISHED'}

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

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

# ==============================================================================
#  PANELS
# ==============================================================================
class PT_MainPanel(Panel):
    bl_label = "YZ Plane Rays (V50)"
    bl_idname = f"{PREFIX}_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

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

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

        # UI: Physics Properties
        phys = layout.box()
        phys.label(text="Physics Parameters", icon='PHYSICS')
        phys.prop(props, "emission_x")
        phys.prop(props, "speed_v")
        phys.prop(props, "time_t")
        phys.operator(OT_Reset.bl_idname, icon='LOOP_BACK', text="Reset Params")

        # UI: Realtime Info
        info_box = layout.box()
        info_box.label(text="Realtime Info", icon='INFO')
        info = get_physics_info(props)
        info_box.label(text=f"Speed: {info['speed_pct']:.1f} % ({props.speed_v:.2f}c)")
        info_box.label(text=f"Angle to X: {info['angle_deg']:.2f} °")
        info_box.label(text=f"[Real] X: {info['x_pos']:.2f} | R: {info['r_rays']:.2f}")
        info_box.label(text=f"[If 100%] X: {info['if_x_pos']:.2f} | R: {info['if_r_rays']:.2f}")
        info_box.operator(OT_CopyInfo.bl_idname, icon='COPYDOWN', text="Copy Info to Clipboard")
        layout.separator()

        # UI: Visuals
        col = layout.column()
        col.operator(OT_ExecuteDraw.bl_idname, icon='PLAY', text="Force Update Draw")

        box = layout.box()
        box.prop(props, "show_circle")
        if props.show_circle:
            box.prop(props, "circle_color", text="")
            box.prop(props, "circle_depth")
            box.prop(props, "circle_solid")

        box = layout.box()
        box.prop(props, "show_train_rays")
        if props.show_train_rays:
            box.prop(props, "ray_color", text="")
            box.prop(props, "ray_thickness")

        box = layout.box()
        box.prop(props, "show_cone_rays")
        if props.show_cone_rays:
            box.prop(props, "cone_color", text="")
            box.prop(props, "cone_thickness")

        box = layout.box()
        box.prop(props, "show_if_train_rays")
        if props.show_if_train_rays:
            box.prop(props, "if_train_color", text="")
            box.prop(props, "if_train_thickness")

class PT_LinksPanel(Panel):
    bl_label = "Theory 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"], icon='WORLD').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_YZProps, 
    OT_ExecuteDraw, 
    OT_CopyFullScript, 
    OT_CopyInfo,
    OT_Reset,
    OT_OpenUrl, 
    OT_RemoveAddon, 
    PT_MainPanel, 
    PT_LinksPanel, 
    PT_RemovePanel
)

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_YZProps))

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: 15:00:01
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, Matrix
from datetime import datetime

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

PREFIX = "YZRays20260318_If"
TAB_NAME = "   [ YZ Rays ]   "

# ### ZIONAD_SOURCE_ID: YZ_RAYS_2026_03_18_IF_FIXED ###

bl_info = {
    "name": f"zionad 520 [ YZ Rays ] {PREFIX}",
    "author": "zionadchat",
    "version": (1, 40, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": "YZ Plane 12 Rays with If-100% Extension",
    "category": "3D View",
}

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

ADDON_LINKS = (
    {"label": "Theory Background", "url": "<https://www.notion.so/>"},
    {"label": "Blender Guide", "url": "<https://www.notion.so/>"},
)

# ==============================================================================
#  デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_circle": True,
    "show_train_rays": True,
    "show_cone_rays": True,
    "show_if_train_rays": True,
    "ray_color": (0.8000, 0.8000, 0.1000, 1.0000),
    "cone_color": (0.8000, 0.2000, 0.1000, 0.6000),
    "circle_color": (0.1000, 0.5000, 0.8000, 0.8000),
    "if_train_color": (0.1000, 0.8000, 0.5000, 0.5000),
    "emission_x": 0.0000,
    "speed_v": 0.6000,
    "time_t": 100.0000,
    "circle_depth": 0.1000,
    "circle_solid": 0.0500,
    "ray_thickness": 1.0000,
    "cone_thickness": 1.0000,
    "if_train_thickness": 1.0000,
}
# <END_DICT>

# ==============================================================================
#  物理情報計算ロジック
# ==============================================================================
def get_physics_info(p):
    c = 1.0
    v = min(0.9999, max(0.0, p.speed_v))
    t = p.time_t
    
    # 実際の座標と半径
    x_pos = p.emission_x + (v * t)
    val = (c * t)**2 - (v * t)**2
    r_rays = math.sqrt(max(0, val))
    
    # 角度 (X軸に対する角度 / ZX平面での角度)
    angle_rad = math.acos(v / c)
    angle_deg = math.degrees(angle_rad)
    
    # If 100% (そのままの角度で、半径が光速100%相当 t になるまで延長した場合の交点)
    # これは相対論におけるローレンツ因子(γ)による延長と一致します
    gamma = 1.0 / math.sqrt(1.0 - v**2) if v < 0.9999 else 1.0
    if_r_rays = c * t
    if_x_pos = p.emission_x + (v * t * gamma)
    
    return {
        "speed_pct": v * 100.0,
        "x_pos": x_pos,
        "r_rays": r_rays,
        "angle_deg": angle_deg,
        "if_x_pos": if_x_pos,
        "if_r_rays": if_r_rays
    }

# ==============================================================================
#  マテリアル作成ロジック
# ==============================================================================
def create_material(color, name_prefix="Mat"):
    mat_name = f"{name_prefix}_{PREFIX}"
    
    mat = bpy.data.materials.get(mat_name)
    if not mat:
        mat = bpy.data.materials.new(name=mat_name)
        
    mat.use_nodes = True
    mat.blend_method = 'BLEND'
    
    if mat.use_nodes:
        tree = mat.node_tree
        tree.nodes.clear()
        
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        bsdf.location = (0, 0)
        
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        out.location = (300, 0)
        
        tree.links.new(bsdf.outputs[0], out.inputs[0])
        
        if "Base Color" in bsdf.inputs:
            bsdf.inputs['Base Color'].default_value = color
        if "Alpha" in bsdf.inputs:
            bsdf.inputs['Alpha'].default_value = color[3]
            
    mat.diffuse_color = color
    return mat

# ==============================================================================
#  ジオメトリエンジン & 描画ロジック
# ==============================================================================
def create_arrow_bm(bm, start, end, thick):
    vec = end - start
    length = vec.length
    if length < 0.001: return
    s_len, h_len = length * 0.9, length * 0.1
    s = bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=thick, radius2=thick, depth=s_len)
    bmesh.ops.translate(bm, verts=s['verts'], vec=Vector((0, 0, s_len/2)))
    h = bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=thick*2, radius2=0, depth=h_len)
    bmesh.ops.translate(bm, verts=h['verts'], vec=Vector((0, 0, s_len + h_len/2)))
    rot = Vector((0, 0, 1)).rotation_difference(vec.normalized())
    bmesh.ops.rotate(bm, verts=list(s['verts']) + list(h['verts']), cent=(0,0,0), matrix=rot.to_matrix().to_4x4())
    bmesh.ops.translate(bm, verts=list(s['verts']) + list(h['verts']), vec=start)

OUTPUT_COL_NAME = f"{PREFIX}_Output"
TAG_C, TAG_T, TAG_O, TAG_I = f"{PREFIX}_c", f"{PREFIX}_t", f"{PREFIX}_o", f"{PREFIX}_i"

def update_geometry(context):
    p = getattr(context.scene, PROPS_NAME, None)
    if not p: return

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

    def sync_obj(name, tag):
        obj = next((o for o in col.objects if o.get(tag)), None)
        if obj: 
            obj.data.clear_geometry()
            return obj, obj.data
        mesh = bpy.data.meshes.new(name)
        obj = bpy.data.objects.new(name, mesh)
        obj[tag] = True
        col.objects.link(obj)
        return obj, mesh

    info = get_physics_info(p)
    R_circle = 1.0 * p.time_t
    R_rays = info["r_rays"]
    
    cone_origin = Vector((p.emission_x, 0, 0))
    train_origin = Vector((info["x_pos"], 0, 0))

    # 1. Circle
    o_c, m_c = sync_obj(f"YZ_Circle_{PREFIX}", TAG_C)
    o_c.hide_viewport = not p.show_circle
    bm = bmesh.new()
    res_c = bmesh.ops.create_cone(bm, cap_ends=False, segments=96, radius1=R_circle, radius2=R_circle, depth=p.circle_depth)
    
    circle_faces = [f for f in res_c.get('geom', res_c.get('faces', [])) if isinstance(f, bmesh.types.BMFace)]
    if not circle_faces: circle_faces = bm.faces[:]
    
    if p.circle_solid > 0:
        bmesh.ops.solidify(bm, geom=circle_faces, thickness=p.circle_solid)
        
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    bmesh.ops.rotate(bm, verts=bm.verts, cent=(0,0,0), matrix=Matrix.Rotation(math.radians(90), 4, 'Y'))
    bmesh.ops.translate(bm, verts=bm.verts, vec=train_origin)
    
    bm.to_mesh(m_c)
    bm.free()
    o_c.data.materials.clear()
    o_c.data.materials.append(create_material(p.circle_color, "Mat_Circle"))

    # 2. Train Rays (Real)
    o_t, m_t = sync_obj(f"YZ_Train_Rays_{PREFIX}", TAG_T)
    o_t.hide_viewport = not p.show_train_rays
    bm = bmesh.new()
    if R_rays > 0.001:
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, R_rays * math.cos(ang), R_rays * math.sin(ang)))
            create_arrow_bm(bm, train_origin, train_origin + tip_offset, p.ray_thickness)
    bm.to_mesh(m_t)
    bm.free()
    o_t.data.materials.clear()
    o_t.data.materials.append(create_material(p.ray_color, "Mat_Train"))

    # 3. Cone Rays
    o_o, m_o = sync_obj(f"YZ_Cone_Rays_{PREFIX}", TAG_O)
    o_o.hide_viewport = not p.show_cone_rays
    bm = bmesh.new()
    if R_rays > 0.001:
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, R_rays * math.cos(ang), R_rays * math.sin(ang)))
            create_arrow_bm(bm, cone_origin, train_origin + tip_offset, p.cone_thickness)
    bm.to_mesh(m_o)
    bm.free()
    o_o.data.materials.clear()
    o_o.data.materials.append(create_material(p.cone_color, "Mat_Cone"))

    # 4. If Train Rays (100% Extension)
    o_i, m_i = sync_obj(f"YZ_If_Train_Rays_{PREFIX}", TAG_I)
    o_i.hide_viewport = not p.show_if_train_rays
    bm = bmesh.new()
    if info["if_r_rays"] > 0.001:
        train_if_origin = Vector((info["if_x_pos"], 0, 0))
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, info["if_r_rays"] * math.cos(ang), info["if_r_rays"] * math.sin(ang)))
            create_arrow_bm(bm, train_if_origin, train_if_origin + tip_offset, p.if_train_thickness)
    bm.to_mesh(m_i)
    bm.free()
    o_i.data.materials.clear()
    o_i.data.materials.append(create_material(p.if_train_color, "Mat_If_Train"))

# ==============================================================================
#  自動更新用タイマー
# ==============================================================================
_timer = None
def delayed_update():
    global _timer
    _timer = None
    if bpy.context and bpy.context.scene:
        update_geometry(bpy.context)
    return None

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

# ==============================================================================
#  PROPERTIES
# ==============================================================================
class PG_YZProps(PropertyGroup):
    show_circle: BoolProperty(name="Show Circle", default=CURRENT_DEFAULTS["show_circle"], update=on_update)
    show_train_rays: BoolProperty(name="Show Train Rays", default=CURRENT_DEFAULTS["show_train_rays"], update=on_update)
    show_cone_rays: BoolProperty(name="Show Cone Rays", default=CURRENT_DEFAULTS["show_cone_rays"], update=on_update)
    show_if_train_rays: BoolProperty(name="Show If Train Rays", default=CURRENT_DEFAULTS["show_if_train_rays"], update=on_update)
    
    ray_color: FloatVectorProperty(name="Train Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS["ray_color"], update=on_update)
    cone_color: FloatVectorProperty(name="Cone Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS["cone_color"], update=on_update)
    circle_color: FloatVectorProperty(name="Circle Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS["circle_color"], update=on_update)
    if_train_color: FloatVectorProperty(name="If Train Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS["if_train_color"], update=on_update)
    
    emission_x: FloatProperty(name="Cone Emission X", default=CURRENT_DEFAULTS["emission_x"], update=on_update)
    speed_v: FloatProperty(name="Velocity (v/c)", default=CURRENT_DEFAULTS["speed_v"], min=0.0, max=0.99, update=on_update)
    time_t: FloatProperty(name="Time (t)", default=CURRENT_DEFAULTS["time_t"], min=0.1, update=on_update)
    
    circle_depth: FloatProperty(name="Axial Width", default=CURRENT_DEFAULTS["circle_depth"], min=0.0, max=10.0, update=on_update)
    circle_solid: FloatProperty(name="Face Solidify", default=CURRENT_DEFAULTS["circle_solid"], min=0.0, max=2.0, update=on_update)
    ray_thickness: FloatProperty(name="Train Ray Thick", default=CURRENT_DEFAULTS["ray_thickness"], min=0.01, max=10.0, update=on_update)
    cone_thickness: FloatProperty(name="Cone Ray Thick", default=CURRENT_DEFAULTS["cone_thickness"], min=0.01, max=10.0, update=on_update)
    if_train_thickness: FloatProperty(name="If Train Ray Thick", default=CURRENT_DEFAULTS["if_train_thickness"], min=0.01, max=10.0, update=on_update)

# ==============================================================================
#  OPERATORS
# ==============================================================================
class OT_ExecuteDraw(Operator):
    bl_idname = f"{OP_PREFIX}.execute_draw"
    bl_label = "Force Execute Draw"
    def execute(self, context):
        update_geometry(context)
        return {'FINISHED'}

class OT_CopyFullScript(Operator):
    bl_idname = f"{OP_PREFIX}.copy_script"
    bl_label = "Copy Script"
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return {'CANCELLED'}

        target_text = None
        for t in bpy.data.texts:
            if SOURCE_ID_TAG in t.as_string():
                target_text = t; break
        
        if not target_text:
            self.report({'ERROR'}, "Script source not found.")
            return {'CANCELLED'}

        code = target_text.as_string()
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_circle": {props.show_circle},\n'
        new_dict += f'    "show_train_rays": {props.show_train_rays},\n'
        new_dict += f'    "show_cone_rays": {props.show_cone_rays},\n'
        new_dict += f'    "show_if_train_rays": {props.show_if_train_rays},\n'
        
        rc, cc, circ, itc = props.ray_color, props.cone_color, props.circle_color, props.if_train_color
        new_dict += f'    "ray_color": ({rc[0]:.4f}, {rc[1]:.4f}, {rc[2]:.4f}, {rc[3]:.4f}),\n'
        new_dict += f'    "cone_color": ({cc[0]:.4f}, {cc[1]:.4f}, {cc[2]:.4f}, {cc[3]:.4f}),\n'
        new_dict += f'    "circle_color": ({circ[0]:.4f}, {circ[1]:.4f}, {circ[2]:.4f}, {circ[3]:.4f}),\n'
        new_dict += f'    "if_train_color": ({itc[0]:.4f}, {itc[1]:.4f}, {itc[2]:.4f}, {itc[3]:.4f}),\n'
        
        new_dict += f'    "emission_x": {props.emission_x:.4f},\n'
        new_dict += f'    "speed_v": {props.speed_v:.4f},\n'
        new_dict += f'    "time_t": {props.time_t:.4f},\n'
        new_dict += f'    "circle_depth": {props.circle_depth:.4f},\n'
        new_dict += f'    "circle_solid": {props.circle_solid:.4f},\n'
        new_dict += f'    "ray_thickness": {props.ray_thickness:.4f},\n'
        new_dict += f'    "cone_thickness": {props.cone_thickness:.4f},\n'
        new_dict += f'    "if_train_thickness": {props.if_train_thickness:.4f},\n'
        new_dict += "}\n"

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

class OT_CopyInfo(Operator):
    bl_idname = f"{OP_PREFIX}.copy_info"
    bl_label = "Copy Info"
    
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME, None)
        if not p: return {'CANCELLED'}
        
        info = get_physics_info(p)
        text = (
            f"[ YZ Rays Physics Info ]\n"
            f"Speed (v/c)       : {info['speed_pct']:.1f} %  ({p.speed_v:.2f}c)\n"
            f"Time (t)          : {p.time_t:.4f}\n"
            f"Ray Angle to X    : {info['angle_deg']:.2f} deg\n"
            f"---------------------------------\n"
            f"Real Reach X Pos  : {info['x_pos']:.4f}\n"
            f"Real Wave Radius  : {info['r_rays']:.4f}\n"
            f"---------------------------------\n"
            f"If 100% Reach X   : {info['if_x_pos']:.4f}\n"
            f"If 100% Radius    : {info['if_r_rays']:.4f}\n"
        )
        context.window_manager.clipboard = text
        self.report({'INFO'}, "Physics Info Copied!")
        return {'FINISHED'}

class OT_Reset(Operator):
    bl_idname = f"{OP_PREFIX}.reset"
    bl_label = "Reset View"
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME)
        p.emission_x = 0.0; p.speed_v = 0.6; p.time_t = 100.0
        return {'FINISHED'}

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

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

# ==============================================================================
#  PANELS
# ==============================================================================
class PT_MainPanel(Panel):
    bl_label = "YZ Plane Rays (V40)"
    bl_idname = f"{PREFIX}_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

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

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

        col = layout.column()
        col.scale_y = 1.2
        col.operator(OT_ExecuteDraw.bl_idname, icon='PLAY', text="Force Execute Draw")
        layout.separator()

        # UI: Physics Properties
        phys = layout.box()
        phys.label(text="Physics Parameters", icon='PHYSICS')
        phys.prop(props, "emission_x")
        phys.prop(props, "speed_v")
        phys.prop(props, "time_t")
        phys.operator(OT_Reset.bl_idname, icon='LOOP_BACK', text="Reset Params")

        # UI: Realtime Info
        info_box = layout.box()
        info_box.label(text="Realtime Info", icon='INFO')
        info = get_physics_info(props)
        info_box.label(text=f"Speed: {info['speed_pct']:.1f} % ({props.speed_v:.2f}c)")
        info_box.label(text=f"Angle to X: {info['angle_deg']:.2f} °")
        info_box.label(text=f"[ Real ] X: {info['x_pos']:.2f} | R: {info['r_rays']:.2f}")
        info_box.label(text=f"[ If 100% ] X: {info['if_x_pos']:.2f} | R: {info['if_r_rays']:.2f}")
        
        info_box.operator(OT_CopyInfo.bl_idname, icon='COPYDOWN', text="Copy Info to Clipboard")
        layout.separator()

        # UI: Visuals
        box = layout.box()
        box.prop(props, "show_circle")
        if props.show_circle:
            box.prop(props, "circle_color")
            box.prop(props, "circle_depth")
            box.prop(props, "circle_solid")

        box = layout.box()
        box.prop(props, "show_train_rays")
        if props.show_train_rays:
            box.prop(props, "ray_color")
            box.prop(props, "ray_thickness")

        box = layout.box()
        box.prop(props, "show_cone_rays")
        if props.show_cone_rays:
            box.prop(props, "cone_color")
            box.prop(props, "cone_thickness")

        box = layout.box()
        box.prop(props, "show_if_train_rays")
        if props.show_if_train_rays:
            box.prop(props, "if_train_color")
            box.prop(props, "if_train_thickness")

class PT_LinksPanel(Panel):
    bl_label = "Theory 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"], icon='WORLD').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_YZProps, 
    OT_ExecuteDraw, 
    OT_CopyFullScript, 
    OT_CopyInfo,
    OT_Reset,
    OT_OpenUrl, 
    OT_RemoveAddon, 
    PT_MainPanel, 
    PT_LinksPanel, 
    PT_RemovePanel
)

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_YZProps))

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: 15:00:01
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, Matrix
from datetime import datetime

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

PREFIX = "YZRays20260318"
TAB_NAME = "   [ YZ Rays ]   "

# ### ZIONAD_SOURCE_ID: YZ_RAYS_2026_03_18_FIXED ###

bl_info = {
    "name": f"zionad 520 [ YZ Rays ] {PREFIX}",
    "author": "zionadchat",
    "version": (1, 30, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": "YZ Plane 12 Rays Generator with Face Solidify",
    "category": "3D View",
}

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

ADDON_LINKS = (
    {"label": "Theory Background", "url": "<https://www.notion.so/>"},
    {"label": "Blender Guide", "url": "<https://www.notion.so/>"},
)

# ==============================================================================
#  デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_circle": True,
    "show_train_rays": True,
    "show_cone_rays": True,
    "ray_color": (0.8000, 0.8000, 0.1000, 1.0000),
    "cone_color": (0.8000, 0.2000, 0.1000, 0.6000),
    "circle_color": (0.1000, 0.5000, 0.8000, 0.8000),
    "emission_x": 0.0000,
    "speed_v": 0.6000,
    "time_t": 100.0000,
    "circle_depth": 0.1000,
    "circle_solid": 0.0500,
    "ray_thickness": 1.0000,
    "cone_thickness": 1.0000,
}
# <END_DICT>

# ==============================================================================
#  物理情報計算ロジック
# ==============================================================================
def get_physics_info(p):
    c = 1.0
    v = min(0.9999, max(0.0, p.speed_v))
    t = p.time_t
    
    # 座標と半径の計算
    x_pos = p.emission_x + (v * t)
    val = (c * t)**2 - (v * t)**2
    r_rays = math.sqrt(max(0, val))
    
    # 角度の計算 (X軸に対する角度 / ZX平面での角度)
    angle_rad = math.acos(v / c)
    angle_deg = math.degrees(angle_rad)
    
    return {
        "speed_pct": v * 100.0,
        "x_pos": x_pos,
        "r_rays": r_rays,
        "angle_deg": angle_deg
    }

# ==============================================================================
#  マテリアル作成ロジック
# ==============================================================================
def create_material(color, name_prefix="Mat"):
    mat_name = f"{name_prefix}_{PREFIX}"
    
    mat = bpy.data.materials.get(mat_name)
    if not mat:
        mat = bpy.data.materials.new(name=mat_name)
        
    mat.use_nodes = True
    mat.blend_method = 'BLEND'
    
    if mat.use_nodes:
        tree = mat.node_tree
        tree.nodes.clear()
        
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        bsdf.location = (0, 0)
        
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        out.location = (300, 0)
        
        tree.links.new(bsdf.outputs[0], out.inputs[0])
        
        if "Base Color" in bsdf.inputs:
            bsdf.inputs['Base Color'].default_value = color
        if "Alpha" in bsdf.inputs:
            bsdf.inputs['Alpha'].default_value = color[3]
            
    mat.diffuse_color = color
    return mat

# ==============================================================================
#  ジオメトリエンジン & 描画ロジック
# ==============================================================================
def create_arrow_bm(bm, start, end, thick):
    vec = end - start
    length = vec.length
    if length < 0.001: return
    s_len, h_len = length * 0.9, length * 0.1
    s = bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=thick, radius2=thick, depth=s_len)
    bmesh.ops.translate(bm, verts=s['verts'], vec=Vector((0, 0, s_len/2)))
    h = bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=thick*2, radius2=0, depth=h_len)
    bmesh.ops.translate(bm, verts=h['verts'], vec=Vector((0, 0, s_len + h_len/2)))
    rot = Vector((0, 0, 1)).rotation_difference(vec.normalized())
    bmesh.ops.rotate(bm, verts=list(s['verts']) + list(h['verts']), cent=(0,0,0), matrix=rot.to_matrix().to_4x4())
    bmesh.ops.translate(bm, verts=list(s['verts']) + list(h['verts']), vec=start)

OUTPUT_COL_NAME = f"{PREFIX}_Output"
TAG_C, TAG_T, TAG_O = f"{PREFIX}_c", f"{PREFIX}_t", f"{PREFIX}_o"

def update_geometry(context):
    p = getattr(context.scene, PROPS_NAME, None)
    if not p: return

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

    def sync_obj(name, tag):
        obj = next((o for o in col.objects if o.get(tag)), None)
        if obj: 
            obj.data.clear_geometry()
            return obj, obj.data
        mesh = bpy.data.meshes.new(name)
        obj = bpy.data.objects.new(name, mesh)
        obj[tag] = True
        col.objects.link(obj)
        return obj, mesh

    info = get_physics_info(p)
    v, t = p.speed_v, p.time_t
    R_circle = 1.0 * t
    R_rays = info["r_rays"]
    
    cone_origin = Vector((p.emission_x, 0, 0))
    train_origin = Vector((info["x_pos"], 0, 0))

    # 1. Circle
    o_c, m_c = sync_obj(f"YZ_Circle_{PREFIX}", TAG_C)
    o_c.hide_viewport = not p.show_circle
    bm = bmesh.new()
    res_c = bmesh.ops.create_cone(bm, cap_ends=False, segments=96, radius1=R_circle, radius2=R_circle, depth=p.circle_depth)
    
    circle_faces = [f for f in res_c.get('geom', res_c.get('faces', [])) if isinstance(f, bmesh.types.BMFace)]
    if not circle_faces: circle_faces = bm.faces[:]
    
    if p.circle_solid > 0:
        bmesh.ops.solidify(bm, geom=circle_faces, thickness=p.circle_solid)
        
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    bmesh.ops.rotate(bm, verts=bm.verts, cent=(0,0,0), matrix=Matrix.Rotation(math.radians(90), 4, 'Y'))
    
    bmesh.ops.translate(bm, verts=bm.verts, vec=train_origin)
    
    bm.to_mesh(m_c)
    bm.free()
    o_c.data.materials.clear()
    o_c.data.materials.append(create_material(p.circle_color, "Mat_Circle"))

    # 2. Train Rays
    o_t, m_t = sync_obj(f"YZ_Train_Rays_{PREFIX}", TAG_T)
    o_t.hide_viewport = not p.show_train_rays
    bm = bmesh.new()
    if R_rays > 0.001:
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, R_rays * math.cos(ang), R_rays * math.sin(ang)))
            create_arrow_bm(bm, train_origin, train_origin + tip_offset, p.ray_thickness)
    bm.to_mesh(m_t)
    bm.free()
    o_t.data.materials.clear()
    o_t.data.materials.append(create_material(p.ray_color, "Mat_Train"))

    # 3. Cone Rays
    o_o, m_o = sync_obj(f"YZ_Cone_Rays_{PREFIX}", TAG_O)
    o_o.hide_viewport = not p.show_cone_rays
    bm = bmesh.new()
    if R_rays > 0.001:
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, R_rays * math.cos(ang), R_rays * math.sin(ang)))
            create_arrow_bm(bm, cone_origin, train_origin + tip_offset, p.cone_thickness)
    bm.to_mesh(m_o)
    bm.free()
    o_o.data.materials.clear()
    o_o.data.materials.append(create_material(p.cone_color, "Mat_Cone"))

# ==============================================================================
#  自動更新用タイマー
# ==============================================================================
_timer = None
def delayed_update():
    global _timer
    _timer = None
    if bpy.context and bpy.context.scene:
        update_geometry(bpy.context)
    return None

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

# ==============================================================================
#  PROPERTIES
# ==============================================================================
class PG_YZProps(PropertyGroup):
    show_circle: BoolProperty(name="Show Circle", default=CURRENT_DEFAULTS["show_circle"], update=on_update)
    show_train_rays: BoolProperty(name="Show Train Rays", default=CURRENT_DEFAULTS["show_train_rays"], update=on_update)
    show_cone_rays: BoolProperty(name="Show Cone Rays", default=CURRENT_DEFAULTS["show_cone_rays"], update=on_update)
    
    ray_color: FloatVectorProperty(name="Train Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS["ray_color"], update=on_update)
    cone_color: FloatVectorProperty(name="Cone Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS["cone_color"], update=on_update)
    circle_color: FloatVectorProperty(name="Circle Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS["circle_color"], update=on_update)
    
    emission_x: FloatProperty(name="Cone Emission X", default=CURRENT_DEFAULTS["emission_x"], update=on_update)
    speed_v: FloatProperty(name="Velocity (v/c)", default=CURRENT_DEFAULTS["speed_v"], min=0.0, max=0.99, update=on_update)
    time_t: FloatProperty(name="Time (t)", default=CURRENT_DEFAULTS["time_t"], min=0.1, update=on_update)
    
    circle_depth: FloatProperty(name="Axial Width", default=CURRENT_DEFAULTS["circle_depth"], min=0.0, max=10.0, update=on_update)
    circle_solid: FloatProperty(name="Face Solidify", default=CURRENT_DEFAULTS["circle_solid"], min=0.0, max=2.0, update=on_update)
    ray_thickness: FloatProperty(name="Train Ray Thick", default=CURRENT_DEFAULTS["ray_thickness"], min=0.01, max=10.0, update=on_update)
    cone_thickness: FloatProperty(name="Cone Ray Thick", default=CURRENT_DEFAULTS["cone_thickness"], min=0.01, max=10.0, update=on_update)

# ==============================================================================
#  OPERATORS
# ==============================================================================
class OT_ExecuteDraw(Operator):
    bl_idname = f"{OP_PREFIX}.execute_draw"
    bl_label = "Force Execute Draw"
    def execute(self, context):
        update_geometry(context)
        return {'FINISHED'}

class OT_CopyFullScript(Operator):
    bl_idname = f"{OP_PREFIX}.copy_script"
    bl_label = "Copy Script"
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return {'CANCELLED'}

        target_text = None
        for t in bpy.data.texts:
            if SOURCE_ID_TAG in t.as_string():
                target_text = t; break
        
        if not target_text:
            self.report({'ERROR'}, "Script source not found.")
            return {'CANCELLED'}

        code = target_text.as_string()
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_circle": {props.show_circle},\n'
        new_dict += f'    "show_train_rays": {props.show_train_rays},\n'
        new_dict += f'    "show_cone_rays": {props.show_cone_rays},\n'
        
        rc, cc, circ = props.ray_color, props.cone_color, props.circle_color
        new_dict += f'    "ray_color": ({rc[0]:.4f}, {rc[1]:.4f}, {rc[2]:.4f}, {rc[3]:.4f}),\n'
        new_dict += f'    "cone_color": ({cc[0]:.4f}, {cc[1]:.4f}, {cc[2]:.4f}, {cc[3]:.4f}),\n'
        new_dict += f'    "circle_color": ({circ[0]:.4f}, {circ[1]:.4f}, {circ[2]:.4f}, {circ[3]:.4f}),\n'
        
        new_dict += f'    "emission_x": {props.emission_x:.4f},\n'
        new_dict += f'    "speed_v": {props.speed_v:.4f},\n'
        new_dict += f'    "time_t": {props.time_t:.4f},\n'
        new_dict += f'    "circle_depth": {props.circle_depth:.4f},\n'
        new_dict += f'    "circle_solid": {props.circle_solid:.4f},\n'
        new_dict += f'    "ray_thickness": {props.ray_thickness:.4f},\n'
        new_dict += f'    "cone_thickness": {props.cone_thickness:.4f},\n'
        new_dict += "}\n"

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

class OT_CopyInfo(Operator):
    bl_idname = f"{OP_PREFIX}.copy_info"
    bl_label = "Copy Info"
    
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME, None)
        if not p: return {'CANCELLED'}
        
        info = get_physics_info(p)
        text = (
            f"[ YZ Rays Physics Info ]\n"
            f"Speed (v/c)       : {info['speed_pct']:.1f} %  ({p.speed_v:.2f}c)\n"
            f"Time (t)          : {p.time_t:.4f}\n"
            f"Ray Reach X Pos   : {info['x_pos']:.4f}\n"
            f"Wavefront Radius  : {info['r_rays']:.4f}\n"
            f"Ray Angle to X    : {info['angle_deg']:.2f} deg\n"
        )
        context.window_manager.clipboard = text
        self.report({'INFO'}, "Physics Info Copied!")
        return {'FINISHED'}

class OT_Reset(Operator):
    bl_idname = f"{OP_PREFIX}.reset"
    bl_label = "Reset View"
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME)
        p.emission_x = 0.0; p.speed_v = 0.6; p.time_t = 100.0
        return {'FINISHED'}

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

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

# ==============================================================================
#  PANELS
# ==============================================================================
class PT_MainPanel(Panel):
    bl_label = "YZ Plane Rays (V30)"
    bl_idname = f"{PREFIX}_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

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

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

        col = layout.column()
        col.scale_y = 1.2
        col.operator(OT_ExecuteDraw.bl_idname, icon='PLAY', text="Force Execute Draw")
        layout.separator()

        # UI: Physics Properties
        phys = layout.box()
        phys.label(text="Physics Parameters", icon='PHYSICS')
        phys.prop(props, "emission_x")
        phys.prop(props, "speed_v")
        phys.prop(props, "time_t")
        phys.operator(OT_Reset.bl_idname, icon='LOOP_BACK', text="Reset Params")

        # UI: Realtime Info
        info_box = layout.box()
        info_box.label(text="Realtime Info", icon='INFO')
        info = get_physics_info(props)
        info_box.label(text=f"Speed: {info['speed_pct']:.1f} % ({props.speed_v:.2f}c)")
        info_box.label(text=f"Reach X: {info['x_pos']:.4f}")
        info_box.label(text=f"Wave Radius: {info['r_rays']:.4f}")
        info_box.label(text=f"Angle to X (ZX plane): {info['angle_deg']:.2f} °")
        
        info_box.operator(OT_CopyInfo.bl_idname, icon='COPYDOWN', text="Copy Info to Clipboard")
        layout.separator()

        # UI: Visuals
        box = layout.box()
        box.prop(props, "show_circle")
        if props.show_circle:
            box.prop(props, "circle_color")
            box.prop(props, "circle_depth")
            box.prop(props, "circle_solid")

        box = layout.box()
        box.prop(props, "show_train_rays")
        if props.show_train_rays:
            box.prop(props, "ray_color")
            box.prop(props, "ray_thickness")

        box = layout.box()
        box.prop(props, "show_cone_rays")
        if props.show_cone_rays:
            box.prop(props, "cone_color")
            box.prop(props, "cone_thickness")

class PT_LinksPanel(Panel):
    bl_label = "Theory 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"], icon='WORLD').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_YZProps, 
    OT_ExecuteDraw, 
    OT_CopyFullScript, 
    OT_CopyInfo,
    OT_Reset,
    OT_OpenUrl, 
    OT_RemoveAddon, 
    PT_MainPanel, 
    PT_LinksPanel, 
    PT_RemovePanel
)

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_YZProps))

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: 15:00:01
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, Matrix
from datetime import datetime

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

PREFIX = "YZRays20260318"
TAB_NAME = "   [ YZ Rays ]   "

# ### ZIONAD_SOURCE_ID: YZ_RAYS_2026_03_18_FIXED ###

bl_info = {
    "name": f"zionad 520 [ YZ Rays ] {PREFIX}",
    "author": "zionadchat",
    "version": (1, 30, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": "YZ Plane 12 Rays Generator with Face Solidify",
    "category": "3D View",
}

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

ADDON_LINKS = (
    {"label": "Theory Background", "url": "<https://www.notion.so/>"},
    {"label": "Blender Guide", "url": "<https://www.notion.so/>"},
)

# ==============================================================================
#  デフォルト値設定 (RGBA対応 & Cone発射位置追加)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_circle": True,
    "show_train_rays": True,
    "show_cone_rays": True,
    "ray_color": (0.8000, 0.8000, 0.1000, 1.0000),
    "cone_color": (0.8000, 0.2000, 0.1000, 0.6000),
    "circle_color": (0.1000, 0.5000, 0.8000, 0.8000),
    "emission_x": 0.0000,
    "speed_v": 0.6000,
    "time_t": 100.0000,
    "circle_depth": 0.1000,
    "circle_solid": 0.0500,
    "ray_thickness": 1.0000,
    "cone_thickness": 1.0000,
}
# <END_DICT>

# ==============================================================================
#  マテリアル作成ロジック
# ==============================================================================
def create_material(color, name_prefix="Mat"):
    mat_name = f"{name_prefix}_{PREFIX}"
    
    mat = bpy.data.materials.get(mat_name)
    if not mat:
        mat = bpy.data.materials.new(name=mat_name)
        
    mat.use_nodes = True
    mat.blend_method = 'BLEND'
    
    if mat.use_nodes:
        tree = mat.node_tree
        tree.nodes.clear()
        
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        bsdf.location = (0, 0)
        
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        out.location = (300, 0)
        
        tree.links.new(bsdf.outputs[0], out.inputs[0])
        
        if "Base Color" in bsdf.inputs:
            bsdf.inputs['Base Color'].default_value = color
        if "Alpha" in bsdf.inputs:
            bsdf.inputs['Alpha'].default_value = color[3]
            
    mat.diffuse_color = color
    return mat

# ==============================================================================
#  ジオメトリエンジン & 描画ロジック
# ==============================================================================
def create_arrow_bm(bm, start, end, thick):
    vec = end - start
    length = vec.length
    if length < 0.001: return
    s_len, h_len = length * 0.9, length * 0.1
    s = bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=thick, radius2=thick, depth=s_len)
    bmesh.ops.translate(bm, verts=s['verts'], vec=Vector((0, 0, s_len/2)))
    h = bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=thick*2, radius2=0, depth=h_len)
    bmesh.ops.translate(bm, verts=h['verts'], vec=Vector((0, 0, s_len + h_len/2)))
    rot = Vector((0, 0, 1)).rotation_difference(vec.normalized())
    bmesh.ops.rotate(bm, verts=list(s['verts']) + list(h['verts']), cent=(0,0,0), matrix=rot.to_matrix().to_4x4())
    bmesh.ops.translate(bm, verts=list(s['verts']) + list(h['verts']), vec=start)

OUTPUT_COL_NAME = f"{PREFIX}_Output"
TAG_C, TAG_T, TAG_O = f"{PREFIX}_c", f"{PREFIX}_t", f"{PREFIX}_o"

def update_geometry(context):
    p = getattr(context.scene, PROPS_NAME, None)
    if not p: return

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

    def sync_obj(name, tag):
        obj = next((o for o in col.objects if o.get(tag)), None)
        if obj: 
            obj.data.clear_geometry()
            return obj, obj.data
        mesh = bpy.data.meshes.new(name)
        obj = bpy.data.objects.new(name, mesh)
        obj[tag] = True
        col.objects.link(obj)
        return obj, mesh

    c, t, v = 1.0, p.time_t, p.speed_v
    R_circle = c * t
    val = (c*t)**2 - (v*t)**2
    R_rays = math.sqrt(max(0, val))
    
    # ★ 指定されたX座標からConeが発射され、TrainとCircleがv*tで進んでいく計算
    cone_origin = Vector((p.emission_x, 0, 0))
    train_origin = Vector((p.emission_x + (v * t), 0, 0))

    # 1. Circle
    o_c, m_c = sync_obj(f"YZ_Circle_{PREFIX}", TAG_C)
    o_c.hide_viewport = not p.show_circle
    bm = bmesh.new()
    res_c = bmesh.ops.create_cone(bm, cap_ends=False, segments=96, radius1=R_circle, radius2=R_circle, depth=p.circle_depth)
    
    circle_faces = [f for f in res_c.get('geom', res_c.get('faces', [])) if isinstance(f, bmesh.types.BMFace)]
    if not circle_faces: circle_faces = bm.faces[:]
    
    if p.circle_solid > 0:
        bmesh.ops.solidify(bm, geom=circle_faces, thickness=p.circle_solid)
        
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    bmesh.ops.rotate(bm, verts=bm.verts, cent=(0,0,0), matrix=Matrix.Rotation(math.radians(90), 4, 'Y'))
    
    # 円を追随位置(train_origin)に移動
    bmesh.ops.translate(bm, verts=bm.verts, vec=train_origin)
    
    bm.to_mesh(m_c)
    bm.free()
    o_c.data.materials.clear()
    o_c.data.materials.append(create_material(p.circle_color, "Mat_Circle"))

    # 2. Train Rays
    o_t, m_t = sync_obj(f"YZ_Train_Rays_{PREFIX}", TAG_T)
    o_t.hide_viewport = not p.show_train_rays
    bm = bmesh.new()
    if R_rays > 0.001:
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, R_rays * math.cos(ang), R_rays * math.sin(ang)))
            # 電車の光は、追随位置から発射される
            create_arrow_bm(bm, train_origin, train_origin + tip_offset, p.ray_thickness)
    bm.to_mesh(m_t)
    bm.free()
    o_t.data.materials.clear()
    o_t.data.materials.append(create_material(p.ray_color, "Mat_Train"))

    # 3. Cone Rays
    o_o, m_o = sync_obj(f"YZ_Cone_Rays_{PREFIX}", TAG_O)
    o_o.hide_viewport = not p.show_cone_rays
    bm = bmesh.new()
    if R_rays > 0.001:
        for i in range(12):
            ang = math.radians(i * 30)
            tip_offset = Vector((0, R_rays * math.cos(ang), R_rays * math.sin(ang)))
            # 地上の光円錐は、指定された発射位置から追随先端へ伸びる
            create_arrow_bm(bm, cone_origin, train_origin + tip_offset, p.cone_thickness)
    bm.to_mesh(m_o)
    bm.free()
    o_o.data.materials.clear()
    o_o.data.materials.append(create_material(p.cone_color, "Mat_Cone"))

# ==============================================================================
#  自動更新用タイマー
# ==============================================================================
_timer = None
def delayed_update():
    global _timer
    _timer = None
    if bpy.context and bpy.context.scene:
        update_geometry(bpy.context)
    return None

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

# ==============================================================================
#  PROPERTIES
# ==============================================================================
class PG_YZProps(PropertyGroup):
    show_circle: BoolProperty(name="Show Circle", default=CURRENT_DEFAULTS["show_circle"], update=on_update)
    show_train_rays: BoolProperty(name="Show Train Rays", default=CURRENT_DEFAULTS["show_train_rays"], update=on_update)
    show_cone_rays: BoolProperty(name="Show Cone Rays", default=CURRENT_DEFAULTS["show_cone_rays"], update=on_update)
    
    ray_color: FloatVectorProperty(name="Train Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS["ray_color"], update=on_update)
    cone_color: FloatVectorProperty(name="Cone Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS["cone_color"], update=on_update)
    circle_color: FloatVectorProperty(name="Circle Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS["circle_color"], update=on_update)
    
    emission_x: FloatProperty(name="Cone Emission X", default=CURRENT_DEFAULTS["emission_x"], update=on_update)
    speed_v: FloatProperty(name="Velocity (v/c)", default=CURRENT_DEFAULTS["speed_v"], min=0.0, max=0.99, update=on_update)
    time_t: FloatProperty(name="Time (t)", default=CURRENT_DEFAULTS["time_t"], min=0.1, update=on_update)
    
    circle_depth: FloatProperty(name="Axial Width", default=CURRENT_DEFAULTS["circle_depth"], min=0.0, max=10.0, update=on_update)
    circle_solid: FloatProperty(name="Face Solidify", default=CURRENT_DEFAULTS["circle_solid"], min=0.0, max=2.0, update=on_update)
    ray_thickness: FloatProperty(name="Train Ray Thick", default=CURRENT_DEFAULTS["ray_thickness"], min=0.01, max=10.0, update=on_update)
    cone_thickness: FloatProperty(name="Cone Ray Thick", default=CURRENT_DEFAULTS["cone_thickness"], min=0.01, max=10.0, update=on_update)

# ==============================================================================
#  OPERATORS
# ==============================================================================
class OT_ExecuteDraw(Operator):
    bl_idname = f"{OP_PREFIX}.execute_draw"
    bl_label = "Force Execute Draw"
    def execute(self, context):
        update_geometry(context)
        return {'FINISHED'}

class OT_CopyFullScript(Operator):
    bl_idname = f"{OP_PREFIX}.copy_script"
    bl_label = "Copy Script"
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return {'CANCELLED'}

        target_text = None
        for t in bpy.data.texts:
            if SOURCE_ID_TAG in t.as_string():
                target_text = t; break
        
        if not target_text:
            self.report({'ERROR'}, "Script source not found.")
            return {'CANCELLED'}

        code = target_text.as_string()
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_circle": {props.show_circle},\n'
        new_dict += f'    "show_train_rays": {props.show_train_rays},\n'
        new_dict += f'    "show_cone_rays": {props.show_cone_rays},\n'
        
        rc, cc, circ = props.ray_color, props.cone_color, props.circle_color
        new_dict += f'    "ray_color": ({rc[0]:.4f}, {rc[1]:.4f}, {rc[2]:.4f}, {rc[3]:.4f}),\n'
        new_dict += f'    "cone_color": ({cc[0]:.4f}, {cc[1]:.4f}, {cc[2]:.4f}, {cc[3]:.4f}),\n'
        new_dict += f'    "circle_color": ({circ[0]:.4f}, {circ[1]:.4f}, {circ[2]:.4f}, {circ[3]:.4f}),\n'
        
        new_dict += f'    "emission_x": {props.emission_x:.4f},\n'
        new_dict += f'    "speed_v": {props.speed_v:.4f},\n'
        new_dict += f'    "time_t": {props.time_t:.4f},\n'
        new_dict += f'    "circle_depth": {props.circle_depth:.4f},\n'
        new_dict += f'    "circle_solid": {props.circle_solid:.4f},\n'
        new_dict += f'    "ray_thickness": {props.ray_thickness:.4f},\n'
        new_dict += f'    "cone_thickness": {props.cone_thickness:.4f},\n'
        new_dict += "}\n"

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

class OT_Reset(Operator):
    bl_idname = f"{OP_PREFIX}.reset"
    bl_label = "Reset View"
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME)
        p.emission_x = 0.0; p.speed_v = 0.6; p.time_t = 100.0
        return {'FINISHED'}

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

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

# ==============================================================================
#  PANELS
# ==============================================================================
class PT_MainPanel(Panel):
    bl_label = "YZ Plane Rays (V30)"
    bl_idname = f"{PREFIX}_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

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

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

        col = layout.column()
        col.scale_y = 1.2
        col.operator(OT_ExecuteDraw.bl_idname, icon='PLAY', text="Force Execute Draw")
        layout.separator()

        box = layout.box()
        box.prop(props, "show_circle")
        if props.show_circle:
            box.prop(props, "circle_color")
            box.prop(props, "circle_depth")
            box.prop(props, "circle_solid")

        box = layout.box()
        box.prop(props, "show_train_rays")
        if props.show_train_rays:
            box.prop(props, "ray_color")
            box.prop(props, "ray_thickness")

        box = layout.box()
        box.prop(props, "show_cone_rays")
        if props.show_cone_rays:
            box.prop(props, "cone_color")
            box.prop(props, "cone_thickness")

        phys = layout.box()
        phys.label(text="Physics Parameters", icon='PHYSICS')
        phys.prop(props, "emission_x")
        phys.prop(props, "speed_v")
        phys.prop(props, "time_t")
        phys.operator(OT_Reset.bl_idname, icon='LOOP_BACK', text="Reset Params")

class PT_LinksPanel(Panel):
    bl_label = "Theory 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"], icon='WORLD').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_YZProps, 
    OT_ExecuteDraw, 
    OT_CopyFullScript, 
    OT_Reset,
    OT_OpenUrl, 
    OT_RemoveAddon, 
    PT_MainPanel, 
    PT_LinksPanel, 
    PT_RemovePanel
)

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_YZProps))

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()