I have rebuilt the script 
to be strictly ASCII-only (no Japanese characters anywhere) 
and made the Copy logic more robust to ensure it 
captures the latest values without breaking.

I have corrected the script to include the missing Train Disk 
and Railway Disk panels. 
I have also improved the Export Script logic to ensure that 
every single property—including the disk colors 
and thicknesses—is perfectly updated in the output text block.

This version is 100% English and uses standard ASCII characters 
to prevent any syntax errors.

平面矢印 (Planar Rays): 放射状の太さ、色、透明度
円錐矢印 (Conical Rays): 放射状の太さ、色、透明度
列車円盤 (Train Disk): 板の厚み(奥行き)、色、透明度
線路円盤 (Railway Disk): 板の厚み(奥行き)、色、透明度
共通: アドオン削除、コピー、理論リンクの全機能

context.window_manager.clipboard は
BlenderのクリップボードAPIで、
長いテキストが途中で切れる既知の制限があります。

代わりに、Blender内のTextブロックに書き出す方式に変更します。
コピーボタン押下時に YZ_RAYS_EXPORT という名前の
Textブロックを作成・上書きし、
ユーザーはそこから全文をコピーできるようにします。






# 2026-02-22 Session Template
# Blender 5.0+ | V46 - Unified Colors & Fixed Value Export

UNIQUE_ID = "YZ_RAYS_V46_STABLE"

bl_info = {
    "name": "YZ Plane 12 Rays Generator (V46)",
    "author": "zionadchat Gemini + Claude",
    "version": (1, 46),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > YZ_Rays",
    "description": "Shaft & arrowhead use same color. 100% accurate export.",
    "category": "Object",
}

import bpy
import bmesh
import math
import webbrowser
from mathutils import Vector, Matrix

# ==============================================================================
#  FIXED RATIO
# ==============================================================================
HEAD_RATIO = 2.5

# ==============================================================================
#  DICTIONARY DATA - Source of Truth
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "speed_v": 0.6000,
    "time_t": 10.0000,
    "p_show": True,
    "p_color": (1.0000, 0.9000, 0.0000),
    "p_thick": 0.0500,
    "p_alpha": 1.0000,
    "p_head_alpha": 1.0000,
    "s_show": True,
    "s_color": (1.0000, 0.3000, 0.0000),
    "s_thick": 0.0400,
    "s_alpha": 0.8000,
    "s_head_alpha": 0.8000,
    "show_tr_disk": True,
    "tr_disk_col": (0.1000, 0.4000, 0.8000),
    "tr_disk_alpha": 0.2000,
    "tr_disk_thick": 0.0100,
    "show_rw_disk": True,
    "rw_disk_col": (0.1000, 0.8000, 0.5000),
    "rw_disk_alpha": 0.1500,
    "rw_disk_thick": 0.0100,
}
# <END_DICT>

TAB_NAME = "YZ_Rays"
COL_NAME = "YZ_Rays_Output"

# ==============================================================================
#  CORE FUNCTIONS
# ==============================================================================
def get_physics_mat(name, color, alpha):
    m_name = f"Mat_{name}_V46"
    mat = bpy.data.materials.get(m_name) or bpy.data.materials.new(name=m_name)
    mat.use_nodes = True
    if hasattr(mat, "blend_method"): mat.blend_method = 'BLEND'
    nodes = mat.node_tree.nodes
    nodes.clear()
    out = nodes.new('ShaderNodeOutputMaterial')
    bsdf = nodes.new('ShaderNodeBsdfPrincipled')
    bsdf.inputs["Base Color"].default_value = (*color, 1.0)
    bsdf.inputs["Alpha"].default_value = alpha
    bsdf.inputs["Emission Color"].default_value = (*color, 1.0)
    bsdf.inputs["Emission Strength"].default_value = 1.5
    mat.node_tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
    return mat

def build_arrow_dual(bm_s, bm_h, start, end, s_thick, h_thick):
    direction = end - start
    dist = direction.length
    if dist < 0.01: return
    s_h, h_h = dist * 0.82, dist * 0.18
    rot = Vector((0, 0, 1)).rotation_difference(direction.normalized()).to_matrix().to_4x4()
    
    # Shaft
    res_s = bmesh.ops.create_cone(bm_s, cap_ends=True, segments=12, radius1=s_thick, radius2=s_thick, depth=s_h)
    bmesh.ops.translate(bm_s, verts=res_s['verts'], vec=Vector((0, 0, s_h/2)))
    bmesh.ops.rotate(bm_s, verts=res_s['verts'], cent=(0, 0, 0), matrix=rot)
    bmesh.ops.translate(bm_s, verts=res_s['verts'], vec=start)
    
    # Head
    res_h = bmesh.ops.create_cone(bm_h, cap_ends=True, segments=12, radius1=h_thick, radius2=0.0, depth=h_h)
    bmesh.ops.translate(bm_h, verts=res_h['verts'], vec=Vector((0, 0, s_h + h_h/2)))
    bmesh.ops.rotate(bm_h, verts=res_h['verts'], cent=(0, 0, 0), matrix=rot)
    bmesh.ops.translate(bm_h, verts=res_h['verts'], vec=start)

def draw_physics_scene(context):
    p = context.scene.yz_rays_props
    col = bpy.data.collections.get(COL_NAME) or bpy.data.collections.new(COL_NAME)
    if col.name not in context.scene.collection.children: context.scene.collection.children.link(col)

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

    t, v = p.time_t, p.speed_v
    r_c = math.sqrt(max(0.0, t**2 - (v*t)**2))
    e_pt, c_pt = Vector((-v*t, 0, 0)), Vector((0, 0, 0))

    # 1. Planar (Unified Color)
    o_ps, m_ps = get_obj("R_P_Shaft", "t_ps"); o_ph, m_ph = get_obj("R_P_Head", "t_ph")
    o_ps.hide_viewport = o_ph.hide_viewport = not p.p_show
    bm_s, bm_h = bmesh.new(), bmesh.new()
    for i in range(12):
        ang = math.radians(i*30)
        tgt = Vector((0, r_c * math.cos(ang), r_c * math.sin(ang)))
        build_arrow_dual(bm_s, bm_h, c_pt, tgt, p.p_thick, p.p_thick * HEAD_RATIO)
    bm_s.to_mesh(m_ps); bm_h.to_mesh(m_ph); bm_s.free(); bm_h.free()
    o_ps.data.materials.clear(); o_ps.data.materials.append(get_physics_mat("P_S", p.p_color, p.p_alpha))
    o_ph.data.materials.clear(); o_ph.data.materials.append(get_physics_mat("P_H", p.p_color, p.p_head_alpha))

    # 2. Conical (Unified Color)
    o_ss, m_ss = get_obj("R_S_Shaft", "t_ss"); o_sh, m_sh = get_obj("R_S_Head", "t_sh")
    o_ss.hide_viewport = o_sh.hide_viewport = not p.s_show
    bm_s, bm_h = bmesh.new(), bmesh.new()
    for i in range(12):
        ang = math.radians(i*30)
        tgt = Vector((0, r_c * math.cos(ang), r_c * math.sin(ang)))
        build_arrow_dual(bm_s, bm_h, e_pt, tgt, p.s_thick, p.s_thick * HEAD_RATIO)
    bm_s.to_mesh(m_ss); bm_h.to_mesh(m_sh); bm_s.free(); bm_h.free()
    o_ss.data.materials.clear(); o_ss.data.materials.append(get_physics_mat("S_S", p.s_color, p.s_alpha))
    o_sh.data.materials.clear(); o_sh.data.materials.append(get_physics_mat("S_H", p.s_color, p.s_head_alpha))

    # 3. Disks
    for d in [("Disk_Tr", "t_tr", r_c, c_pt, p.show_tr_disk, p.tr_disk_col, p.tr_disk_alpha, p.tr_disk_thick),
              ("Disk_Rw", "t_rw", t, e_pt, p.show_rw_disk, p.rw_disk_col, p.rw_disk_alpha, p.rw_disk_thick)]:
        o, m = get_obj(d[0], d[1]); o.hide_viewport = not d[4]
        bm = bmesh.new()
        bmesh.ops.create_cone(bm, cap_ends=True, segments=64, radius1=d[2], radius2=d[2], depth=max(0.002, d[7]))
        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=d[3])
        bm.to_mesh(m); bm.free()
        o.data.materials.clear(); o.data.materials.append(get_physics_mat(d[0], d[5], d[6]))

# ==============================================================================
#  PROPERTIES & OPERATORS
# ==============================================================================
_timer = None
def update_call(self, context):
    global _timer
    if _timer: 
        try: bpy.app.timers.unregister(_timer)
        except: pass
    _timer = bpy.app.timers.register(lambda: draw_physics_scene(bpy.context) and None, first_interval=0.05)

class YZ_Props(bpy.types.PropertyGroup):
    speed_v: bpy.props.FloatProperty(name="Velocity (v/c)", default=0.6, min=0.0, max=0.99, update=update_call)
    time_t: bpy.props.FloatProperty(name="Time (t)", default=10.0, min=0.1, update=update_call)
    # Planar
    p_show: bpy.props.BoolProperty(name="Show", default=True, update=update_call)
    p_color: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', default=(1, 0.9, 0), update=update_call)
    p_thick: bpy.props.FloatProperty(name="Thickness", default=0.05, min=0.002, update=update_call)
    p_alpha: bpy.props.FloatProperty(name="Shaft Alpha", default=1.0, min=0, max=1, update=update_call)
    p_head_alpha: bpy.props.FloatProperty(name="Head Alpha", default=1.0, min=0, max=1, update=update_call)
    # Conical
    s_show: bpy.props.BoolProperty(name="Show", default=True, update=update_call)
    s_color: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', default=(1, 0.3, 0), update=update_call)
    s_thick: bpy.props.FloatProperty(name="Thickness", default=0.04, min=0.002, update=update_call)
    s_alpha: bpy.props.FloatProperty(name="Shaft Alpha", default=0.8, min=0, max=1, update=update_call)
    s_head_alpha: bpy.props.FloatProperty(name="Head Alpha", default=0.8, min=0, max=1, update=update_call)
    # Train Disk
    show_tr_disk: bpy.props.BoolProperty(name="Show Train Disk", default=True, update=update_call)
    tr_disk_col: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', default=(0.1, 0.4, 0.8), update=update_call)
    tr_disk_alpha: bpy.props.FloatProperty(name="Alpha", default=0.2, min=0, max=1, update=update_call)
    tr_disk_thick: bpy.props.FloatProperty(name="Thickness", default=0.01, min=0.001, update=update_call)
    # Railway Disk
    show_rw_disk: bpy.props.BoolProperty(name="Show Railway Disk", default=True, update=update_call)
    rw_disk_col: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', default=(0.1, 0.8, 0.5), update=update_call)
    rw_disk_alpha: bpy.props.FloatProperty(name="Alpha", default=0.15, min=0, max=1, update=update_call)
    rw_disk_thick: bpy.props.FloatProperty(name="Thickness", default=0.01, min=0.001, update=update_call)

class YZ_OT_Generate(bpy.types.Operator):
    bl_idname = "yz.generate"; bl_label = "Generate Scene"
    def execute(self, context): draw_physics_scene(context); return {'FINISHED'}

class YZ_OT_Copy(bpy.types.Operator):
    bl_idname = "yz.copy_script"; bl_label = "Export to Text Block"
    def execute(self, context):
        p = context.scene.yz_rays_props
        M_S, M_E = "# <BEGIN_DICT>", "# <END_DICT>"
        src = next((t for t in bpy.data.texts if UNIQUE_ID in t.as_string()), None)
        if not src: 
            self.report({'WARNING'}, "Source script block not found.")
            return {'CANCELLED'}
        
        # Build fresh dictionary from current UI values
        lines = ["CURRENT_DEFAULTS = {"]
        for k in CURRENT_DEFAULTS.keys():
            val = getattr(p, k)
            if hasattr(val, "__len__"):
                val_str = f'({", ".join([f"{x:.4f}" for x in val])})'
            elif isinstance(val, bool):
                val_str = str(val)
            else:
                val_str = f"{val:.4f}"
            lines.append(f'    "{k}": {val_str},')
        lines.append("}")
        
        code = src.as_string()
        if M_S not in code or M_E not in code:
            self.report({'ERROR'}, "Markers missing in script.")
            return {'CANCELLED'}
            
        res = code.split(M_S)[0] + M_S + "\n" + "\n".join(lines) + "\n" + M_E + code.split(M_E)[1]
        out = bpy.data.texts.get("YZ_RAYS_EXPORT") or bpy.data.texts.new("YZ_RAYS_EXPORT")
        out.clear(); out.write(res)
        self.report({'INFO'}, "Latest values exported to 'YZ_RAYS_EXPORT'")
        return {'FINISHED'}

class YZ_OT_Reset(bpy.types.Operator):
    bl_idname = "yz.reset"; bl_label = "Reset Defaults"
    def execute(self, context):
        p = context.scene.yz_rays_props
        for k, v in CURRENT_DEFAULTS.items(): setattr(p, k, v)
        return {'FINISHED'}

class YZ_OT_Url(bpy.types.Operator):
    bl_idname = "wm.url_op"; bl_label = "Open URL"
    url: bpy.props.StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

class YZ_OT_Remove(bpy.types.Operator):
    bl_idname = "wm.yz_remove"; bl_label = "Remove Addon"
    def execute(self, context): unregister(); return {'FINISHED'}

# ==============================================================================
#  UI PANELS
# ==============================================================================
class YZ_PT_Main(bpy.types.Panel):
    bl_label = "YZ Rays - Global Control"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
    def draw(self, context):
        layout = self.layout; p = context.scene.yz_rays_props
        col = layout.column(align=True)
        col.scale_y = 1.3; col.operator("yz.generate", icon='PLAY')
        row = layout.row(align=True); row.operator("yz.reset", icon='LOOP_BACK'); row.operator("yz.copy_script", icon='TEXT')
        layout.separator(); layout.prop(p, "speed_v"); layout.prop(p, "time_t")

class YZ_PT_Planar(bpy.types.Panel):
    bl_label = "Planar Rays (Yellow)"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
    def draw_header(self, context): self.layout.prop(context.scene.yz_rays_props, "p_show", text="")
    def draw(self, context):
        layout = self.layout; p = context.scene.yz_rays_props; layout.enabled = p.p_show
        box = layout.box(); box.prop(p, "p_thick"); box.prop(p, "p_color"); 
        row = box.row(); row.prop(p, "p_alpha", slider=True); row.prop(p, "p_head_alpha", slider=True)

class YZ_PT_Conical(bpy.types.Panel):
    bl_label = "Conical Rays (Red)"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
    def draw_header(self, context): self.layout.prop(context.scene.yz_rays_props, "s_show", text="")
    def draw(self, context):
        layout = self.layout; p = context.scene.yz_rays_props; layout.enabled = p.s_show
        box = layout.box(); box.prop(p, "s_thick"); box.prop(p, "s_color"); 
        row = box.row(); row.prop(p, "s_alpha", slider=True); row.prop(p, "s_head_alpha", slider=True)

class YZ_PT_Disks(bpy.types.Panel):
    bl_label = "Disks (Train & Railway)"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout = self.layout; p = context.scene.yz_rays_props
        b1 = layout.box(); b1.prop(p, "show_tr_disk", text="Train Disk (x=0)")
        if p.show_tr_disk:
            b1.prop(p, "tr_disk_col"); b1.prop(p, "tr_disk_thick"); b1.prop(p, "tr_disk_alpha", slider=True)
        b2 = layout.box(); b2.prop(p, "show_rw_disk", text="Railway Disk (x=-vt)")
        if p.show_rw_disk:
            b2.prop(p, "rw_disk_col"); b2.prop(p, "rw_disk_thick"); b2.prop(p, "rw_disk_alpha", slider=True)

class YZ_PT_System(bpy.types.Panel):
    bl_label = "System"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout = self.layout
        layout.operator("wm.url_op", text="Lorentz Theory Community", icon='COMMUNITY').url = "<https://reddit.com/r/LightRayStandard>"
        layout.operator("wm.yz_remove", icon='CANCEL', text="Uninstall Addon")

# ==============================================================================
#  REGISTRATION
# ==============================================================================
classes = (YZ_Props, YZ_OT_Generate, YZ_OT_Copy, YZ_OT_Reset, YZ_OT_Url, YZ_OT_Remove, 
           YZ_PT_Main, YZ_PT_Planar, YZ_PT_Conical, YZ_PT_Disks, YZ_PT_System)

def register():
    for c in classes: bpy.utils.register_class(c)
    bpy.types.Scene.yz_rays_props = bpy.props.PointerProperty(type=YZ_Props)
def unregister():
    for c in reversed(classes): bpy.utils.unregister_class(c)
    if hasattr(bpy.types.Scene, "yz_rays_props"): del bpy.types.Scene.yz_rays_props

if __name__ == "__main__": register()
# 2026-02-22 Session Template
# Blender 5.0+ | V46 - Shaft & Head linked by fixed ratio (single Thickness slider)

UNIQUE_ID = "YZ_RAYS_V46_STABLE"

bl_info = {
    "name": "YZ Plane 12 Rays Generator (V46)",
    "author": "zionadchat Gemini + Claude",
    "version": (1, 46),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > YZ_Rays",
    "description": "Shaft & arrowhead scale together. 1 property per line UI.",
    "category": "Object",
}

import bpy
import bmesh
import math
import webbrowser
from mathutils import Vector, Matrix

# ==============================================================================
#  FIXED RATIO - head_radius = shaft_radius * HEAD_RATIO
# ==============================================================================
HEAD_RATIO = 2.5

# ==============================================================================
#  DICTIONARY DATA
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "speed_v": 0.6000,
    "time_t": 10.0000,
    "p_show": True,
    "p_color": (1.0000, 0.9000, 0.0000),
    "p_thick": 0.0500,
    "p_alpha": 1.0000,
    "p_head_color": (1.0000, 0.9000, 0.0000),
    "p_head_alpha": 1.0000,
    "s_show": True,
    "s_color": (1.0000, 0.3000, 0.0000),
    "s_thick": 0.0400,
    "s_alpha": 0.8000,
    "s_head_color": (1.0000, 0.3000, 0.0000),
    "s_head_alpha": 0.8000,
    "show_tr_disk": True,
    "tr_disk_col": (0.1000, 0.4000, 0.8000),
    "tr_disk_alpha": 0.2000,
    "tr_disk_thick": 0.0100,
    "show_rw_disk": True,
    "rw_disk_col": (0.1000, 0.8000, 0.5000),
    "rw_disk_alpha": 0.1500,
    "rw_disk_thick": 0.0100,
}
# <END_DICT>

TAB_NAME = "YZ_Rays"
COL_NAME = "YZ_Rays_Output"

# ==============================================================================
#  MATERIAL ENGINE
# ==============================================================================
def get_physics_mat(name, color, alpha):
    m_name = f"Mat_{name}_V46"
    mat = bpy.data.materials.get(m_name) or bpy.data.materials.new(name=m_name)
    mat.use_nodes = True
    if hasattr(mat, "blend_method"):
        mat.blend_method = 'BLEND'
    nodes = mat.node_tree.nodes
    nodes.clear()
    out  = nodes.new('ShaderNodeOutputMaterial')
    bsdf = nodes.new('ShaderNodeBsdfPrincipled')
    bsdf.inputs["Base Color"].default_value = (*color, 1.0)
    bsdf.inputs["Alpha"].default_value = alpha
    bsdf.inputs["Emission Color"].default_value = (*color, 1.0)
    bsdf.inputs["Emission Strength"].default_value = 1.5
    mat.node_tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
    return mat

# ==============================================================================
#  GEOMETRY
# ==============================================================================
def build_arrow_dual(bm_shaft, bm_head, start, end, shaft_thick, head_thick):
    direction = end - start
    dist = direction.length
    if dist < 0.01: return
    s_h = dist * 0.82
    h_h = dist * 0.18
    rot = Vector((0, 0, 1)).rotation_difference(direction.normalized()).to_matrix().to_4x4()

    res_s = bmesh.ops.create_cone(bm_shaft, cap_ends=True, segments=10, radius1=shaft_thick, radius2=shaft_thick, depth=s_h)
    bmesh.ops.translate(bm_shaft, verts=res_s['verts'], vec=Vector((0, 0, s_h / 2)))
    bmesh.ops.rotate(bm_shaft, verts=res_s['verts'], cent=(0, 0, 0), matrix=rot)
    bmesh.ops.translate(bm_shaft, verts=res_s['verts'], vec=start)

    res_h = bmesh.ops.create_cone(bm_head, cap_ends=True, segments=10, radius1=head_thick, radius2=0.0, depth=h_h)
    bmesh.ops.translate(bm_head, verts=res_h['verts'], vec=Vector((0, 0, s_h + h_h / 2)))
    bmesh.ops.rotate(bm_head, verts=res_h['verts'], cent=(0, 0, 0), matrix=rot)
    bmesh.ops.translate(bm_head, verts=res_h['verts'], vec=start)

def draw_physics_scene(context):
    p = context.scene.yz_rays_props
    col = bpy.data.collections.get(COL_NAME) or bpy.data.collections.new(COL_NAME)
    if col.name not in context.scene.collection.children: context.scene.collection.children.link(col)

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

    t, v = p.time_t, p.speed_v
    r_c = math.sqrt(max(0.0, t ** 2 - (v * t) ** 2))
    e_pt, c_pt = Vector((-v * t, 0, 0)), Vector((0, 0, 0))

    # 1. Planar
    o_ps, m_ps = get_obj("R_Planar_Shaft", "t_ps"); o_ph, m_ph = get_obj("R_Planar_Head", "t_ph")
    o_ps.hide_viewport = o_ph.hide_viewport = not p.p_show
    bm_s, bm_h = bmesh.new(), bmesh.new()
    for i in range(12):
        ang = math.radians(i * 30)
        tgt = Vector((0, r_c * math.cos(ang), r_c * math.sin(ang)))
        build_arrow_dual(bm_s, bm_h, c_pt, tgt, p.p_thick, p.p_thick * HEAD_RATIO)
    bm_s.to_mesh(m_ps); bm_h.to_mesh(m_ph); bm_s.free(); bm_h.free()
    o_ps.data.materials.clear(); o_ps.data.materials.append(get_physics_mat("P_S", p.p_color, p.p_alpha))
    o_ph.data.materials.clear(); o_ph.data.materials.append(get_physics_mat("P_H", p.p_head_color, p.p_head_alpha))

    # 2. Conical
    o_ss, m_ss = get_obj("R_Conical_Shaft", "t_ss"); o_sh, m_sh = get_obj("R_Conical_Head", "t_sh")
    o_ss.hide_viewport = o_sh.hide_viewport = not p.s_show
    bm_s, bm_h = bmesh.new(), bmesh.new()
    for i in range(12):
        ang = math.radians(i * 30)
        tgt = Vector((0, r_c * math.cos(ang), r_c * math.sin(ang)))
        build_arrow_dual(bm_s, bm_h, e_pt, tgt, p.s_thick, p.s_thick * HEAD_RATIO)
    bm_s.to_mesh(m_ss); bm_h.to_mesh(m_sh); bm_s.free(); bm_h.free()
    o_ss.data.materials.clear(); o_ss.data.materials.append(get_physics_mat("S_S", p.s_color, p.s_alpha))
    o_sh.data.materials.clear(); o_sh.data.materials.append(get_physics_mat("S_H", p.s_head_color, p.s_head_alpha))

    # 3. Disks
    for d in [("Disk_Tr", "t_tr", r_c, Vector((0,0,0)), p.show_tr_disk, p.tr_disk_col, p.tr_disk_alpha, p.tr_disk_thick),
              ("Disk_Rw", "t_rw", t, e_pt, p.show_rw_disk, p.rw_disk_col, p.rw_disk_alpha, p.rw_disk_thick)]:
        o, m = get_obj(d[0], d[1]); o.hide_viewport = not d[4]
        bm = bmesh.new()
        bmesh.ops.create_cone(bm, cap_ends=True, segments=64, radius1=d[2], radius2=d[2], depth=max(0.002, d[7]))
        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=d[3])
        bm.to_mesh(m); bm.free()
        o.data.materials.clear(); o.data.materials.append(get_physics_mat(d[0], d[5], d[6]))

# ==============================================================================
#  PROPERTIES & OPS
# ==============================================================================
_timer = None
def update_call(self, context):
    global _timer
    if _timer: 
        try: bpy.app.timers.unregister(_timer)
        except: pass
    _timer = bpy.app.timers.register(lambda: draw_physics_scene(bpy.context) and None, first_interval=0.05)

class YZ_Props(bpy.types.PropertyGroup):
    speed_v: bpy.props.FloatProperty(name="Velocity (v/c)", default=0.6, min=0.0, max=0.99, update=update_call)
    time_t: bpy.props.FloatProperty(name="Time (t)", default=10.0, min=0.1, update=update_call)
    p_show: bpy.props.BoolProperty(name="Show Planar", default=True, update=update_call)
    p_color: bpy.props.FloatVectorProperty(name="P Color", subtype='COLOR', default=(1, 0.9, 0), update=update_call)
    p_thick: bpy.props.FloatProperty(name="P Thick", default=0.05, min=0.002, update=update_call)
    p_alpha: bpy.props.FloatProperty(name="P Alpha", default=1.0, min=0, max=1, update=update_call)
    p_head_color: bpy.props.FloatVectorProperty(name="P Head Color", subtype='COLOR', default=(1, 0.9, 0), update=update_call)
    p_head_alpha: bpy.props.FloatProperty(name="P Head Alpha", default=1.0, min=0, max=1, update=update_call)
    s_show: bpy.props.BoolProperty(name="Show Conical", default=True, update=update_call)
    s_color: bpy.props.FloatVectorProperty(name="S Color", subtype='COLOR', default=(1, 0.3, 0), update=update_call)
    s_thick: bpy.props.FloatProperty(name="S Thick", default=0.04, min=0.002, update=update_call)
    s_alpha: bpy.props.FloatProperty(name="S Alpha", default=0.8, min=0, max=1, update=update_call)
    s_head_color: bpy.props.FloatVectorProperty(name="S Head Color", subtype='COLOR', default=(1, 0.3, 0), update=update_call)
    s_head_alpha: bpy.props.FloatProperty(name="S Head Alpha", default=0.8, min=0, max=1, update=update_call)
    show_tr_disk: bpy.props.BoolProperty(name="Show Train Disk", default=True, update=update_call)
    tr_disk_col: bpy.props.FloatVectorProperty(name="Tr Disk Color", subtype='COLOR', default=(0.1, 0.4, 0.8), update=update_call)
    tr_disk_alpha: bpy.props.FloatProperty(name="Tr Alpha", default=0.2, update=update_call)
    tr_disk_thick: bpy.props.FloatProperty(name="Tr Thick", default=0.01, update=update_call)
    show_rw_disk: bpy.props.BoolProperty(name="Show Railway Disk", default=True, update=update_call)
    rw_disk_col: bpy.props.FloatVectorProperty(name="Rw Disk Color", subtype='COLOR', default=(0.1, 0.8, 0.5), update=update_call)
    rw_disk_alpha: bpy.props.FloatProperty(name="Rw Alpha", default=0.15, update=update_call)
    rw_disk_thick: bpy.props.FloatProperty(name="Rw Thick", default=0.01, update=update_call)

class YZ_OT_Copy(bpy.types.Operator):
    bl_idname = "yz.copy_script"; bl_label = "Export Script"
    def execute(self, context):
        p = context.scene.yz_rays_props
        M_S, M_E = "# <BEGIN_DICT>", "# <END_DICT>"
        src = next((t for t in bpy.data.texts if UNIQUE_ID in t.as_string()), None)
        if not src: return {'CANCELLED'}
        
        lines = ["CURRENT_DEFAULTS = {"]
        for k in CURRENT_DEFAULTS.keys():
            v = getattr(p, k)
            if hasattr(v, "__len__"): lines.append(f'    "{k}": ({", ".join([f"{x:.4f}" for x in v])}),')
            elif isinstance(v, bool): lines.append(f'    "{k}": {v},')
            else: lines.append(f'    "{k}": {v:.4f},')
        lines.append("}")
        
        code = src.as_string()
        result = code.split(M_S)[0] + M_S + "\n" + "\n".join(lines) + "\n" + M_E + code.split(M_E)[1]
        out = bpy.data.texts.get("YZ_RAYS_EXPORT") or bpy.data.texts.new("YZ_RAYS_EXPORT")
        out.clear(); out.write(result)
        self.report({'INFO'}, "Exported to 'YZ_RAYS_EXPORT' text block.")
        return {'FINISHED'}

class YZ_OT_Reset(bpy.types.Operator):
    bl_idname = "yz.reset"; bl_label = "Reset"
    def execute(self, context):
        p = context.scene.yz_rays_props
        for k, v in CURRENT_DEFAULTS.items(): setattr(p, k, v)
        return {'FINISHED'}

# ==============================================================================
#  PANEL
# ==============================================================================
class YZ_PT_Main(bpy.types.Panel):
    bl_label = "YZ Rays V46"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
    def draw(self, context):
        layout = self.layout; p = context.scene.yz_rays_props
        row = layout.row(align=True)
        row.operator("yz.reset", icon='LOOP_BACK'); row.operator("yz.copy_script", icon='TEXT')
        layout.prop(p, "speed_v"); layout.prop(p, "time_t")
        for label, show_prop, thick, col, alpha, h_col, h_alpha in [
            ("Planar (Yellow)", "p_show", "p_thick", "p_color", "p_alpha", "p_head_color", "p_head_alpha"),
            ("Conical (Red)", "s_show", "s_thick", "s_color", "s_alpha", "s_head_color", "s_head_alpha")]:
            box = layout.box(); box.prop(p, show_prop, text=label)
            if getattr(p, show_prop):
                box.prop(p, thick); box.prop(p, col); box.prop(p, alpha, slider=True)
                box.prop(p, h_col); box.prop(p, h_alpha, slider=True)

classes = (YZ_Props, YZ_OT_Copy, YZ_OT_Reset, YZ_PT_Main)
def register():
    for c in classes: bpy.utils.register_class(c)
    bpy.types.Scene.yz_rays_props = bpy.props.PointerProperty(type=YZ_Props)
def unregister():
    for c in reversed(classes): bpy.utils.unregister_class(c)
    del bpy.types.Scene.yz_rays_props

if __name__ == "__main__": register()

blender Million 2026




# 2026-02-22 Session Template copy error
# Blender 5.0+ | V46 - Shaft & Head linked by fixed ratio (single Thickness slider)

UNIQUE_ID = "YZ_RAYS_V46_STABLE"

bl_info = {
    "name": "YZ Plane 12 Rays Generator (V46)",
    "author": "zionadchat Gemini + Claude",
    "version": (1, 46),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > YZ_Rays",
    "description": "Shaft & arrowhead scale together. 1 property per line UI.",
    "category": "Object",
}

import bpy
import bmesh
import math
import webbrowser
from mathutils import Vector, Matrix

# ==============================================================================
#  FIXED RATIO  —  head_radius = shaft_radius * HEAD_RATIO
# ==============================================================================
HEAD_RATIO = 2.5   # 矢先底面半径 ÷ シャフト半径

# ==============================================================================
#  DICTIONARY DATA
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "speed_v": 0.6000,
    "time_t": 10.0000,
    # Planar Rays
    "p_show": True,
    "p_color": (1.0000, 0.9000, 0.0000),
    "p_thick": 0.0500,
    "p_alpha": 1.0000,
    "p_head_color": (1.0000, 0.9000, 0.0000),
    "p_head_alpha": 1.0000,
    # Conical Rays
    "s_show": True,
    "s_color": (1.0000, 0.3000, 0.0000),
    "s_thick": 0.0400,
    "s_alpha": 0.8000,
    "s_head_color": (1.0000, 0.3000, 0.0000),
    "s_head_alpha": 0.8000,
    # Train Disk
    "show_tr_disk": True,
    "tr_disk_col": (0.1000, 0.4000, 0.8000),
    "tr_disk_alpha": 0.2000,
    "tr_disk_thick": 0.0100,
    # Railway Disk
    "show_rw_disk": True,
    "rw_disk_col": (0.1000, 0.8000, 0.5000),
    "rw_disk_alpha": 0.1500,
    "rw_disk_thick": 0.0100,
}
# <END_DICT>

TAB_NAME = "YZ_Rays"
COL_NAME = "YZ_Rays_Output"

# ==============================================================================
#  MATERIAL ENGINE
# ==============================================================================
def get_physics_mat(name, color, alpha):
    m_name = f"Mat_{name}_V46"
    mat = bpy.data.materials.get(m_name) or bpy.data.materials.new(name=m_name)
    mat.use_nodes = True
    if hasattr(mat, "blend_method"):
        mat.blend_method = 'BLEND'
    nodes = mat.node_tree.nodes
    nodes.clear()
    out  = nodes.new('ShaderNodeOutputMaterial')
    bsdf = nodes.new('ShaderNodeBsdfPrincipled')
    bsdf.inputs["Base Color"].default_value      = (*color, 1.0)
    bsdf.inputs["Alpha"].default_value           = alpha
    bsdf.inputs["Emission Color"].default_value  = (*color, 1.0)
    bsdf.inputs["Emission Strength"].default_value = 1.5
    mat.node_tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
    return mat

# ==============================================================================
#  GEOMETRY
#  head_thick は呼び出し側で  shaft_thick * HEAD_RATIO  として渡す
# ==============================================================================
def build_arrow_dual(bm_shaft, bm_head, start, end, shaft_thick, head_thick):
    """
    shaft_thick : シャフト(棒)の半径
    head_thick  : 矢先コーンの底面半径  = shaft_thick * HEAD_RATIO
    """
    direction = end - start
    dist = direction.length
    if dist < 0.01:
        return
    s_h = dist * 0.82   # シャフト長
    h_h = dist * 0.18   # 矢先長
    rot = Vector((0, 0, 1)).rotation_difference(direction.normalized()).to_matrix().to_4x4()

    # ── Shaft ──────────────────────────────────
    res_s = bmesh.ops.create_cone(
        bm_shaft, cap_ends=True, segments=10,
        radius1=shaft_thick, radius2=shaft_thick, depth=s_h)
    bmesh.ops.translate(bm_shaft, verts=res_s['verts'], vec=Vector((0, 0, s_h / 2)))
    bmesh.ops.rotate(bm_shaft,   verts=res_s['verts'], cent=(0, 0, 0), matrix=rot)
    bmesh.ops.translate(bm_shaft, verts=res_s['verts'], vec=start)

    # ── Arrow Head ─────────────────────────────
    res_h = bmesh.ops.create_cone(
        bm_head, cap_ends=True, segments=10,
        radius1=head_thick, radius2=0.0, depth=h_h)
    bmesh.ops.translate(bm_head, verts=res_h['verts'], vec=Vector((0, 0, s_h + h_h / 2)))
    bmesh.ops.rotate(bm_head,   verts=res_h['verts'], cent=(0, 0, 0), matrix=rot)
    bmesh.ops.translate(bm_head, verts=res_h['verts'], vec=start)

def draw_physics_scene(context):
    p = context.scene.yz_rays_props

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

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

    t = p.time_t
    v = p.speed_v
    r_contracted = math.sqrt(max(0.0, t ** 2 - (v * t) ** 2))
    r_static     = t
    emission_pt  = Vector((-v * t, 0, 0))
    current_pt   = Vector((0, 0, 0))

    # ── 1. Planar Rays ────────────────────────────────────────────────
    p_head_r = p.p_thick * HEAD_RATIO          # 矢先半径を自動計算

    o_ps, m_ps = get_obj("Rays_Planar_Shaft", "tag_p_shaft")
    o_ph, m_ph = get_obj("Rays_Planar_Head",  "tag_p_head")
    o_ps.hide_viewport = not p.p_show
    o_ph.hide_viewport = not p.p_show
    bm_s = bmesh.new(); bm_h = bmesh.new()
    for i in range(12):
        ang = math.radians(i * 30)
        tgt = Vector((0, r_contracted * math.cos(ang), r_contracted * math.sin(ang)))
        build_arrow_dual(bm_s, bm_h, current_pt, tgt, p.p_thick, p_head_r)
    bm_s.to_mesh(m_ps); bm_s.free()
    bm_h.to_mesh(m_ph); bm_h.free()
    o_ps.data.materials.clear()
    o_ps.data.materials.append(get_physics_mat("P_Shaft", p.p_color,      p.p_alpha))
    o_ph.data.materials.clear()
    o_ph.data.materials.append(get_physics_mat("P_Head",  p.p_head_color, p.p_head_alpha))

    # ── 2. Conical Rays ───────────────────────────────────────────────
    s_head_r = p.s_thick * HEAD_RATIO          # 矢先半径を自動計算

    o_ss, m_ss = get_obj("Rays_Conical_Shaft", "tag_s_shaft")
    o_sh, m_sh = get_obj("Rays_Conical_Head",  "tag_s_head")
    o_ss.hide_viewport = not p.s_show
    o_sh.hide_viewport = not p.s_show
    bm_s = bmesh.new(); bm_h = bmesh.new()
    for i in range(12):
        ang = math.radians(i * 30)
        tgt = Vector((0, r_contracted * math.cos(ang), r_contracted * math.sin(ang)))
        build_arrow_dual(bm_s, bm_h, emission_pt, tgt, p.s_thick, s_head_r)
    bm_s.to_mesh(m_ss); bm_s.free()
    bm_h.to_mesh(m_sh); bm_h.free()
    o_ss.data.materials.clear()
    o_ss.data.materials.append(get_physics_mat("S_Shaft", p.s_color,      p.s_alpha))
    o_sh.data.materials.clear()
    o_sh.data.materials.append(get_physics_mat("S_Head",  p.s_head_color, p.s_head_alpha))

    # ── 3. Train Disk ─────────────────────────────────────────────────
    o_tr, m_tr = get_obj("Disk_Train_x0", "tag_tr")
    o_tr.hide_viewport = not p.show_tr_disk
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=True, segments=64,
                          radius1=r_contracted, radius2=r_contracted,
                          depth=max(0.002, p.tr_disk_thick))
    bmesh.ops.rotate(bm, verts=bm.verts, cent=(0, 0, 0),
                     matrix=Matrix.Rotation(math.radians(90), 4, 'Y'))
    bm.to_mesh(m_tr); bm.free()
    o_tr.data.materials.clear()
    o_tr.data.materials.append(get_physics_mat("DiskTr", p.tr_disk_col, p.tr_disk_alpha))

    # ── 4. Railway Disk ───────────────────────────────────────────────
    o_rw, m_rw = get_obj("Disk_Railway_xVT", "tag_rw")
    o_rw.hide_viewport = not p.show_rw_disk
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=True, segments=64,
                          radius1=r_static, radius2=r_static,
                          depth=max(0.002, p.rw_disk_thick))
    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=emission_pt)
    bm.to_mesh(m_rw); bm.free()
    o_rw.data.materials.clear()
    o_rw.data.materials.append(get_physics_mat("DiskRw", p.rw_disk_col, p.rw_disk_alpha))

# ==============================================================================
#  DEBOUNCE
# ==============================================================================
_timer = None

def update_call(self, context):
    global _timer
    if _timer:
        try: bpy.app.timers.unregister(_timer)
        except Exception: pass
    _timer = bpy.app.timers.register(
        lambda: draw_physics_scene(bpy.context) and None,
        first_interval=0.05)

# ==============================================================================
#  PROPERTIES
# ==============================================================================
class YZ_Props(bpy.types.PropertyGroup):

    # Global
    speed_v: bpy.props.FloatProperty(
        name="Velocity (v/c)", default=0.6, min=0.0, max=0.99, update=update_call)
    time_t: bpy.props.FloatProperty(
        name="Time (t)", default=10.0, min=0.1, update=update_call)

    # Planar Rays
    p_show: bpy.props.BoolProperty(name="Show", default=True, update=update_call)
    p_color: bpy.props.FloatVectorProperty(
        name="Shaft Color", subtype='COLOR',
        default=(1.0, 0.9, 0.0), min=0.0, max=1.0, update=update_call)
    p_thick: bpy.props.FloatProperty(
        name="Thickness",
        description=f"Shaft radius. Head radius = Thickness × {HEAD_RATIO}",
        default=0.05, min=0.002, max=1.0, update=update_call)
    p_alpha: bpy.props.FloatProperty(
        name="Shaft Alpha", min=0.0, max=1.0, default=1.0, update=update_call)
    p_head_color: bpy.props.FloatVectorProperty(
        name="Head Color", subtype='COLOR',
        default=(1.0, 0.9, 0.0), min=0.0, max=1.0, update=update_call)
    p_head_alpha: bpy.props.FloatProperty(
        name="Head Alpha", min=0.0, max=1.0, default=1.0, update=update_call)

    # Conical Rays
    s_show: bpy.props.BoolProperty(name="Show", default=True, update=update_call)
    s_color: bpy.props.FloatVectorProperty(
        name="Shaft Color", subtype='COLOR',
        default=(1.0, 0.3, 0.0), min=0.0, max=1.0, update=update_call)
    s_thick: bpy.props.FloatProperty(
        name="Thickness",
        description=f"Shaft radius. Head radius = Thickness × {HEAD_RATIO}",
        default=0.04, min=0.002, max=1.0, update=update_call)
    s_alpha: bpy.props.FloatProperty(
        name="Shaft Alpha", min=0.0, max=1.0, default=0.8, update=update_call)
    s_head_color: bpy.props.FloatVectorProperty(
        name="Head Color", subtype='COLOR',
        default=(1.0, 0.3, 0.0), min=0.0, max=1.0, update=update_call)
    s_head_alpha: bpy.props.FloatProperty(
        name="Head Alpha", min=0.0, max=1.0, default=0.8, update=update_call)

    # Train Disk
    show_tr_disk: bpy.props.BoolProperty(name="Show", default=True, update=update_call)
    tr_disk_col: bpy.props.FloatVectorProperty(
        name="Color", subtype='COLOR',
        default=(0.1, 0.4, 0.8), min=0.0, max=1.0, update=update_call)
    tr_disk_alpha: bpy.props.FloatProperty(
        name="Alpha", min=0.0, max=1.0, default=0.2, update=update_call)
    tr_disk_thick: bpy.props.FloatProperty(
        name="Thickness", default=0.01, min=0.001, max=2.0, update=update_call)

    # Railway Disk
    show_rw_disk: bpy.props.BoolProperty(name="Show", default=True, update=update_call)
    rw_disk_col: bpy.props.FloatVectorProperty(
        name="Color", subtype='COLOR',
        default=(0.1, 0.8, 0.5), min=0.0, max=1.0, update=update_call)
    rw_disk_alpha: bpy.props.FloatProperty(
        name="Alpha", min=0.0, max=1.0, default=0.15, update=update_call)
    rw_disk_thick: bpy.props.FloatProperty(
        name="Thickness", default=0.01, min=0.001, max=2.0, update=update_call)

# ==============================================================================
#  OPERATORS
# ==============================================================================
class YZ_OT_Generate(bpy.types.Operator):
    bl_idname = "yz.generate"; bl_label = "Generate Scene"
    def execute(self, context):
        draw_physics_scene(context); return {'FINISHED'}

class YZ_OT_Reset(bpy.types.Operator):
    bl_idname = "yz.reset_defaults"; bl_label = "Reset Defaults"
    def execute(self, context):
        p = context.scene.yz_rays_props
        for k, val in CURRENT_DEFAULTS.items():
            if hasattr(p, k): setattr(p, k, val)
        draw_physics_scene(context); return {'FINISHED'}

class YZ_OT_Copy(bpy.types.Operator):
    bl_idname = "yz.copy_script"; bl_label = "Copy Script"
    def execute(self, context):
        p = context.scene.yz_rays_props
        M_S, M_E = "# <BEGIN_DICT>", "# <END_DICT>"
        txt = next((t for t in bpy.data.texts if UNIQUE_ID in t.as_string()), None)
        if not txt:
            self.report({'WARNING'}, "Script text block not found."); return {'CANCELLED'}
        d_str = "CURRENT_DEFAULTS = {\n"
        for k in CURRENT_DEFAULTS.keys():
            val = getattr(p, k)
            if hasattr(val, "__len__"):
                d_str += f'    "{k}": ({", ".join([f"{x:.4f}" for x in val])}),\n'
            elif isinstance(val, bool):
                d_str += f'    "{k}": {val},\n'
            else:
                d_str += f'    "{k}": {val:.4f},\n'
        d_str += "}\n"
        code = txt.as_string()
        result = code.split(M_S)[0] + M_S + "\n" + d_str + M_E + code.split(M_E)[1]
        context.window_manager.clipboard = result
        self.report({'INFO'}, "Script copied!"); return {'FINISHED'}

class YZ_OT_Url(bpy.types.Operator):
    bl_idname = "wm.url_op"; bl_label = "Open URL"
    url: bpy.props.StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

class YZ_OT_Remove(bpy.types.Operator):
    bl_idname = "wm.yz_remove"; bl_label = "Remove Addon"
    def execute(self, context): unregister(); return {'FINISHED'}

# ==============================================================================
#  PANELS  — 1 property per line
# ==============================================================================

class YZ_PT_Global(bpy.types.Panel):
    bl_label = "Global Physics"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'
    bl_category = TAB_NAME; bl_order = 0

    def draw(self, context):
        layout = self.layout
        p = context.scene.yz_rays_props

        row = layout.row(align=True)
        row.scale_y = 1.25
        row.operator("yz.generate",       icon='PLAY',      text="Generate")
        row.operator("yz.reset_defaults", icon='LOOP_BACK', text="Reset")
        row.operator("yz.copy_script",    icon='COPY_ID',   text="Copy")

        layout.separator()
        layout.prop(p, "speed_v")
        layout.prop(p, "time_t")

# ── Planar Rays ──────────────────────────────────────────────────────────────
class YZ_PT_Planar(bpy.types.Panel):
    bl_label = "Planar Rays (Yellow)"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'
    bl_category = TAB_NAME; bl_order = 1
    bl_options = {'DEFAULT_CLOSED'}

    def draw_header(self, context):
        self.layout.prop(context.scene.yz_rays_props, "p_show", text="")

    def draw(self, context):
        layout = self.layout
        p = context.scene.yz_rays_props
        layout.enabled = p.p_show

        # ── Shaft ──────────────────────────────
        box = layout.box()
        box.label(text="Shaft  /  棒", icon='CURVE_PATH')
        box.prop(p, "p_color")
        box.prop(p, "p_thick")          # ← 1つのスライダーで棒+矢先が連動
        box.prop(p, "p_alpha", slider=True)

        # ── Arrow Head ─────────────────────────
        box = layout.box()
        box.label(text="Arrow Head  /  矢先  ( × 2.5 自動 )", icon='CONE_DATA')
        box.prop(p, "p_head_color")
        box.prop(p, "p_head_alpha", slider=True)

        # 現在の矢先サイズを読み取り専用で表示
        info = box.row()
        info.enabled = False
        info.label(text=f"Head radius = {p.p_thick * HEAD_RATIO:.4f}")

# ── Conical Rays ─────────────────────────────────────────────────────────────
class YZ_PT_Conical(bpy.types.Panel):
    bl_label = "Conical Rays (Red)"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'
    bl_category = TAB_NAME; bl_order = 2
    bl_options = {'DEFAULT_CLOSED'}

    def draw_header(self, context):
        self.layout.prop(context.scene.yz_rays_props, "s_show", text="")

    def draw(self, context):
        layout = self.layout
        p = context.scene.yz_rays_props
        layout.enabled = p.s_show

        box = layout.box()
        box.label(text="Shaft  /  棒", icon='CURVE_PATH')
        box.prop(p, "s_color")
        box.prop(p, "s_thick")          # ← 1つのスライダーで棒+矢先が連動
        box.prop(p, "s_alpha", slider=True)

        box = layout.box()
        box.label(text="Arrow Head  /  矢先  ( × 2.5 自動 )", icon='CONE_DATA')
        box.prop(p, "s_head_color")
        box.prop(p, "s_head_alpha", slider=True)

        info = box.row()
        info.enabled = False
        info.label(text=f"Head radius = {p.s_thick * HEAD_RATIO:.4f}")

# ── Train Disk ───────────────────────────────────────────────────────────────
class YZ_PT_TrainDisk(bpy.types.Panel):
    bl_label = "Train Disk  (x = 0)"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'
    bl_category = TAB_NAME; bl_order = 3
    bl_options = {'DEFAULT_CLOSED'}

    def draw_header(self, context):
        self.layout.prop(context.scene.yz_rays_props, "show_tr_disk", text="")

    def draw(self, context):
        layout = self.layout
        p = context.scene.yz_rays_props
        layout.enabled = p.show_tr_disk

        layout.prop(p, "tr_disk_col")
        layout.prop(p, "tr_disk_thick")
        layout.prop(p, "tr_disk_alpha", slider=True)

# ── Railway Disk ─────────────────────────────────────────────────────────────
class YZ_PT_RailwayDisk(bpy.types.Panel):
    bl_label = "Railway Disk  (x = -vt)"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'
    bl_category = TAB_NAME; bl_order = 4
    bl_options = {'DEFAULT_CLOSED'}

    def draw_header(self, context):
        self.layout.prop(context.scene.yz_rays_props, "show_rw_disk", text="")

    def draw(self, context):
        layout = self.layout
        p = context.scene.yz_rays_props
        layout.enabled = p.show_rw_disk

        layout.prop(p, "rw_disk_col")
        layout.prop(p, "rw_disk_thick")
        layout.prop(p, "rw_disk_alpha", slider=True)

# ── System ───────────────────────────────────────────────────────────────────
class YZ_PT_System(bpy.types.Panel):
    bl_label = "System"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'
    bl_category = TAB_NAME; bl_order = 5
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        layout.operator("wm.yz_remove", icon='CANCEL', text="Remove Addon")
        layout.operator("wm.url_op", text="Lorentz Theory", icon='WORLD').url = "<https://www.notion.so/>"

# ==============================================================================
#  REGISTRATION
# ==============================================================================
classes = (
    YZ_Props,
    YZ_OT_Generate,
    YZ_OT_Reset,
    YZ_OT_Copy,
    YZ_OT_Url,
    YZ_OT_Remove,
    YZ_PT_Global,
    YZ_PT_Planar,
    YZ_PT_Conical,
    YZ_PT_TrainDisk,
    YZ_PT_RailwayDisk,
    YZ_PT_System,
)

def register():
    for c in classes:
        bpy.utils.register_class(c)
    bpy.types.Scene.yz_rays_props = bpy.props.PointerProperty(type=YZ_Props)

def unregister():
    for c in reversed(classes):
        bpy.utils.unregister_class(c)
    if hasattr(bpy.types.Scene, "yz_rays_props"):
        del bpy.types.Scene.yz_rays_props

if __name__ == "__main__":
    register()
# 2026-02-22 Session Template
# Blender 5.0+ | V45 - One Property Per Line UI

UNIQUE_ID = "YZ_RAYS_V45_STABLE"

bl_info = {
    "name": "YZ Plane 12 Rays Generator (V45)",
    "author": "zionadchat Gemini + Claude",
    "version": (1, 45),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > YZ_Rays",
    "description": "1 property per line. Full color/thickness/alpha for all elements.",
    "category": "Object",
}

import bpy
import bmesh
import math
import webbrowser
from mathutils import Vector, Matrix

# ==============================================================================
#  DICTIONARY DATA
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "speed_v": 0.0000,
    "time_t": 10.0000,
    "p_show": True,
    "p_color": (1.0000, 0.9000, 0.0000),
    "p_thick": 0.0500,
    "p_alpha": 1.0000,
    "p_head_color": (1.0000, 0.9000, 0.0000),
    "p_head_thick": 0.1250,
    "p_head_alpha": 1.0000,
    "s_show": True,
    "s_color": (1.0000, 0.3000, 0.0000),
    "s_thick": 0.0320,
    "s_alpha": 0.8000,
    "s_head_color": (1.0000, 0.3000, 0.0000),
    "s_head_thick": 0.1000,
    "s_head_alpha": 0.8000,
    "show_tr_disk": True,
    "tr_disk_col": (0.1000, 0.4000, 0.8000),
    "tr_disk_alpha": 0.2000,
    "tr_disk_thick": 0.0100,
    "show_rw_disk": True,
    "rw_disk_col": (0.1000, 0.8000, 0.5000),
    "rw_disk_alpha": 0.0423,
    "rw_disk_thick": 0.0010,
}
# <END_DICT>

TAB_NAME = "YZ_Rays"
COL_NAME = "YZ_Rays_Output"

# ==============================================================================
#  MATERIAL ENGINE
# ==============================================================================
def get_physics_mat(name, color, alpha):
    m_name = f"Mat_{name}_V45"
    mat = bpy.data.materials.get(m_name) or bpy.data.materials.new(name=m_name)
    mat.use_nodes = True
    if hasattr(mat, "blend_method"):
        mat.blend_method = 'BLEND'
    nodes = mat.node_tree.nodes
    nodes.clear()
    out  = nodes.new('ShaderNodeOutputMaterial')
    bsdf = nodes.new('ShaderNodeBsdfPrincipled')
    bsdf.inputs["Base Color"].default_value     = (*color, 1.0)
    bsdf.inputs["Alpha"].default_value          = alpha
    bsdf.inputs["Emission Color"].default_value = (*color, 1.0)
    bsdf.inputs["Emission Strength"].default_value = 1.5
    mat.node_tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
    return mat

# ==============================================================================
#  GEOMETRY
# ==============================================================================
def build_arrow_dual(bm_shaft, bm_head, start, end, shaft_thick, head_thick):
    direction = end - start
    dist = direction.length
    if dist < 0.01:
        return
    s_h = dist * 0.82
    h_h = dist * 0.18
    rot = Vector((0, 0, 1)).rotation_difference(direction.normalized()).to_matrix().to_4x4()

    res_s = bmesh.ops.create_cone(
        bm_shaft, cap_ends=True, segments=10,
        radius1=shaft_thick, radius2=shaft_thick, depth=s_h)
    bmesh.ops.translate(bm_shaft, verts=res_s['verts'], vec=Vector((0, 0, s_h / 2)))
    bmesh.ops.rotate(bm_shaft, verts=res_s['verts'], cent=(0, 0, 0), matrix=rot)
    bmesh.ops.translate(bm_shaft, verts=res_s['verts'], vec=start)

    res_h = bmesh.ops.create_cone(
        bm_head, cap_ends=True, segments=10,
        radius1=head_thick, radius2=0.0, depth=h_h)
    bmesh.ops.translate(bm_head, verts=res_h['verts'], vec=Vector((0, 0, s_h + h_h / 2)))
    bmesh.ops.rotate(bm_head, verts=res_h['verts'], cent=(0, 0, 0), matrix=rot)
    bmesh.ops.translate(bm_head, verts=res_h['verts'], vec=start)

def draw_physics_scene(context):
    p = context.scene.yz_rays_props

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

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

    t = p.time_t
    v = p.speed_v
    r_contracted = math.sqrt(max(0.0, t ** 2 - (v * t) ** 2))
    r_static     = t
    emission_pt  = Vector((-v * t, 0, 0))
    current_pt   = Vector((0, 0, 0))

    # 1. Planar Rays
    o_ps, m_ps = get_obj("Rays_Planar_Shaft", "tag_p_shaft")
    o_ph, m_ph = get_obj("Rays_Planar_Head",  "tag_p_head")
    o_ps.hide_viewport = not p.p_show
    o_ph.hide_viewport = not p.p_show
    bm_s = bmesh.new(); bm_h = bmesh.new()
    for i in range(12):
        ang = math.radians(i * 30)
        tgt = Vector((0, r_contracted * math.cos(ang), r_contracted * math.sin(ang)))
        build_arrow_dual(bm_s, bm_h, current_pt, tgt, p.p_thick, p.p_head_thick)
    bm_s.to_mesh(m_ps); bm_s.free()
    bm_h.to_mesh(m_ph); bm_h.free()
    o_ps.data.materials.clear()
    o_ps.data.materials.append(get_physics_mat("P_Shaft", p.p_color,      p.p_alpha))
    o_ph.data.materials.clear()
    o_ph.data.materials.append(get_physics_mat("P_Head",  p.p_head_color, p.p_head_alpha))

    # 2. Conical Rays
    o_ss, m_ss = get_obj("Rays_Conical_Shaft", "tag_s_shaft")
    o_sh, m_sh = get_obj("Rays_Conical_Head",  "tag_s_head")
    o_ss.hide_viewport = not p.s_show
    o_sh.hide_viewport = not p.s_show
    bm_s = bmesh.new(); bm_h = bmesh.new()
    for i in range(12):
        ang = math.radians(i * 30)
        tgt = Vector((0, r_contracted * math.cos(ang), r_contracted * math.sin(ang)))
        build_arrow_dual(bm_s, bm_h, emission_pt, tgt, p.s_thick, p.s_head_thick)
    bm_s.to_mesh(m_ss); bm_s.free()
    bm_h.to_mesh(m_sh); bm_h.free()
    o_ss.data.materials.clear()
    o_ss.data.materials.append(get_physics_mat("S_Shaft", p.s_color,      p.s_alpha))
    o_sh.data.materials.clear()
    o_sh.data.materials.append(get_physics_mat("S_Head",  p.s_head_color, p.s_head_alpha))

    # 3. Train Disk
    o_tr, m_tr = get_obj("Disk_Train_x0", "tag_tr")
    o_tr.hide_viewport = not p.show_tr_disk
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=True, segments=64,
                          radius1=r_contracted, radius2=r_contracted,
                          depth=max(0.002, p.tr_disk_thick))
    bmesh.ops.rotate(bm, verts=bm.verts, cent=(0, 0, 0),
                     matrix=Matrix.Rotation(math.radians(90), 4, 'Y'))
    bm.to_mesh(m_tr); bm.free()
    o_tr.data.materials.clear()
    o_tr.data.materials.append(get_physics_mat("DiskTr", p.tr_disk_col, p.tr_disk_alpha))

    # 4. Railway Disk
    o_rw, m_rw = get_obj("Disk_Railway_xVT", "tag_rw")
    o_rw.hide_viewport = not p.show_rw_disk
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=True, segments=64,
                          radius1=r_static, radius2=r_static,
                          depth=max(0.002, p.rw_disk_thick))
    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=emission_pt)
    bm.to_mesh(m_rw); bm.free()
    o_rw.data.materials.clear()
    o_rw.data.materials.append(get_physics_mat("DiskRw", p.rw_disk_col, p.rw_disk_alpha))

# ==============================================================================
#  DEBOUNCE
# ==============================================================================
_timer = None

def update_call(self, context):
    global _timer
    if _timer:
        try: bpy.app.timers.unregister(_timer)
        except Exception: pass
    _timer = bpy.app.timers.register(
        lambda: draw_physics_scene(bpy.context) and None,
        first_interval=0.05)

# ==============================================================================
#  PROPERTIES
# ==============================================================================
class YZ_Props(bpy.types.PropertyGroup):

    speed_v: bpy.props.FloatProperty(
        name="Velocity (v/c)", default=0.6, min=0.0, max=0.99, update=update_call)
    time_t: bpy.props.FloatProperty(
        name="Time (t)", default=10.0, min=0.1, update=update_call)

    # Planar
    p_show: bpy.props.BoolProperty(
        name="Show", default=True, update=update_call)
    p_color: bpy.props.FloatVectorProperty(
        name="Shaft Color", subtype='COLOR',
        default=(1.0, 0.9, 0.0), min=0.0, max=1.0, update=update_call)
    p_thick: bpy.props.FloatProperty(
        name="Shaft Thickness", default=0.05, min=0.002, max=1.0, update=update_call)
    p_alpha: bpy.props.FloatProperty(
        name="Shaft Alpha", min=0.0, max=1.0, default=1.0, update=update_call)
    p_head_color: bpy.props.FloatVectorProperty(
        name="Head Color", subtype='COLOR',
        default=(1.0, 0.9, 0.0), min=0.0, max=1.0, update=update_call)
    p_head_thick: bpy.props.FloatProperty(
        name="Head Size", default=0.125, min=0.005, max=2.0, update=update_call)
    p_head_alpha: bpy.props.FloatProperty(
        name="Head Alpha", min=0.0, max=1.0, default=1.0, update=update_call)

    # Conical
    s_show: bpy.props.BoolProperty(
        name="Show", default=True, update=update_call)
    s_color: bpy.props.FloatVectorProperty(
        name="Shaft Color", subtype='COLOR',
        default=(1.0, 0.3, 0.0), min=0.0, max=1.0, update=update_call)
    s_thick: bpy.props.FloatProperty(
        name="Shaft Thickness", default=0.04, min=0.002, max=1.0, update=update_call)
    s_alpha: bpy.props.FloatProperty(
        name="Shaft Alpha", min=0.0, max=1.0, default=0.8, update=update_call)
    s_head_color: bpy.props.FloatVectorProperty(
        name="Head Color", subtype='COLOR',
        default=(1.0, 0.3, 0.0), min=0.0, max=1.0, update=update_call)
    s_head_thick: bpy.props.FloatProperty(
        name="Head Size", default=0.1, min=0.005, max=2.0, update=update_call)
    s_head_alpha: bpy.props.FloatProperty(
        name="Head Alpha", min=0.0, max=1.0, default=0.8, update=update_call)

    # Train Disk
    show_tr_disk: bpy.props.BoolProperty(
        name="Show", default=True, update=update_call)
    tr_disk_col: bpy.props.FloatVectorProperty(
        name="Color", subtype='COLOR',
        default=(0.1, 0.4, 0.8), min=0.0, max=1.0, update=update_call)
    tr_disk_alpha: bpy.props.FloatProperty(
        name="Alpha", min=0.0, max=1.0, default=0.2, update=update_call)
    tr_disk_thick: bpy.props.FloatProperty(
        name="Thickness", default=0.01, min=0.001, max=2.0, update=update_call)

    # Railway Disk
    show_rw_disk: bpy.props.BoolProperty(
        name="Show", default=True, update=update_call)
    rw_disk_col: bpy.props.FloatVectorProperty(
        name="Color", subtype='COLOR',
        default=(0.1, 0.8, 0.5), min=0.0, max=1.0, update=update_call)
    rw_disk_alpha: bpy.props.FloatProperty(
        name="Alpha", min=0.0, max=1.0, default=0.15, update=update_call)
    rw_disk_thick: bpy.props.FloatProperty(
        name="Thickness", default=0.01, min=0.001, max=2.0, update=update_call)

# ==============================================================================
#  OPERATORS
# ==============================================================================
class YZ_OT_Generate(bpy.types.Operator):
    bl_idname = "yz.generate"; bl_label = "Generate Scene"
    def execute(self, context):
        draw_physics_scene(context); return {'FINISHED'}

class YZ_OT_Reset(bpy.types.Operator):
    bl_idname = "yz.reset_defaults"; bl_label = "Reset Defaults"
    def execute(self, context):
        p = context.scene.yz_rays_props
        for k, val in CURRENT_DEFAULTS.items():
            if hasattr(p, k): setattr(p, k, val)
        draw_physics_scene(context); return {'FINISHED'}

class YZ_OT_Copy(bpy.types.Operator):
    bl_idname = "yz.copy_script"; bl_label = "Copy Script"
    def execute(self, context):
        p = context.scene.yz_rays_props
        M_S, M_E = "# <BEGIN_DICT>", "
平面矢印 (Planar Rays): 放射状の太さ、色、透明度
円錐矢印 (Conical Rays): 放射状の太さ、色、透明度
列車円盤 (Train Disk): 板の厚み(奥行き)、色、透明度
線路円盤 (Railway Disk): 板の厚み(奥行き)、色、透明度
共通: アドオン削除、コピー、理論リンクの全機能
# 2026-02-22 Session Template
# Blender 5.0+ | V44 - Full Individual Control for ALL Elements

UNIQUE_ID = "YZ_RAYS_V44_STABLE"

bl_info = {
    "name": "YZ Plane 12 Rays Generator (V44)",
    "author": "zionadchat Gemini + Claude",
    "version": (1, 44),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > YZ_Rays",
    "description": "Full color/thickness/alpha controls for ALL elements: 2 Disks, 2 Ray types, Arrow heads.",
    "category": "Object",
}

import bpy
import bmesh
import math
import webbrowser
from mathutils import Vector, Matrix
from datetime import datetime

# ==============================================================================
#  DICTIONARY DATA
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    # Global
    "speed_v": 0.6000,
    "time_t": 10.0000,
    # Planar Rays (Yellow)
    "p_show": True,
    "p_color": (1.0000, 0.9000, 0.0000),
    "p_thick": 0.0500,
    "p_alpha": 1.0000,
    "p_head_thick": 0.1250,
    "p_head_alpha": 1.0000,
    "p_head_color": (1.0000, 0.9000, 0.0000),
    # Conical Rays (Red)
    "s_show": True,
    "s_color": (1.0000, 0.3000, 0.0000),
    "s_thick": 0.0400,
    "s_alpha": 0.8000,
    "s_head_thick": 0.1000,
    "s_head_alpha": 0.8000,
    "s_head_color": (1.0000, 0.3000, 0.0000),
    # Train Disk
    "show_tr_disk": True,
    "tr_disk_col": (0.1000, 0.4000, 0.8000),
    "tr_disk_alpha": 0.2000,
    "tr_disk_thick": 0.0100,
    # Railway Disk
    "show_rw_disk": True,
    "rw_disk_col": (0.1000, 0.8000, 0.5000),
    "rw_disk_alpha": 0.1500,
    "rw_disk_thick": 0.0100,
}
# <END_DICT>

TAB_NAME = "YZ_Rays"
COL_NAME = "YZ_Rays_Output"

# ------------------------------------------------------------------------
# Material Engine
# ------------------------------------------------------------------------
def get_physics_mat(name, color, alpha):
    m_name = f"Mat_{name}_V44"
    mat = bpy.data.materials.get(m_name) or bpy.data.materials.new(name=m_name)
    mat.use_nodes = True
    if hasattr(mat, "blend_method"):
        mat.blend_method = 'BLEND'
    nodes = mat.node_tree.nodes
    nodes.clear()
    out = nodes.new('ShaderNodeOutputMaterial')
    p_bsdf = nodes.new('ShaderNodeBsdfPrincipled')
    p_bsdf.inputs["Base Color"].default_value = (*color, 1.0)
    p_bsdf.inputs["Alpha"].default_value = alpha
    p_bsdf.inputs["Emission Color"].default_value = (*color, 1.0)
    p_bsdf.inputs["Emission Strength"].default_value = 1.5
    mat.node_tree.links.new(p_bsdf.outputs["BSDF"], out.inputs["Surface"])
    return mat

# ------------------------------------------------------------------------
# Geometry Helper - shaft and head with separate material slots
# ------------------------------------------------------------------------
def build_arrow_dual(
    bm_shaft, bm_head,
    start, end,
    shaft_thick, head_thick
):
    """Build shaft and arrowhead into separate bmeshes for separate material control."""
    direction = end - start
    dist = direction.length
    if dist < 0.01:
        return

    s_h = dist * 0.82
    h_h = dist * 0.18

    # Shaft
    res_s = bmesh.ops.create_cone(
        bm_shaft, cap_ends=True, segments=10,
        radius1=shaft_thick, radius2=shaft_thick, depth=s_h
    )
    bmesh.ops.translate(bm_shaft, verts=res_s['verts'], vec=Vector((0, 0, s_h / 2)))
    rot = Vector((0, 0, 1)).rotation_difference(direction.normalized())
    bmesh.ops.rotate(bm_shaft, verts=res_s['verts'], cent=(0, 0, 0), matrix=rot.to_matrix().to_4x4())
    bmesh.ops.translate(bm_shaft, verts=res_s['verts'], vec=start)

    # Head (cone)
    res_h = bmesh.ops.create_cone(
        bm_head, cap_ends=True, segments=10,
        radius1=head_thick, radius2=0.0, depth=h_h
    )
    bmesh.ops.translate(bm_head, verts=res_h['verts'], vec=Vector((0, 0, s_h + h_h / 2)))
    bmesh.ops.rotate(bm_head, verts=res_h['verts'], cent=(0, 0, 0), matrix=rot.to_matrix().to_4x4())
    bmesh.ops.translate(bm_head, verts=res_h['verts'], vec=start)

def draw_physics_scene(context):
    p = context.scene.yz_rays_props

    # Ensure collection exists
    col = bpy.data.collections.get(COL_NAME)
    if not col:
        col = bpy.data.collections.new(COL_NAME)
    if col.name not in context.scene.collection.children:
        context.scene.collection.children.link(col)

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

    c = 1.0
    t = p.time_t
    v = p.speed_v

    r_contracted = math.sqrt(max(0.0, (c * t) ** 2 - (v * t) ** 2))
    r_static = c * t
    emission_pt = Vector((-v * t, 0, 0))
    current_pt = Vector((0, 0, 0))

    # ──────────────────────────────────────────────────
    # 1. PLANAR RAYS SHAFT + HEAD (separate objects)
    # ──────────────────────────────────────────────────
    o_ps, m_ps = get_obj("Rays_Planar_Shaft", "tag_p_shaft")
    o_ph, m_ph = get_obj("Rays_Planar_Head", "tag_p_head")
    o_ps.hide_viewport = not p.p_show
    o_ph.hide_viewport = not p.p_show

    bm_s = bmesh.new()
    bm_h = bmesh.new()
    for i in range(12):
        ang = math.radians(i * 30)
        target = Vector((0, r_contracted * math.cos(ang), r_contracted * math.sin(ang)))
        build_arrow_dual(bm_s, bm_h, current_pt, target, p.p_thick, p.p_head_thick)
    bm_s.to_mesh(m_ps); bm_s.free()
    bm_h.to_mesh(m_ph); bm_h.free()

    o_ps.data.materials.clear()
    o_ps.data.materials.append(get_physics_mat("P_Shaft", p.p_color, p.p_alpha))
    o_ph.data.materials.clear()
    o_ph.data.materials.append(get_physics_mat("P_Head", p.p_head_color, p.p_head_alpha))

    # ──────────────────────────────────────────────────
    # 2. CONICAL RAYS SHAFT + HEAD (separate objects)
    # ──────────────────────────────────────────────────
    o_ss, m_ss = get_obj("Rays_Conical_Shaft", "tag_s_shaft")
    o_sh, m_sh = get_obj("Rays_Conical_Head", "tag_s_head")
    o_ss.hide_viewport = not p.s_show
    o_sh.hide_viewport = not p.s_show

    bm_s = bmesh.new()
    bm_h = bmesh.new()
    for i in range(12):
        ang = math.radians(i * 30)
        target = Vector((0, r_contracted * math.cos(ang), r_contracted * math.sin(ang)))
        build_arrow_dual(bm_s, bm_h, emission_pt, target, p.s_thick, p.s_head_thick)
    bm_s.to_mesh(m_ss); bm_s.free()
    bm_h.to_mesh(m_sh); bm_h.free()

    o_ss.data.materials.clear()
    o_ss.data.materials.append(get_physics_mat("S_Shaft", p.s_color, p.s_alpha))
    o_sh.data.materials.clear()
    o_sh.data.materials.append(get_physics_mat("S_Head", p.s_head_color, p.s_head_alpha))

    # ──────────────────────────────────────────────────
    # 3. TRAIN DISK (x=0)
    # ──────────────────────────────────────────────────
    o_tr, m_tr = get_obj("Disk_Train_x0", "tag_tr")
    o_tr.hide_viewport = not p.show_tr_disk
    bm = bmesh.new()
    bmesh.ops.create_cone(
        bm, cap_ends=True, segments=64,
        radius1=r_contracted, radius2=r_contracted,
        depth=max(0.002, p.tr_disk_thick)
    )
    bmesh.ops.rotate(
        bm, verts=bm.verts, cent=(0, 0, 0),
        matrix=Matrix.Rotation(math.radians(90), 4, 'Y')
    )
    bm.to_mesh(m_tr); bm.free()
    o_tr.data.materials.clear()
    o_tr.data.materials.append(get_physics_mat("DiskTr", p.tr_disk_col, p.tr_disk_alpha))

    # ──────────────────────────────────────────────────
    # 4. RAILWAY DISK (x=-vt)
    # ──────────────────────────────────────────────────
    o_rw, m_rw = get_obj("Disk_Railway_xVT", "tag_rw")
    o_rw.hide_viewport = not p.show_rw_disk
    bm = bmesh.new()
    bmesh.ops.create_cone(
        bm, cap_ends=True, segments=64,
        radius1=r_static, radius2=r_static,
        depth=max(0.002, p.rw_disk_thick)
    )
    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=emission_pt)
    bm.to_mesh(m_rw); bm.free()
    o_rw.data.materials.clear()
    o_rw.data.materials.append(get_physics_mat("DiskRw", p.rw_disk_col, p.rw_disk_alpha))

# ------------------------------------------------------------------------
# Debounce Timer
# ------------------------------------------------------------------------
_timer = None

def update_call(self, context):
    global _timer
    if _timer:
        try:
            bpy.app.timers.unregister(_timer)
        except Exception:
            pass
    _timer = bpy.app.timers.register(
        lambda: draw_physics_scene(bpy.context) and None,
        first_interval=0.05
    )

# ------------------------------------------------------------------------
# UI Properties
# ------------------------------------------------------------------------
class YZ_Props(bpy.types.PropertyGroup):

    # ── Global ──
    speed_v: bpy.props.FloatProperty(
        name="Velocity (v/c)", default=0.6, min=0.0, max=0.99, update=update_call)
    time_t: bpy.props.FloatProperty(
        name="Time (t)", default=10.0, min=0.1, update=update_call)

    # ── Planar Rays (Yellow) ──
    p_show: bpy.props.BoolProperty(name="Visible", default=True, update=update_call)
    p_color: bpy.props.FloatVectorProperty(
        name="Shaft Color", subtype='COLOR',
        default=(1.0, 0.9, 0.0), min=0.0, max=1.0, update=update_call)
    p_thick: bpy.props.FloatProperty(
        name="Shaft Thickness", default=0.05, min=0.002, max=1.0, update=update_call)
    p_alpha: bpy.props.FloatProperty(
        name="Shaft Alpha", min=0.0, max=1.0, default=1.0, update=update_call)
    p_head_color: bpy.props.FloatVectorProperty(
        name="Head Color", subtype='COLOR',
        default=(1.0, 0.9, 0.0), min=0.0, max=1.0, update=update_call)
    p_head_thick: bpy.props.FloatProperty(
        name="Head Size", default=0.125, min=0.005, max=2.0, update=update_call)
    p_head_alpha: bpy.props.FloatProperty(
        name="Head Alpha", min=0.0, max=1.0, default=1.0, update=update_call)

    # ── Conical Rays (Red) ──
    s_show: bpy.props.BoolProperty(name="Visible", default=True, update=update_call)
    s_color: bpy.props.FloatVectorProperty(
        name="Shaft Color", subtype='COLOR',
        default=(1.0, 0.3, 0.0), min=0.0, max=1.0, update=update_call)
    s_thick: bpy.props.FloatProperty(
        name="Shaft Thickness", default=0.04, min=0.002, max=1.0, update=update_call)
    s_alpha: bpy.props.FloatProperty(
        name="Shaft Alpha", min=0.0, max=1.0, default=0.8, update=update_call)
    s_head_color: bpy.props.FloatVectorProperty(
        name="Head Color", subtype='COLOR',
        default=(1.0, 0.3, 0.0), min=0.0, max=1.0, update=update_call)
    s_head_thick: bpy.props.FloatProperty(
        name="Head Size", default=0.1, min=0.005, max=2.0, update=update_call)
    s_head_alpha: bpy.props.FloatProperty(
        name="Head Alpha", min=0.0, max=1.0, default=0.8, update=update_call)

    # ── Train Disk ──
    show_tr_disk: bpy.props.BoolProperty(name="Visible", default=True, update=update_call)
    tr_disk_col: bpy.props.FloatVectorProperty(
        name="Color", subtype='COLOR',
        default=(0.1, 0.4, 0.8), min=0.0, max=1.0, update=update_call)
    tr_disk_alpha: bpy.props.FloatProperty(
        name="Alpha", min=0.0, max=1.0, default=0.2, update=update_call)
    tr_disk_thick: bpy.props.FloatProperty(
        name="Thickness", default=0.01, min=0.001, max=1.0, update=update_call)

    # ── Railway Disk ──
    show_rw_disk: bpy.props.BoolProperty(name="Visible", default=True, update=update_call)
    rw_disk_col: bpy.props.FloatVectorProperty(
        name="Color", subtype='COLOR',
        default=(0.1, 0.8, 0.5), min=0.0, max=1.0, update=update_call)
    rw_disk_alpha: bpy.props.FloatProperty(
        name="Alpha", min=0.0, max=1.0, default=0.15, update=update_call)
    rw_disk_thick: bpy.props.FloatProperty(
        name="Thickness", default=0.01, min=0.001, max=1.0, update=update_call)

# ------------------------------------------------------------------------
# Operators
# ------------------------------------------------------------------------
class YZ_OT_Generate(bpy.types.Operator):
    bl_idname = "yz.generate"; bl_label = "Generate Scene"
    def execute(self, context):
        draw_physics_scene(context)
        return {'FINISHED'}

class YZ_OT_Copy(bpy.types.Operator):
    bl_idname = "yz.copy_script"; bl_label = "Copy Full Script"
    def execute(self, context):
        p = context.scene.yz_rays_props
        M_S, M_E = "# <BEGIN_DICT>", "# <END_DICT>"
        txt = next((t for t in bpy.data.texts if UNIQUE_ID in t.as_string()), None)
        if not txt:
            self.report({'WARNING'}, "Script text block not found in Blender.")
            return {'CANCELLED'}
        d_str = "CURRENT_DEFAULTS = {\n"
        for k in CURRENT_DEFAULTS.keys():
            val = getattr(p, k)
            if hasattr(val, "__len__"):
                d_str += f'    "{k}": ({", ".join([f"{x:.4f}" for x in val])}),\n'
            elif isinstance(val, bool):
                d_str += f'    "{k}": {val},\n'
            else:
                d_str += f'    "{k}": {val:.4f},\n'
        d_str += "}\n"
        code = txt.as_string()
        result = code.split(M_S)[0] + M_S + "\n" + d_str + M_E + code.split(M_E)[1]
        context.window_manager.clipboard = result
        self.report({'INFO'}, "Script copied to clipboard!")
        return {'FINISHED'}

class YZ_OT_Reset(bpy.types.Operator):
    bl_idname = "yz.reset_defaults"; bl_label = "Reset All Defaults"
    def execute(self, context):
        p = context.scene.yz_rays_props
        for k, v in CURRENT_DEFAULTS.items():
            if hasattr(p, k):
                setattr(p, k, v)
        draw_physics_scene(context)
        return {'FINISHED'}

class YZ_OT_Url(bpy.types.Operator):
    bl_idname = "wm.url_op"; bl_label = "Open URL"
    url: bpy.props.StringProperty()
    def execute(self, context):
        webbrowser.open(self.url)
        return {'FINISHED'}

class YZ_OT_Remove(bpy.types.Operator):
    bl_idname = "wm.yz_remove"; bl_label = "Remove Addon"
    def execute(self, context):
        unregister()
        return {'FINISHED'}

# ------------------------------------------------------------------------
# UI Panels
# ------------------------------------------------------------------------
class YZ_PT_Main(bpy.types.Panel):
    bl_label = "YZ Rays (V44)"; bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        p = context.scene.yz_rays_props

        # Top buttons
        row = layout.row(align=True)
        row.operator("yz.generate", icon='PLAY', text="Generate")
        row.operator("yz.reset_defaults", icon='LOOP_BACK', text="Reset")
        row.operator("yz.copy_script", icon='COPY_ID', text="Copy")

        # ── Global Physics ──
        box = layout.box()
        box.label(text="GLOBAL PHYSICS", icon='PHYSICS')
        box.prop(p, "speed_v")
        box.prop(p, "time_t")

        # ── Planar Rays ──
        box = layout.box()
        row = box.row()
        row.label(text="PLANAR RAYS (Yellow)", icon='PROP_OFF')
        row.prop(p, "p_show", text="")

        col = box.column()
        col.enabled = p.p_show

        # Shaft
        col.label(text="── Shaft ──")
        row = col.row(align=True)
        row.prop(p, "p_color", text="")
        row.prop(p, "p_thick", text="Thickness")
        col.prop(p, "p_alpha", text="Shaft Alpha", slider=True)

        # Head
        col.label(text="── Arrow Head ──")
        row = col.row(align=True)
        row.prop(p, "p_head_color", text="")
        row.prop(p, "p_head_thick", text="Head Size")
        col.prop(p, "p_head_alpha", text="Head Alpha", slider=True)

        # ── Conical Rays ──
        box = layout.box()
        row = box.row()
        row.label(text="CONICAL RAYS (Red)", icon='CONE_DATA')
        row.prop(p, "s_show", text="")

        col = box.column()
        col.enabled = p.s_show

        col.label(text="── Shaft ──")
        row = col.row(align=True)
        row.prop(p, "s_color", text="")
        row.prop(p, "s_thick", text="Thickness")
        col.prop(p, "s_alpha", text="Shaft Alpha", slider=True)

        col.label(text="── Arrow Head ──")
        row = col.row(align=True)
        row.prop(p, "s_head_color", text="")
        row.prop(p, "s_head_thick", text="Head Size")
        col.prop(p, "s_head_alpha", text="Head Alpha", slider=True)

        # ── Train Disk ──
        box = layout.box()
        row = box.row()
        row.label(text="TRAIN DISK  x=0", icon='MESH_CIRCLE')
        row.prop(p, "show_tr_disk", text="")

        col = box.column()
        col.enabled = p.show_tr_disk
        row = col.row(align=True)
        row.prop(p, "tr_disk_col", text="")
        row.prop(p, "tr_disk_thick", text="Thickness")
        col.prop(p, "tr_disk_alpha", text="Alpha", slider=True)

        # ── Railway Disk ──
        box = layout.box()
        row = box.row()
        row.label(text="RAILWAY DISK  x=-vt", icon='MESH_CIRCLE')
        row.prop(p, "show_rw_disk", text="")

        col = box.column()
        col.enabled = p.show_rw_disk
        row = col.row(align=True)
        row.prop(p, "rw_disk_col", text="")
        row.prop(p, "rw_disk_thick", text="Thickness")
        col.prop(p, "rw_disk_alpha", text="Alpha", slider=True)

class YZ_PT_System(bpy.types.Panel):
    bl_label = "System Panel"; bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'; bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        layout.operator("wm.yz_remove", icon='CANCEL', text="Remove Addon")
        layout.operator("wm.url_op", text="Lorentz Theory", icon='WORLD').url = "<https://www.notion.so/>"

# ------------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------------
classes = (
    YZ_Props,
    YZ_OT_Generate,
    YZ_OT_Copy,
    YZ_OT_Reset,
    YZ_OT_Url,
    YZ_OT_Remove,
    YZ_PT_Main,
    YZ_PT_System,
)

def register():
    for c in classes:
        bpy.utils.register_class(c)
    bpy.types.Scene.yz_rays_props = bpy.props.PointerProperty(type=YZ_Props)

def unregister():
    for c in reversed(classes):
        bpy.utils.unregister_class(c)
    if hasattr(bpy.types.Scene, "yz_rays_props"):
        del bpy.types.Scene.yz_rays_props

if __name__ == "__main__":
    register()