基本オブジェクト操作

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

列車慣性系  干渉計20260222

rapture_20260315235558.png

# Copied: 18:41:56
# Copied: 20260319 18:37:36
# 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"
TAB_NAME = "   [ YZ Rays ]   "

# ★ このスクリプト自身のID (コピー機能で使用)
# ### 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対応)
# ==============================================================================
# <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.2141, 0.0047, 0.5063, 0.2790),
    "speed_v": 0.6000,
    "time_t": 100.0000,
    "circle_depth": 10.0000,
    "circle_solid": 2.0000,
    "ray_thickness": 1.0000,
    "cone_thickness": 1.0000,
}
# <END_DICT>

# ==============================================================================
#  マテリアル作成ロジック (Sphereと全く同じノード構築構造)
# ==============================================================================
def create_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 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))
    emission_origin = Vector((-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'))
    
    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 = Vector((0, R_rays * math.cos(ang), R_rays * math.sin(ang)))
            create_arrow_bm(bm, Vector((0,0,0)), tip, 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 = Vector((0, R_rays * math.cos(ang), R_rays * math.sin(ang)))
            create_arrow_bm(bm, emission_origin, tip, 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"))

# ==============================================================================
#  自動更新用タイマー (Sphereの構造)
# ==============================================================================
_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)
    
    # ★ Sphereと同じ RGBA (size=4) 仕様に変更しました
    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)
    
    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()
        
        # ★ RGBAの4要素を正しくコピーコードに書き込むよう修正
        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'    "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.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, "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()
# 2026-02-22 02:30:00 Session Template
# Blender 5.0+ Exclusive | International English | V30 - Circle Solidify

UNIQUE_SCRIPT_ID = "YZ_RAYS_STABLE_TEMPLATE_2026_02_22_V30"
SCRIPT_VERSION = 30

bl_info = {
    "name": "YZ Plane 12 Rays Generator (V30)",
    "author": "zionadchat Gemini",
    "version": (1, 30),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > YZ_Rays",
    "description": "Added Face Solidify (Thickness) to the circular wave front.",
    "category": "Object",
}

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

# ==============================================================================
#  DYNAMIC DEFAULTS (Saved State)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_circle": True,
    "show_train_rays": True,
    "show_cone_rays": True,
    "ray_color": (0.8000, 0.8000, 0.1000),
    "cone_color": (0.8000, 0.2000, 0.1000),
    "circle_color": (0.1000, 0.5000, 0.8000),
    "speed_v": 0.5000,
    "time_t": 10.0000,
    "circle_depth": 0.1000,
    "circle_solid": 0.0500,
    "ray_thickness": 0.0500,
    "cone_thickness": 0.0350,
}
# <END_DICT>

TAB_NAME = "YZ_Rays"
COLLECTION_NAME = "YZ_Rays_Output"
TAG_C, TAG_T, TAG_O = "tag_yz_c", "tag_yz_t", "tag_yz_o"

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

# ------------------------------------------------------------------------
# Material Helper
# ------------------------------------------------------------------------
def get_mat(name, color_rgb, alpha=1.0):
    mat_name = f"Mat_{name}_V30"
    mat = bpy.data.materials.get(mat_name) or bpy.data.materials.new(name=mat_name)
    mat.use_nodes = True
    if hasattr(mat, "blend_method"): mat.blend_method = 'BLEND'
    if hasattr(mat, "eevee"): mat.eevee.shadow_method = 'NONE'
    nodes = mat.node_tree.nodes
    bsdf = nodes.get("Principled BSDF") or nodes.new('ShaderNodeBsdfPrincipled')
    bsdf.inputs["Base Color"].default_value = (*color_rgb, 1.0)
    bsdf.inputs["Alpha"].default_value = alpha
    bsdf.inputs["Emission Color"].default_value = (*color_rgb, 1.0)
    bsdf.inputs["Emission Strength"].default_value = 1.0
    return mat

# ------------------------------------------------------------------------
# Geometry Engine
# ------------------------------------------------------------------------
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 draw_rays_core(context):
    p = context.scene.yz_rays_props
    col = bpy.data.collections.get(COLLECTION_NAME) or bpy.data.collections.new(COLLECTION_NAME)
    if col.name not in context.scene.collection.children: 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
    R_rays = math.sqrt(max(0, (c*t)**2 - (v*t)**2))
    emission_origin = Vector((-v * t, 0, 0))

    # 1. Circle with Solidify (Tube effect)
    o_c, m_c = sync_obj("YZ_Circle", 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)
    
    # Apply Face Solidify
    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.rotate(bm, verts=bm.verts, cent=(0,0,0), matrix=Matrix.Rotation(math.radians(90), 4, 'Y'))
    bm.to_mesh(m_c); bm.free()
    o_c.data.materials.clear(); o_c.data.materials.append(get_mat("Circle", p.circle_color, 0.5))

    # 2. Train Rays
    o_t, m_t = sync_obj("YZ_Train_Rays", 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 = Vector((0, R_rays * math.cos(ang), R_rays * math.sin(ang)))
            create_arrow_bm(bm, Vector((0,0,0)), tip, p.ray_thickness)
    bm.to_mesh(m_t); bm.free()
    o_t.data.materials.clear(); o_t.data.materials.append(get_mat("Train", p.ray_color, 1.0))

    # 3. Cone Rays
    o_o, m_o = sync_obj("YZ_Cone_Rays", 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 = Vector((0, R_rays * math.cos(ang), R_rays * math.sin(ang)))
            create_arrow_bm(bm, emission_origin, tip, p.cone_thickness)
    bm.to_mesh(m_o); bm.free()
    o_o.data.materials.clear(); o_o.data.materials.append(get_mat("Cone", p.cone_color, 0.6))

# ------------------------------------------------------------------------
# UI & Logic
# ------------------------------------------------------------------------
_update_timer = None
def update_view(self, context):
    global _update_timer
    if _update_timer:
        try: bpy.app.timers.unregister(_update_timer)
        except: pass
    _update_timer = bpy.app.timers.register(lambda: draw_rays_core(bpy.context), first_interval=0.05)

class PG_YZRaysProps(bpy.types.PropertyGroup):
    show_circle: bpy.props.BoolProperty(name="Show Circle", default=True, update=update_view)
    show_train_rays: bpy.props.BoolProperty(name="Show Train Rays", default=True, update=update_view)
    show_cone_rays: bpy.props.BoolProperty(name="Show Cone Rays", default=True, update=update_view)
    ray_color: bpy.props.FloatVectorProperty(name="Train Color", subtype='COLOR', size=3, default=CURRENT_DEFAULTS["ray_color"], update=update_view)
    cone_color: bpy.props.FloatVectorProperty(name="Cone Color", subtype='COLOR', size=3, default=CURRENT_DEFAULTS["cone_color"], update=update_view)
    circle_color: bpy.props.FloatVectorProperty(name="Circle Color", subtype='COLOR', size=3, default=CURRENT_DEFAULTS["circle_color"], update=update_view)
    speed_v: bpy.props.FloatProperty(name="Velocity (v/c)", default=0.5, min=0.0, max=0.99, update=update_view)
    time_t: bpy.props.FloatProperty(name="Time (t)", default=10.0, min=0.1, update=update_view)
    circle_depth: bpy.props.FloatProperty(name="Axial Width (Depth)", default=0.1, min=0.0, max=10.0, update=update_view)
    circle_solid: bpy.props.FloatProperty(name="Face Solidify (Thickness)", default=0.05, min=0.0, max=2.0, update=update_view)
    ray_thickness: bpy.props.FloatProperty(name="Train Ray Thick", default=0.05, min=0.01, max=1.0, update=update_view)
    cone_thickness: bpy.props.FloatProperty(name="Cone Ray Thick", default=0.035, min=0.01, max=1.0, update=update_view)

class OBJECT_OT_DrawYZRays(bpy.types.Operator):
    bl_idname = "object.draw_yz_rays"; bl_label = "EXECUTE DRAW"
    def execute(self, context): draw_rays_core(context); return {'FINISHED'}

class WM_OT_CopyYZScript(bpy.types.Operator):
    bl_idname = "wm.copy_yz_script"; bl_label = "Copy Full Script"
    def execute(self, context):
        p = context.scene.yz_rays_props; M_START, M_END = "# <BEGIN_DICT>", "# <END_DICT>"
        target_text = next((t for t in bpy.data.texts if UNIQUE_SCRIPT_ID in t.as_string()), None)
        if not target_text: 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"{v:.4f}" for v 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 = target_text.as_string()
        try:
            res = code.split(M_START)[0] + M_START + "\n" + d_str + M_END + code.split(M_END)[1]
            context.window_manager.clipboard = f"# {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Session Template\n" + '\n'.join(res.split('\n')[1:])
            self.report({'INFO'}, "Copied.")
        except: pass
        return {'FINISHED'}

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

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

class VIEW3D_PT_YZRays(bpy.types.Panel):
    bl_label = "YZ Plane Rays (V30)"; 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
        layout.operator("object.draw_yz_rays", icon='PLAY')
        layout.operator("wm.copy_yz_script", icon='COPY_ID')
        box = layout.box(); box.prop(p, "show_circle"); box.prop(p, "circle_color", text=""); box.prop(p, "circle_depth"); box.prop(p, "circle_solid")
        box = layout.box(); box.prop(p, "show_train_rays"); box.prop(p, "ray_color", text=""); box.prop(p, "ray_thickness")
        box = layout.box(); box.prop(p, "show_cone_rays"); box.prop(p, "cone_color", text=""); box.prop(p, "cone_thickness")
        phys = layout.box(); phys.prop(p, "speed_v"); phys.prop(p, "time_t")

class VIEW3D_PT_YZLinks(bpy.types.Panel):
    bl_label = "Theory 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: op = self.layout.operator("wm.open_rays_url", text=l["label"], icon='WORLD'); op.url = l["url"]

class VIEW3D_PT_YZSystem(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): self.layout.operator("wm.remove_yz_addon", icon='CANCEL')

classes = (PG_YZRaysProps, OBJECT_OT_DrawYZRays, WM_OT_CopyYZScript, WM_OT_OpenRaysUrl, WM_OT_RemoveYZAddon, VIEW3D_PT_YZRays, VIEW3D_PT_YZLinks, VIEW3D_PT_YZSystem)

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

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