blender Million 2026












# 2026-03-06 13:36:06 Session Template
# Blender 4.0+ Compatible

UNIQUE_SCRIPT_ID = "RELATIVITY_VISUALIZER_2026_03_06_V1"
SCRIPT_VERSION = 1

bl_info = {
    "name": "Relativity Visualizer: Reality vs Image",
    "author": "zionadchat Gemini",
    "version": (6, 1),
    "blender": (4, 0, 0),
    "location": "View3D > Sidebar",
    "description": "Maxwellの絶対静止系における「実体の位置」と「遅れて届く映像」のズレを可視化",
    "category": "Physics",
}

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

# ==============================================================================
#  DYNAMIC DEFAULTS (Saved State)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "velocity": 0.6000,
    "radius": 10.0000,
    "obs_time": 0.0000,
    "show_rays": True,
    "resolution": 12,
    "ring_thick": 0.1000,
    "ray_thick": 0.0500,
    "obs_size": 0.3000,
}
# <END_DICT>

# ==============================================================================
#  SYSTEM DEFAULTS (Factory Reset)
# ==============================================================================
SYSTEM_DEFAULTS = {
    "velocity": 0.6,
    "radius": 5.0,
    "obs_time": 10.0,
    "show_rays": True,
    "resolution": 72,
    "ring_thick": 0.1,
    "ray_thick": 0.05,
    "obs_size": 0.3,
}

TAB_NAME = "77Relativity_Visual"
COLLECTION_NAME = "Relativity_Visualizer_Output"
OBJECT_TAG = "relativity_visualizer_tag"

ADDON_LINKS = (
    {"label": "Code Copy Template 20260221", "url": "<https://www.notion.so/Code-copy-20260221-30ef5dacaf4380f2984bd865b38b55b3>"},
    {"label": "Theory Background: Notion Doc", "url": "<https://www.notion.so/Einstein-from-20260119-main-2edc563be1b080bb94d9f6e5b667fdec>"},
    {"label": "Blender Simulation Guide", "url": "<https://www.notion.so/blender-deviationtokyo-30c293bfbb2980118c25dfc02259b096>"},
)

# ------------------------------------------------------------------------
# Material Setup (Improved Safety)
# ------------------------------------------------------------------------
def get_fixed_material(name, color):
    mat = bpy.data.materials.get(name) or bpy.data.materials.new(name=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 the user deleted the node manually, recreate it
    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:
        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

# ------------------------------------------------------------------------
# Object Creation Utilities
# ------------------------------------------------------------------------
def create_curve(col, name, points, thickness, mat_name, color, circular=False):
    curve = bpy.data.curves.new(name, 'CURVE')
    curve.dimensions = '3D'
    obj = bpy.data.objects.new(name, curve)
    obj[OBJECT_TAG] = True
    col.objects.link(obj)
    
    spline = curve.splines.new('POLY')
    spline.use_cyclic_u = circular
    spline.points.add(len(points) - 1)
    for i, p in enumerate(points):
        spline.points[i].co = (p.x, p.y, p.z, 1)
        
    curve.bevel_depth = thickness
    obj.data.materials.append(get_fixed_material(mat_name, color))
    return obj

def create_sphere(col, name, location, radius, mat_name, color):
    mesh = bpy.data.meshes.new(name)
    obj = bpy.data.objects.new(name, mesh)
    obj[OBJECT_TAG] = True 
    col.objects.link(obj)

    bm = bmesh.new()
    try:
        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=radius)
        bmesh.ops.translate(bm, vec=Vector(location), verts=bm.verts)
        bm.to_mesh(mesh)
    except Exception as e:
        print(f"BMesh Error: {e}")
    finally:
        bm.free()

    obj.data.materials.append(get_fixed_material(mat_name, color))
    return obj

# ------------------------------------------------------------------------
# Core Drawing Logic
# ------------------------------------------------------------------------
def draw_rel_visual_core(context):
    if not context or not context.scene: return

    p = context.scene.rel_visual
    v = p.velocity
    R = p.radius
    t_obs = p.obs_time
    res = p.resolution

    # Safe Collection Linking
    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)

    # Clean up previous tagged objects
    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:
            if isinstance(mesh_data, bpy.types.Mesh):
                bpy.data.meshes.remove(mesh_data)
            elif isinstance(mesh_data, bpy.types.Curve):
                bpy.data.curves.remove(mesh_data)

    # 1. 観測者(現在位置)
    center_pos_now = Vector((v * t_obs, 0, t_obs))
    create_sphere(col, "Observer_Now", center_pos_now, p.obs_size, "Mat_Obs", (0.0, 0.5, 1.0, 1.0)) # 青

    # 2. 実体のリング (Physical Ring - Green)
    phys_points =[]
    for i in range(res):
        phi = math.radians(i * 360.0 / res)
        x = v * t_obs + R * math.cos(phi)
        y = R * math.sin(phi)
        z = t_obs
        phys_points.append(Vector((x, y, z)))
    
    create_curve(col, "Physical_Ring", phys_points, p.ring_thick, "Mat_Phys", (0.0, 1.0, 0.0, 0.8), circular=True) # 緑

    # 3. 映像のリング (Visual Ring - Red) & 光路
    visual_points = []
    ray_paths =[]

    denom = 1.0 - v**2
    if abs(denom) < 1e-9: denom = 1e-9 # v=1対策

    for i in range(res):
        phi = math.radians(i * 360.0 / res)
        term_sqrt = math.sqrt(max(0.0, 1.0 - (v * math.sin(phi))**2))
        term_vcos = v * math.cos(phi)
        
        dt = (R * (term_sqrt - term_vcos)) / denom
        t_emit = t_obs - dt
        
        x_emit = v * t_emit + R * math.cos(phi)
        y_emit = R * math.sin(phi)
        z_emit = t_emit
        
        emit_pos = Vector((x_emit, y_emit, z_emit))
        visual_points.append(emit_pos)
        
        if p.show_rays:
            ray_paths.append([emit_pos, center_pos_now])

    create_curve(col, "Visual_Ring", visual_points, p.ring_thick, "Mat_Vis", (1.0, 0.0, 0.0, 0.8), circular=True) # 赤
    
    if p.show_rays:
        for i, path in enumerate(ray_paths):
            create_curve(col, f"Ray_{i}", path, p.ray_thick, "Mat_Ray", (1.0, 1.0, 0.0, 0.3)) # 黄色

# ------------------------------------------------------------------------
# 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_rel_visual_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_RelativityVisual(bpy.types.PropertyGroup):
    velocity: bpy.props.FloatProperty(
        name="Velocity (v/c)", default=CURRENT_DEFAULTS["velocity"], min=0.0, max=0.99, update=update_view,
        description="円の移動速度 (光速=1)"
    )
    radius: bpy.props.FloatProperty(
        name="Radius (R)", default=CURRENT_DEFAULTS["radius"], min=0.1, update=update_view,
        description="円の半径(剛体として計算)"
    )
    obs_time: bpy.props.FloatProperty(
        name="Observation Time (t)", default=CURRENT_DEFAULTS["obs_time"], min=0.0, update=update_view,
        description="観測者が「見た」時刻。この瞬間の実体と映像を計算します。"
    )
    show_rays: bpy.props.BoolProperty(name="Show Light Paths", default=CURRENT_DEFAULTS["show_rays"], update=update_view)
    resolution: bpy.props.IntProperty(name="Resolution", default=CURRENT_DEFAULTS["resolution"], min=12, max=360, update=update_view)
    ring_thick: bpy.props.FloatProperty(name="Ring Thick", default=CURRENT_DEFAULTS["ring_thick"], min=0.01, update=update_view)
    ray_thick: bpy.props.FloatProperty(name="Ray Thick", default=CURRENT_DEFAULTS["ray_thick"], min=0.01, update=update_view)
    obs_size: bpy.props.FloatProperty(name="Observer Size", default=CURRENT_DEFAULTS["obs_size"], min=0.01, update=update_view)

# ------------------------------------------------------------------------
# Operators
# ------------------------------------------------------------------------
class OBJECT_OT_DrawRelVisual(bpy.types.Operator):
    bl_idname = "object.draw_rel_visual"
    bl_label = "Force Refresh"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        draw_rel_visual_core(context)
        return {'FINISHED'}

class OBJECT_OT_DetachVisual(bpy.types.Operator):
    bl_idname = "object.detach_visual"
    bl_label = "Detach & Keep"
    bl_description = "Stop controlling the current visualization and keep it in the scene"
    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:
            self.report({'WARNING'}, "No active visualization to detach.")
            return {'CANCELLED'}
        
        scene_col = context.scene.collection
        count = 0
        timestamp = datetime.now().strftime('%H%M%S')
        
        for obj in targets:
            if OBJECT_TAG in obj: del obj[OBJECT_TAG]
            
            orig_name = obj.name
            obj.name = f"{orig_name}_Baked_{timestamp}"
            
            # Independent Material Logic
            if obj.data.materials:
                old_mat = obj.data.materials[0]
                new_mat = old_mat.copy()
                new_mat.name = f"{old_mat.name}_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
            
        self.report({'INFO'}, f"Detached {count} object(s).")
        return {'FINISHED'}

class WM_OT_ResetToDefaults(bpy.types.Operator):
    bl_idname = "wm.reset_to_defaults"
    bl_label = "Reset to Defaults"
    bl_description = "Reset parameters to system defaults"
    
    def execute(self, context):
        p = context.scene.rel_visual
        for key, val in SYSTEM_DEFAULTS.items():
            setattr(p, key, val)
        self.report({'INFO'}, "Parameters reset to defaults.")
        return {'FINISHED'}

class WM_OT_RemoveAddon(bpy.types.Operator):
    bl_idname = "wm.remove_addon"
    bl_label = "Remove Addon"
    
    def execute(self, context):
        def cleanup_logic():
            if __name__ == "__main__":
                unregister()
                print(f"[{bl_info['name']}] Unregistered from Script Mode.")
            else:
                import addon_utils
                module_name = __package__ if __package__ else __name__
                addon_utils.disable(module_name, default_set=True)
                print(f"[{bl_info['name']}] Disabled from Addon Mode.")
            
            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)
        self.report({'INFO'}, "Removing Addon UI...")
        return {'FINISHED'}

class WM_OT_CopyFullScript(bpy.types.Operator):
    bl_idname = "wm.copy_full_script"
    bl_label = "Copy Full Script"
    
    def execute(self, context):
        p = context.scene.rel_visual
        M_START, M_END = "# <BEG" + "IN_DICT>", "# <EN" + "D_DICT>"
        
        found_texts =[t for t in bpy.data.texts if UNIQUE_SCRIPT_ID in t.as_string()]
        
        if not found_texts:
            self.report({'ERROR'}, "Script source not found.")
            return {'CANCELLED'}
        
        target_text = found_texts[0]
        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): 
                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: d_str += f'    "{k}": {val},\n'
        d_str += "}\n"
        
        try:
            pre_dict = code_str.split(M_START)[0]; post_dict = code_str.split(M_END)[1]
            new_code = pre_dict + M_START + "\n" + d_str + M_END + post_dict
            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:
            self.report({'ERROR'}, f"Failed to parse: {e}")
            return {'CANCELLED'}
        return {'FINISHED'}

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

# ------------------------------------------------------------------------
# UI Panels
# ------------------------------------------------------------------------
class VIEW3D_PT_RelVisualPanel(bpy.types.Panel):
    bl_label = "Relativity Visualizer"
    bl_idname = "VIEW3D_PT_rel_visual"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        p = context.scene.rel_visual
        
        row = layout.row()
        row.operator("wm.copy_full_script", text="Copy Code with Values", icon='COPY_ID')
        layout.separator()
        
        box = layout.box()
        box.label(text="Time Control (重要)", icon='TIME')
        box.prop(p, "obs_time", text="Observation Time (t)")
        
        box = layout.box()
        box.label(text="Physics Parameters", icon='PHYSICS')
        box.prop(p, "velocity")
        box.prop(p, "radius")
        
        box = layout.box()
        box.label(text="Visual Settings", icon='MESH_GRID')
        box.prop(p, "show_rays")
        box.prop(p, "resolution")
        box.prop(p, "ring_thick")
        box.prop(p, "ray_thick")
        box.prop(p, "obs_size")
        
        layout.separator()
        row = layout.row()
        row.label(text="Green: Physical Reality (Now)", icon='FILE_3D')
        row = layout.row()
        row.label(text="Red: Visual Image (Past)", icon='IMAGE_RGB')
        
        layout.separator()
        layout.operator("wm.reset_to_defaults", text="Reset to Defaults", icon='LOOP_BACK')
        
        row = layout.row()
        row.scale_y = 1.5
        row.operator("object.detach_visual", text="Detach & Keep", icon='PINNED')
        
        layout.operator("object.draw_rel_visual", text="Force Refresh", icon='FILE_REFRESH')

class VIEW3D_PT_Links(bpy.types.Panel):
    bl_label = "Theory Links"
    bl_idname = "VIEW3D_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: 
            op = self.layout.operator("wm.open_url", text=l["label"])
            op.url = l["url"]

class VIEW3D_PT_System(bpy.types.Panel):
    bl_label = "System"
    bl_idname = "VIEW3D_PT_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_addon", icon='CANCEL', text="Remove Addon")

# ------------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------------
classes = (
    PG_RelativityVisual,
    OBJECT_OT_DrawRelVisual,
    OBJECT_OT_DetachVisual,
    WM_OT_ResetToDefaults,
    WM_OT_RemoveAddon,
    WM_OT_CopyFullScript,
    WM_OT_OpenUrl,
    VIEW3D_PT_RelVisualPanel,
    VIEW3D_PT_Links,
    VIEW3D_PT_System,
)

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

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

if __name__ == "__main__":
    register()
# 2026-03-06 Session Template
# Blender 4.0+ Compatible

UNIQUE_SCRIPT_ID = "RELATIVITY_VISUALIZER_2026_03_06_V1"
SCRIPT_VERSION = 1

bl_info = {
    "name": "Relativity Visualizer: Reality vs Image",
    "author": "zionadchat Gemini",
    "version": (6, 1),
    "blender": (4, 0, 0),
    "location": "View3D > Sidebar",
    "description": "Maxwellの絶対静止系における「実体の位置」と「遅れて届く映像」のズレを可視化",
    "category": "Physics",
}

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

# ==============================================================================
#  DYNAMIC DEFAULTS (Saved State)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "velocity": 0.6000,
    "radius": 5.0000,
    "obs_time": 10.0000,
    "show_rays": True,
    "resolution": 72,
    "ring_thick": 0.1000,
    "ray_thick": 0.0500,
    "obs_size": 0.3000,
}
# <END_DICT>

# ==============================================================================
#  SYSTEM DEFAULTS (Factory Reset)
# ==============================================================================
SYSTEM_DEFAULTS = {
    "velocity": 0.6,
    "radius": 5.0,
    "obs_time": 10.0,
    "show_rays": True,
    "resolution": 72,
    "ring_thick": 0.1,
    "ray_thick": 0.05,
    "obs_size": 0.3,
}

TAB_NAME = "Relativity_Visual"
COLLECTION_NAME = "Relativity_Visualizer_Output"
OBJECT_TAG = "relativity_visualizer_tag"

ADDON_LINKS = (
    {"label": "Code Copy Template 20260221", "url": "<https://www.notion.so/Code-copy-20260221-30ef5dacaf4380f2984bd865b38b55b3>"},
    {"label": "Theory Background: Notion Doc", "url": "<https://www.notion.so/Einstein-from-20260119-main-2edc563be1b080bb94d9f6e5b667fdec>"},
    {"label": "Blender Simulation Guide", "url": "<https://www.notion.so/blender-deviationtokyo-30c293bfbb2980118c25dfc02259b096>"},
)

# ------------------------------------------------------------------------
# Material Setup (Improved Safety)
# ------------------------------------------------------------------------
def get_fixed_material(name, color):
    mat = bpy.data.materials.get(name) or bpy.data.materials.new(name=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 the user deleted the node manually, recreate it
    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:
        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

# ------------------------------------------------------------------------
# Object Creation Utilities
# ------------------------------------------------------------------------
def create_curve(col, name, points, thickness, mat_name, color, circular=False):
    curve = bpy.data.curves.new(name, 'CURVE')
    curve.dimensions = '3D'
    obj = bpy.data.objects.new(name, curve)
    obj[OBJECT_TAG] = True
    col.objects.link(obj)
    
    spline = curve.splines.new('POLY')
    spline.use_cyclic_u = circular
    spline.points.add(len(points) - 1)
    for i, p in enumerate(points):
        spline.points[i].co = (p.x, p.y, p.z, 1)
        
    curve.bevel_depth = thickness
    obj.data.materials.append(get_fixed_material(mat_name, color))
    return obj

def create_sphere(col, name, location, radius, mat_name, color):
    mesh = bpy.data.meshes.new(name)
    obj = bpy.data.objects.new(name, mesh)
    obj[OBJECT_TAG] = True 
    col.objects.link(obj)

    bm = bmesh.new()
    try:
        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=radius)
        bmesh.ops.translate(bm, vec=Vector(location), verts=bm.verts)
        bm.to_mesh(mesh)
    except Exception as e:
        print(f"BMesh Error: {e}")
    finally:
        bm.free()

    obj.data.materials.append(get_fixed_material(mat_name, color))
    return obj

# ------------------------------------------------------------------------
# Core Drawing Logic
# ------------------------------------------------------------------------
def draw_rel_visual_core(context):
    if not context or not context.scene: return

    p = context.scene.rel_visual
    v = p.velocity
    R = p.radius
    t_obs = p.obs_time
    res = p.resolution

    # Safe Collection Linking
    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)

    # Clean up previous tagged objects
    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:
            if isinstance(mesh_data, bpy.types.Mesh):
                bpy.data.meshes.remove(mesh_data)
            elif isinstance(mesh_data, bpy.types.Curve):
                bpy.data.curves.remove(mesh_data)

    # 1. 観測者(現在位置)
    center_pos_now = Vector((v * t_obs, 0, t_obs))
    create_sphere(col, "Observer_Now", center_pos_now, p.obs_size, "Mat_Obs", (0.0, 0.5, 1.0, 1.0)) # 青

    # 2. 実体のリング (Physical Ring - Green)
    phys_points =[]
    for i in range(res):
        phi = math.radians(i * 360.0 / res)
        x = v * t_obs + R * math.cos(phi)
        y = R * math.sin(phi)
        z = t_obs
        phys_points.append(Vector((x, y, z)))
    
    create_curve(col, "Physical_Ring", phys_points, p.ring_thick, "Mat_Phys", (0.0, 1.0, 0.0, 0.8), circular=True) # 緑

    # 3. 映像のリング (Visual Ring - Red) & 光路
    visual_points = []
    ray_paths =[]

    denom = 1.0 - v**2
    if abs(denom) < 1e-9: denom = 1e-9 # v=1対策

    for i in range(res):
        phi = math.radians(i * 360.0 / res)
        term_sqrt = math.sqrt(max(0.0, 1.0 - (v * math.sin(phi))**2))
        term_vcos = v * math.cos(phi)
        
        dt = (R * (term_sqrt - term_vcos)) / denom
        t_emit = t_obs - dt
        
        x_emit = v * t_emit + R * math.cos(phi)
        y_emit = R * math.sin(phi)
        z_emit = t_emit
        
        emit_pos = Vector((x_emit, y_emit, z_emit))
        visual_points.append(emit_pos)
        
        if p.show_rays:
            ray_paths.append([emit_pos, center_pos_now])

    create_curve(col, "Visual_Ring", visual_points, p.ring_thick, "Mat_Vis", (1.0, 0.0, 0.0, 0.8), circular=True) # 赤
    
    if p.show_rays:
        for i, path in enumerate(ray_paths):
            create_curve(col, f"Ray_{i}", path, p.ray_thick, "Mat_Ray", (1.0, 1.0, 0.0, 0.3)) # 黄色

# ------------------------------------------------------------------------
# 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_rel_visual_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_RelativityVisual(bpy.types.PropertyGroup):
    velocity: bpy.props.FloatProperty(
        name="Velocity (v/c)", default=CURRENT_DEFAULTS["velocity"], min=0.0, max=0.99, update=update_view,
        description="円の移動速度 (光速=1)"
    )
    radius: bpy.props.FloatProperty(
        name="Radius (R)", default=CURRENT_DEFAULTS["radius"], min=0.1, update=update_view,
        description="円の半径(剛体として計算)"
    )
    obs_time: bpy.props.FloatProperty(
        name="Observation Time (t)", default=CURRENT_DEFAULTS["obs_time"], min=0.0, update=update_view,
        description="観測者が「見た」時刻。この瞬間の実体と映像を計算します。"
    )
    show_rays: bpy.props.BoolProperty(name="Show Light Paths", default=CURRENT_DEFAULTS["show_rays"], update=update_view)
    resolution: bpy.props.IntProperty(name="Resolution", default=CURRENT_DEFAULTS["resolution"], min=12, max=360, update=update_view)
    ring_thick: bpy.props.FloatProperty(name="Ring Thick", default=CURRENT_DEFAULTS["ring_thick"], min=0.01, update=update_view)
    ray_thick: bpy.props.FloatProperty(name="Ray Thick", default=CURRENT_DEFAULTS["ray_thick"], min=0.01, update=update_view)
    obs_size: bpy.props.FloatProperty(name="Observer Size", default=CURRENT_DEFAULTS["obs_size"], min=0.01, update=update_view)

# ------------------------------------------------------------------------
# Operators
# ------------------------------------------------------------------------
class OBJECT_OT_DrawRelVisual(bpy.types.Operator):
    bl_idname = "object.draw_rel_visual"
    bl_label = "Force Refresh"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        draw_rel_visual_core(context)
        return {'FINISHED'}

class OBJECT_OT_DetachVisual(bpy.types.Operator):
    bl_idname = "object.detach_visual"
    bl_label = "Detach & Keep"
    bl_description = "Stop controlling the current visualization and keep it in the scene"
    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:
            self.report({'WARNING'}, "No active visualization to detach.")
            return {'CANCELLED'}
        
        scene_col = context.scene.collection
        count = 0
        timestamp = datetime.now().strftime('%H%M%S')
        
        for obj in targets:
            if OBJECT_TAG in obj: del obj[OBJECT_TAG]
            
            orig_name = obj.name
            obj.name = f"{orig_name}_Baked_{timestamp}"
            
            # Independent Material Logic
            if obj.data.materials:
                old_mat = obj.data.materials[0]
                new_mat = old_mat.copy()
                new_mat.name = f"{old_mat.name}_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
            
        self.report({'INFO'}, f"Detached {count} object(s).")
        return {'FINISHED'}

class WM_OT_ResetToDefaults(bpy.types.Operator):
    bl_idname = "wm.reset_to_defaults"
    bl_label = "Reset to Defaults"
    bl_description = "Reset parameters to system defaults"
    
    def execute(self, context):
        p = context.scene.rel_visual
        for key, val in SYSTEM_DEFAULTS.items():
            setattr(p, key, val)
        self.report({'INFO'}, "Parameters reset to defaults.")
        return {'FINISHED'}

class WM_OT_RemoveAddon(bpy.types.Operator):
    bl_idname = "wm.remove_addon"
    bl_label = "Remove Addon"
    
    def execute(self, context):
        def cleanup_logic():
            if __name__ == "__main__":
                unregister()
                print(f"[{bl_info['name']}] Unregistered from Script Mode.")
            else:
                import addon_utils
                module_name = __package__ if __package__ else __name__
                addon_utils.disable(module_name, default_set=True)
                print(f"[{bl_info['name']}] Disabled from Addon Mode.")
            
            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)
        self.report({'INFO'}, "Removing Addon UI...")
        return {'FINISHED'}

class WM_OT_CopyFullScript(bpy.types.Operator):
    bl_idname = "wm.copy_full_script"
    bl_label = "Copy Full Script"
    
    def execute(self, context):
        p = context.scene.rel_visual
        M_START, M_END = "# <BEG" + "IN_DICT>", "# <EN" + "D_DICT>"
        
        found_texts =[t for t in bpy.data.texts if UNIQUE_SCRIPT_ID in t.as_string()]
        
        if not found_texts:
            self.report({'ERROR'}, "Script source not found.")
            return {'CANCELLED'}
        
        target_text = found_texts[0]
        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): 
                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: d_str += f'    "{k}": {val},\n'
        d_str += "}\n"
        
        try:
            pre_dict = code_str.split(M_START)[0]; post_dict = code_str.split(M_END)[1]
            new_code = pre_dict + M_START + "\n" + d_str + M_END + post_dict
            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:
            self.report({'ERROR'}, f"Failed to parse: {e}")
            return {'CANCELLED'}
        return {'FINISHED'}

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

# ------------------------------------------------------------------------
# UI Panels
# ------------------------------------------------------------------------
class VIEW3D_PT_RelVisualPanel(bpy.types.Panel):
    bl_label = "Relativity Visualizer"
    bl_idname = "VIEW3D_PT_rel_visual"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        p = context.scene.rel_visual
        
        row = layout.row()
        row.operator("wm.copy_full_script", text="Copy Code with Values", icon='COPY_ID')
        layout.separator()
        
        box = layout.box()
        box.label(text="Time Control (重要)", icon='TIME')
        box.prop(p, "obs_time", text="Observation Time (t)")
        
        box = layout.box()
        box.label(text="Physics Parameters", icon='PHYSICS')
        box.prop(p, "velocity")
        box.prop(p, "radius")
        
        box = layout.box()
        box.label(text="Visual Settings", icon='MESH_GRID')
        box.prop(p, "show_rays")
        box.prop(p, "resolution")
        box.prop(p, "ring_thick")
        box.prop(p, "ray_thick")
        box.prop(p, "obs_size")
        
        layout.separator()
        row = layout.row()
        row.label(text="Green: Physical Reality (Now)", icon='FILE_3D')
        row = layout.row()
        row.label(text="Red: Visual Image (Past)", icon='IMAGE_RGB')
        
        layout.separator()
        layout.operator("wm.reset_to_defaults", text="Reset to Defaults", icon='LOOP_BACK')
        
        row = layout.row()
        row.scale_y = 1.5
        row.operator("object.detach_visual", text="Detach & Keep", icon='PINNED')
        
        layout.operator("object.draw_rel_visual", text="Force Refresh", icon='FILE_REFRESH')

class VIEW3D_PT_Links(bpy.types.Panel):
    bl_label = "Theory Links"
    bl_idname = "VIEW3D_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: 
            op = self.layout.operator("wm.open_url", text=l["label"])
            op.url = l["url"]

class VIEW3D_PT_System(bpy.types.Panel):
    bl_label = "System"
    bl_idname = "VIEW3D_PT_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_addon", icon='CANCEL', text="Remove Addon")

# ------------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------------
classes = (
    PG_RelativityVisual,
    OBJECT_OT_DrawRelVisual,
    OBJECT_OT_DetachVisual,
    WM_OT_ResetToDefaults,
    WM_OT_RemoveAddon,
    WM_OT_CopyFullScript,
    WM_OT_OpenUrl,
    VIEW3D_PT_RelVisualPanel,
    VIEW3D_PT_Links,
    VIEW3D_PT_System,
)

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

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

if __name__ == "__main__":
    register()