blender Million 2026

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

https://x.com/zionadchat

1c7157f0-s.png

作業場

作業場 (1)

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

rapture_20260222001041.png

# 2026-02-22 00:04:59 Session Template
# Blender 5.0+ Exclusive | International English | Bug Fix V19

UNIQUE_SCRIPT_ID = "YZ_RAYS_STABLE_TEMPLATE_2026_02_22_V19"
SCRIPT_VERSION = 19

bl_info = {
    "name": "YZ Plane 12 Rays Generator (V19)",
    "author": "zionadchat Gemini",
    "version": (1, 19),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > YZ_Rays",
    "description": "Robust relativity visualization with corrected script copy engine.",
    "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_rays": True,
    "ray_color": (0.0615, 0.8000, 0.1287),
    "ray_alpha": 1.0000,
    "circle_color": (0.1000, 0.5000, 0.8000),
    "circle_alpha": 1.0000,
    "speed_v": 0.8500,
    "time_t": 10.0000,
    "ray_thickness": 0.1900,
    "circle_depth": 3.7200,
    "circle_solid": 0.0500,
}
# <END_DICT>

# ==============================================================================
#  SYSTEM DEFAULTS (Factory Reset)
# ==============================================================================
SYSTEM_DEFAULTS = {
    "speed_v": 0.1,
    "time_t": 10.0,
    "ray_thickness": 0.05,
    "circle_depth": 0.1,
    "circle_solid": 0.05,
}

TAB_NAME = "YZ_Rays"
COLLECTION_NAME = "YZ_Rays_Output"
OBJECT_TAG = "yz_rays_tag"

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

# ------------------------------------------------------------------------
# Material Setup
# ------------------------------------------------------------------------
def get_transparent_material(part_id, color_rgb, alpha):
    mat_name = f"Mat_{part_id}_V19"
    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
    nodes.clear()
    node_output = nodes.new(type='ShaderNodeOutputMaterial')
    node_principled = nodes.new(type='ShaderNodeBsdfPrincipled')
    
    rgba = (*color_rgb, 1.0)
    node_principled.inputs["Base Color"].default_value = rgba
    node_principled.inputs["Alpha"].default_value = alpha
    node_principled.inputs["Emission Color"].default_value = rgba
    node_principled.inputs["Emission Strength"].default_value = 1.0
    
    mat.node_tree.links.new(node_principled.outputs["BSDF"], node_output.inputs["Surface"])
    return mat

# ------------------------------------------------------------------------
# Core Drawing
# ------------------------------------------------------------------------
def draw_rays_core(context):
    if not context or not context.scene: return
    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)

    for obj in [o for o in col.objects if o.get(OBJECT_TAG)]:
        mesh_data = obj.data
        bpy.data.objects.remove(obj, do_unlink=True)
        if mesh_data and mesh_data.users == 0:
            bpy.data.meshes.remove(mesh_data)

    if not p.show_rays: return

    mesh = bpy.data.meshes.new("YZ_Rays_Mesh")
    obj = bpy.data.objects.new("YZ_Rays_Obj", mesh)
    obj[OBJECT_TAG] = True
    col.objects.link(obj)

    obj.data.materials.append(get_transparent_material("Rays", p.ray_color, p.ray_alpha))
    obj.data.materials.append(get_transparent_material("Circle", p.circle_color, p.circle_alpha))

    bm = bmesh.new()
    c = 1.0

    # Circle
    R_circle = c * p.time_t
    if R_circle > 0.001:
        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)
        for face in bm.faces: face.material_index = 1
        
        bmesh.ops.rotate(bm, verts=bm.verts, cent=(0,0,0), matrix=Matrix.Rotation(math.radians(90), 4, 'Y'))

    # Rays
    c_dist, v_dist = c * p.time_t, p.speed_v * p.time_t
    R_rays = math.sqrt(c_dist**2 - v_dist**2) if c_dist >= v_dist else 0.0

    if R_rays > 0.001:
        for i in range(12):
            angle_rad = math.radians(i * 30)
            shaft_len, head_len = R_rays * 0.9, R_rays * 0.1
            
            res_s = bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=p.ray_thickness, radius2=p.ray_thickness, depth=shaft_len)
            for f in res_s.get('geom', res_s.get('faces', [])):
                if isinstance(f, bmesh.types.BMFace): f.material_index = 0
            bmesh.ops.translate(bm, verts=res_s['verts'], vec=Vector((0, 0, shaft_len/2)))

            res_h = bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=p.ray_thickness * 2, radius2=0, depth=head_len)
            for f in res_h.get('geom', res_h.get('faces', [])):
                if isinstance(f, bmesh.types.BMFace): f.material_index = 0
            bmesh.ops.translate(bm, verts=res_h['verts'], vec=Vector((0, 0, shaft_len + head_len/2)))

            bmesh.ops.rotate(bm, verts=list(res_s['verts']) + list(res_h['verts']), cent=(0,0,0), matrix=Matrix.Rotation(angle_rad, 4, 'X'))

    bm.to_mesh(mesh)
    bm.free()

# ------------------------------------------------------------------------
# Update Logic
# ------------------------------------------------------------------------
_update_timer = None
def delayed_update_func():
    global _update_timer
    _update_timer = None
    if bpy.context and bpy.context.scene: draw_rays_core(bpy.context)
    return 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(delayed_update_func, first_interval=0.05)

# ------------------------------------------------------------------------
# Properties
# ------------------------------------------------------------------------
class PG_YZRaysProps(bpy.types.PropertyGroup):
    show_rays: bpy.props.BoolProperty(name="Show Elements", default=CURRENT_DEFAULTS["show_rays"], update=update_view)
    ray_color: bpy.props.FloatVectorProperty(name="Ray Color", subtype='COLOR', size=3, default=CURRENT_DEFAULTS["ray_color"], update=update_view)
    ray_alpha: bpy.props.FloatProperty(name="Ray Alpha", min=0.0, max=1.0, default=CURRENT_DEFAULTS["ray_alpha"], update=update_view)
    circle_color: bpy.props.FloatVectorProperty(name="Circle Color", subtype='COLOR', size=3, default=CURRENT_DEFAULTS["circle_color"], update=update_view)
    circle_alpha: bpy.props.FloatProperty(name="Circle Alpha", min=0.0, max=1.0, default=CURRENT_DEFAULTS["circle_alpha"], update=update_view)
    speed_v: bpy.props.FloatProperty(name="Velocity (v/c)", default=CURRENT_DEFAULTS["speed_v"], min=0.0, max=1.0, update=update_view)
    time_t: bpy.props.FloatProperty(name="Time (t)", default=CURRENT_DEFAULTS["time_t"], min=0.01, update=update_view)
    ray_thickness: bpy.props.FloatProperty(name="Ray Thickness", default=CURRENT_DEFAULTS["ray_thickness"], min=0.01, max=1.0, update=update_view)
    circle_depth: bpy.props.FloatProperty(name="Circle Width", default=CURRENT_DEFAULTS["circle_depth"], min=0.0, max=10.0, update=update_view)
    circle_solid: bpy.props.FloatProperty(name="Face Thickness", default=CURRENT_DEFAULTS["circle_solid"], min=0.0, max=1.0, update=update_view)

# ------------------------------------------------------------------------
# Operators (Fixed Copy Engine)
# ------------------------------------------------------------------------
class WM_OT_CopyYZScript(bpy.types.Operator):
    bl_idname = "wm.copy_yz_script"; bl_label = "Copy Full Script"; bl_options = {'REGISTER'}
    def execute(self, context):
        p = context.scene.yz_rays_props
        M_START, M_END = "# <BEG" + "IN_DICT>", "# <EN" + "D_DICT>"
        target_text = next((t for t in bpy.data.texts if UNIQUE_SCRIPT_ID in t.as_string()), None)
        
        if not target_text:
            self.report({'ERROR'}, "Script ID not found in Text Editor.")
            return {'CANCELLED'}

        d_str = "CURRENT_DEFAULTS = {\n"
        for k in CURRENT_DEFAULTS.keys():
            val = getattr(p, k)
            if hasattr(val, "__len__"): # Vectors/Tuples
                v_str = ", ".join([f"{v:.4f}" for v in val])
                d_str += f'    "{k}": ({v_str}),\n'
            elif isinstance(val, float):
                d_str += f'    "{k}": {val:.4f},\n'
            else: # Bools/Ints
                d_str += f'    "{k}": {val},\n'
        d_str += "}\n"

        code = target_text.as_string()
        try:
            pre_part = code.split(M_START)[0]
            post_part = code.split(M_END)[1]
            new_code = pre_part + M_START + "\n" + d_str + M_END + post_part
            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            context.window_manager.clipboard = f"# {timestamp} Session Template\n" + '\n'.join(new_code.split('\n')[1:])
            self.report({'INFO'}, "Copied with latest values.")
        except Exception as e:
            self.report({'ERROR'}, f"Failed to parse dict tags: {e}")
        return {'FINISHED'}

class WM_OT_ResetYZDefaults(bpy.types.Operator):
    bl_idname = "wm.reset_yz_defaults"; bl_label = "Reset Physics"; bl_description = "Reset values to system defaults"
    def execute(self, context):
        p = context.scene.yz_rays_props
        for k, v in SYSTEM_DEFAULTS.items(): setattr(p, k, v)
        self.report({'INFO'}, "Physics parameters reset.")
        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'}

# ------------------------------------------------------------------------
# UI Panels
# ------------------------------------------------------------------------
class VIEW3D_PT_YZRays(bpy.types.Panel):
    bl_label = "YZ Plane Rays (V19)"; 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("wm.copy_yz_script", text="Copy Code with Values", icon='COPY_ID')
        layout.separator()
        layout.prop(p, "show_rays", icon='HIDE_OFF')
        box = layout.box(); box.label(text="Arrows"); box.prop(p, "ray_color", text=""); box.prop(p, "ray_alpha", slider=True); box.prop(p, "ray_thickness")
        box = layout.box(); box.label(text="Circle"); box.prop(p, "circle_color", text=""); box.prop(p, "circle_alpha", slider=True); box.prop(p, "circle_depth"); box.prop(p, "circle_solid")
        phys = layout.box(); phys.label(text="Physics"); phys.prop(p, "speed_v"); phys.prop(p, "time_t"); phys.operator("wm.reset_yz_defaults", icon='LOOP_BACK')

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

# ------------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------------
classes = (PG_YZRaysProps, WM_OT_CopyYZScript, WM_OT_ResetYZDefaults, 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__":
    try: unregister()
    except: pass
    register()