blender Million 2026

https://gemini.google.com/share/4b26fce1374c

# 2026-02-21 Session Template: YZ Plane 12 Rays (V4 - Prominent Execute Button)
# Blender 4.3+ Exclusive

UNIQUE_SCRIPT_ID = "YZ_RAYS_TEMPLATE_2026_02_21_V4"
SCRIPT_VERSION = 4

bl_info = {
    "name": "YZ Plane 12 Rays Generator",
    "author": "zionadchat Gemini",
    "version": (1, 4),
    "blender": (4, 3, 0),
    "location": "View3D > Sidebar",
    "description": "Visualize Light Cannot Double Spend in YZ plane (x=0).",
    "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.8000, 0.8000, 0.1000, 1.0000),
    "speed_v": 0.1000,
    "time_t": 10.0000,
    "ray_thickness": 0.0500,
}
# <END_DICT>

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

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

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

# ------------------------------------------------------------------------
# Material Setup
# ------------------------------------------------------------------------
def get_fixed_material(part_id, color):
    mat_name = f"Mat_{part_id}"
    mat = bpy.data.materials.get(mat_name) or bpy.data.materials.new(name=mat_name)
    mat.use_nodes = True
    mat.blend_method = 'BLEND'
    
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    bsdf = nodes.get("Principled BSDF")

    if not bsdf:
        nodes.clear()
        bsdf = nodes.new("ShaderNodeBsdfPrincipled")
        output = nodes.new("ShaderNodeOutputMaterial")
        output.location = (300, 0)
        links.new(bsdf.outputs[0], output.inputs[0])

    if bsdf:
        bsdf.inputs["Base Color"].default_value = color
        if "Alpha" in bsdf.inputs: bsdf.inputs["Alpha"].default_value = color[3]
        if "Emission Color" in bsdf.inputs: bsdf.inputs["Emission Color"].default_value = (color[0], color[1], color[2], 1.0)
        if "Emission Strength" in bsdf.inputs: bsdf.inputs["Emission Strength"].default_value = 1.0
    
    mat.diffuse_color = color
    return mat

# ------------------------------------------------------------------------
# Core Drawing Logic
# ------------------------------------------------------------------------
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)
    if not col:
        col = bpy.data.collections.new(COLLECTION_NAME)
    
    if col.name not in context.scene.collection.children:
        context.scene.collection.children.link(col)

    objects_to_remove = [obj for obj in col.objects if obj.get(OBJECT_TAG)]
    
    for obj in objects_to_remove:
        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")
    obj = bpy.data.objects.new("YZ_Rays", mesh)
    obj[OBJECT_TAG] = True 
    col.objects.link(obj)

    bm = bmesh.new()
    try:
        c = 1.0
        c_dist = c * p.time_t
        v_dist = p.speed_v * p.time_t
        
        if c_dist >= v_dist:
            R = math.sqrt(c_dist**2 - v_dist**2)
        else:
            R = 0.0
            
        if R > 0.001:
            num_rays = 12
            for i in range(num_rays):
                angle_deg = i * 30
                angle_rad = math.radians(angle_deg)
                
                shaft_len = R * 0.90
                head_len = R * 0.10
                thickness = p.ray_thickness
                
                shaft = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=12, radius1=thickness, radius2=thickness, depth=shaft_len)
                bmesh.ops.translate(bm, verts=shaft['verts'], vec=Vector((0, 0, shaft_len/2)))
                
                head = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=12, radius1=thickness*2, radius2=0.0, depth=head_len)
                bmesh.ops.translate(bm, verts=head['verts'], vec=Vector((0, 0, shaft_len + head_len/2)))
                
                rot_mat = Matrix.Rotation(angle_rad, 4, 'X')
                bmesh.ops.rotate(bm, verts=shaft['verts'] + head['verts'], cent=(0,0,0), matrix=rot_mat)

        bm.to_mesh(mesh)
    except Exception as e:
        print(f"BMesh Error: {e}")
    finally:
        bm.free()

    obj.data.materials.clear()
    obj.data.materials.append(get_fixed_material("YZ_Rays", p.ray_color))

# ------------------------------------------------------------------------
# Update Throttling (Debounce)
# ------------------------------------------------------------------------
_update_timer = None

def delayed_update_func():
    global _update_timer
    _update_timer = None 
    try:
        if bpy.context and bpy.context.scene:
            draw_rays_core(bpy.context)
    except Exception as e:
        print(f"Update Error: {e}")
    return None 

def update_view(self, context):
    global _update_timer
    if _update_timer:
        try:
            bpy.app.timers.unregister(_update_timer)
        except ValueError:
            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 Rays", default=CURRENT_DEFAULTS["show_rays"], update=update_view)
    ray_color: bpy.props.FloatVectorProperty(name="Ray Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["ray_color"], update=update_view)
    speed_v: bpy.props.FloatProperty(name="Train Speed Ratio (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)

# ------------------------------------------------------------------------
# Operators
# ------------------------------------------------------------------------
class OBJECT_OT_DrawYZRays(bpy.types.Operator):
    bl_idname = "object.draw_yz_rays"
    bl_label = "描画実行"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        draw_rays_core(context)
        self.report({'INFO'}, "YZ Plane Rays Drawing Executed.")
        return {'FINISHED'}

class OBJECT_OT_DetachRays(bpy.types.Operator):
    bl_idname = "object.detach_rays"; bl_label = "Detach & Keep"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col = bpy.data.collections.get(COLLECTION_NAME)
        if not col: return {'CANCELLED'}
        targets = [obj for obj in col.objects if obj.get(OBJECT_TAG)]
        if not targets: return {'CANCELLED'}
        scene_col = context.scene.collection
        count = 0
        for obj in targets:
            if OBJECT_TAG in obj: del obj[OBJECT_TAG]
            timestamp = datetime.now().strftime('%H%M%S')
            obj.name = f"YZ_Rays_Baked_{timestamp}"
            if obj.data.materials:
                new_mat = obj.data.materials[0].copy()
                new_mat.name = f"Mat_Baked_{timestamp}"
                obj.data.materials.clear()
                obj.data.materials.append(new_mat)
            if obj.name not in scene_col.objects: scene_col.objects.link(obj)
            col.objects.unlink(obj)
            obj.select_set(True)
            context.view_layer.objects.active = obj
            count += 1
        return {'FINISHED'}

class WM_OT_ResetRaysDefaults(bpy.types.Operator):
    bl_idname = "wm.reset_rays_defaults"; bl_label = "Reset Defaults"
    def execute(self, context):
        p = context.scene.yz_rays_props
        p.speed_v = SYSTEM_DEFAULTS["speed_v"]
        p.time_t = SYSTEM_DEFAULTS["time_t"]
        p.ray_thickness = SYSTEM_DEFAULTS["ray_thickness"]
        return {'FINISHED'}

class WM_OT_RemoveYZAddon(bpy.types.Operator):
    bl_idname = "wm.remove_yz_addon"; bl_label = "Remove Addon"
    def execute(self, context):
        def cleanup_logic():
            try: unregister()
            except: pass
            for win in bpy.context.window_manager.windows:
                for area in win.screen.areas: area.tag_redraw()
        bpy.app.timers.register(cleanup_logic, first_interval=0.1)
        return {'FINISHED'}

class WM_OT_CopyRaysScript(bpy.types.Operator):
    bl_idname = "wm.copy_rays_script"; bl_label = "Copy Full Script"
    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: return {'CANCELLED'}
        code_str = target_text.as_string()
        d_str = "CURRENT_DEFAULTS = {\n"
        for k in CURRENT_DEFAULTS.keys():
            val = getattr(p, k)
            if isinstance(val, str): d_str += f'    "{k}": "{val}",\n'
            elif hasattr(val, "__len__") and not isinstance(val, str): d_str += f'    "{k}": ({", ".join([f"{v:.4f}" for v in val])}),\n'
            elif isinstance(val, float): d_str += f'    "{k}": {val:.4f},\n'
            else: d_str += f'    "{k}": {val},\n'
        d_str += "}\n"
        try:
            new_code = code_str.split(M_START)[0] + M_START + "\n" + d_str + M_END + code_str.split(M_END)[1]
            context.window_manager.clipboard = f"# {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Session Template\n" + '\n'.join(new_code.split('\n')[1:])
            self.report({'INFO'}, "Code copied with current values.")
        except Exception as e: return {'CANCELLED'}
        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'}

# ------------------------------------------------------------------------
# UI Panels
# ------------------------------------------------------------------------
class VIEW3D_PT_YZRays(bpy.types.Panel):
    bl_label = "YZ Plane Rays (x=0)"; bl_idname = "VIEW3D_PT_yz_rays"; 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
        
        # 最上段に「描画実行ボタン」を配置!
        draw_row = layout.row()
        draw_row.scale_y = 1.8
        draw_row.operator("object.draw_yz_rays", text="描画実行 (Execute)", icon='PLAY')
        
        layout.separator()
        
        row = layout.row()
        row.operator("wm.copy_rays_script", text="Copy Code with Values", icon='COPY_ID')
        layout.separator()

        layout.prop(p, "show_rays", text="Show Rays")
        if p.show_rays:
            box = layout.box()
            box.prop(p, "ray_color")
            box.prop(p, "ray_thickness")
            layout.separator()
            
            phys_box = layout.box()
            phys_box.label(text="Physics Setup (c=1.0 Fixed)", icon='PHYSICS')
            phys_box.prop(p, "speed_v")
            phys_box.prop(p, "time_t")
            
            c = 1.0
            c_d = c * p.time_t
            v_d = p.speed_v * p.time_t
            current_R = math.sqrt(c_d**2 - v_d**2) if c_d >= v_d else 0.0
            phys_box.label(text=f"→ YZ Plane Radius (R): {current_R:.4f}", icon='OUT_OF_NODE')
            
            sub = box.row()
            sub.scale_y = 1.2
            sub.operator("wm.reset_rays_defaults", text="Reset Defaults", icon='LOOP_BACK')
            
        layout.separator()
        
        row = layout.row()
        row.scale_y = 1.2
        row.operator("object.detach_rays", text="Detach & Keep", icon='PINNED')

class VIEW3D_PT_YZLinks(bpy.types.Panel):
    bl_label = "Theory Links"; bl_idname = "VIEW3D_PT_yz_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"]); op.url = l["url"]

class VIEW3D_PT_YZSystem(bpy.types.Panel):
    bl_label = "System"; bl_idname = "VIEW3D_PT_yz_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', text="Remove Addon")

# ------------------------------------------------------------------------
# Registration (Safe Reload Logic)
# ------------------------------------------------------------------------
classes = (
    PG_YZRaysProps, OBJECT_OT_DrawYZRays, OBJECT_OT_DetachRays,
    WM_OT_ResetRaysDefaults, WM_OT_CopyRaysScript, WM_OT_OpenRaysUrl, WM_OT_RemoveYZAddon, 
    VIEW3D_PT_YZRays, VIEW3D_PT_YZLinks, VIEW3D_PT_YZSystem
)

def register():
    for cls in classes: bpy.utils.register_class(cls)
    if not hasattr(bpy.types.Scene, "yz_rays_props"):
        bpy.types.Scene.yz_rays_props = bpy.props.PointerProperty(type=PG_YZRaysProps)

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

if __name__ == "__main__":
    # 強制的に古いクラスを削除してから登録し直す(テキストエディタ用)
    try: unregister()
    except Exception: pass
    register()