blender Million 2026






今回のログをPDF内に リンクを残す

20260220 Gemini mathjack アームB角度調整機能の追加
<https://gemini.google.com/share/08dd3d8acb83>

# 2026-02-21 09:00:00 Thickness Integrated Master good
# Blender 4.3+ Exclusive

bl_info = {
    "name": "Symmetric Spacetime (v5.44 Thick)",
    "author": "zionadchat Gemini",
    "version": (5, 44),
    "blender": (4, 3, 0),
    "location": "View3D > Sidebar",
    "description": "Full thickness control for all spacetime structures",
    "category": "Physics",
}

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

# ==============================================================================
#  DYNAMIC DEFAULTS
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "calc_mode": "VEL", "velocity": 0.9600, "radius": 10.0000, "target_x": 68.5714,
    "base_angle": 0.0000, "arm_b_angle": 90.0000, "ray_mode": "2",
    "show_ray_classic": True, "show_ray_converge": True, "show_ray_ell_conv": True,
    "show_ray_a_out": True, "show_ray_a_in": True, "show_ray_b_out": True, "show_ray_b_in": True,
    "color_ray_a_out": (1.000, 0.500, 0.000, 1.000), "thick_ray_a_out": 1.41,
    "color_ray_a_in": (0.005, 0.008, 0.271, 1.000), "thick_ray_a_in": 0.73,
    "color_ray_b_out": (0.400, 0.000, 0.000, 1.000), "thick_ray_b_out": 0.80,
    "color_ray_b_in": (0.000, 0.500, 0.000, 1.000), "thick_ray_b_in": 0.30,
    "show_spheroid": True, "color_spheroid": (0.200, 0.100, 0.500, 0.10),
    "show_st_ellipse": True, "color_st_ellipse": (1.000, 1.000, 1.000, 0.80), "thick_st_ellipse": 0.05,
    "show_wavefronts": True, "color_wavefronts": (0.160, 0.000, 0.050, 0.40), "thick_wavefronts": 0.40,
    "show_circle_rings": True, "color_circle_rings": (0.100, 0.200, 0.300, 0.30), "thick_circle_rings": 0.03,
    "show_skeleton": True, "color_skeleton": (0.500, 0.500, 0.500, 0.20), "skel_thick": 0.01,
}
# <END_DICT>

TAB_NAME = "Relativity_Sym_v5"
COLLECTION_NAME = "Relativity_Sym_Output"

ADDON_LINKS = (
    {"label": "理論背景: Notion 資料", "url": "<https://www.notion.so/Einstein-from-20260119-main-2edc563be1b080bb94d9f6e5b667fdec>"},
    {"label": "Blender シミュレーション解説", "url": "<https://www.notion.so/blender-deviationtokyo-30c293bfbb2980118c25dfc02259b096>"},
)

# ------------------------------------------------------------------------
# Material Fix
# ------------------------------------------------------------------------
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'
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    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

# ------------------------------------------------------------------------
# Properties & Physics
# ------------------------------------------------------------------------
def update_physics(self, context):
    p = self
    if p.calc_mode == 'X':
        p.velocity = p.target_x / math.sqrt(p.target_x**2 + (2*p.radius)**2)
    else:
        gamma = 1.0 / math.sqrt(max(0.0001, 1.0 - p.velocity**2))
        p.target_x = p.velocity * 2 * p.radius * gamma
    update_view(self, context)

def update_view(self, context):
    try: bpy.ops.object.draw_spacetime_sym_v5('INVOKE_DEFAULT')
    except: pass

class PG_RelativitySymV5(bpy.types.PropertyGroup):
    calc_mode: bpy.props.EnumProperty(items=[('VEL', "Velocity Mode", ""), ('X', "Target X Mode", "")], default='VEL', update=update_view)
    velocity: bpy.props.FloatProperty(name="Velocity", default=0.96, min=0.0, max=0.999, update=update_view)
    target_x: bpy.props.FloatProperty(name="Target X", default=68.57, min=10.0, max=100.0, update=update_physics)
    radius: bpy.props.FloatProperty(name="Radius", default=10.0, min=0.1, update=update_physics)
    base_angle: bpy.props.FloatProperty(name="Base Angle", default=0.0, min=0, max=360, update=update_view)
    arm_b_angle: bpy.props.FloatProperty(name="Arm B Angle", default=90.0, min=0, max=360, update=update_view)
    ray_mode: bpy.props.EnumProperty(items=[('2', "2 Rays", ""), ('12', "12 Rays", "")], default='2', update=update_view)

    show_ray_classic: bpy.props.BoolProperty(name="Classic", default=True, update=update_view)
    show_ray_converge: bpy.props.BoolProperty(name="Converge", default=True, update=update_view)
    show_ray_ell_conv: bpy.props.BoolProperty(name="Ellipse Conv", default=True, update=update_view)

    color_ray_a_out: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=(1, 0.5, 0, 1), update=update_view)
    color_ray_a_in:  bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.005, 0.008, 0.27, 1), update=update_view)
    color_ray_b_out: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.4, 0, 0, 1), update=update_view)
    color_ray_b_in:  bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=(0, 0.5, 0, 1), update=update_view)
    
    thick_ray_a_out: bpy.props.FloatProperty(default=1.41, min=0, max=2.0, update=update_view)
    thick_ray_a_in:  bpy.props.FloatProperty(default=0.73, min=0, max=2.0, update=update_view)
    thick_ray_b_out: bpy.props.FloatProperty(default=0.8, min=0, max=2.0, update=update_view)
    thick_ray_b_in:  bpy.props.FloatProperty(default=0.3, min=0, max=2.0, update=update_view)
    
    show_ray_a_out: bpy.props.BoolProperty(default=True, update=update_view); show_ray_a_in: bpy.props.BoolProperty(default=True, update=update_view)
    show_ray_b_out: bpy.props.BoolProperty(default=True, update=update_view); show_ray_b_in: bpy.props.BoolProperty(default=True, update=update_view)

    show_spheroid:   bpy.props.BoolProperty(name="Spheroid Surface", default=True, update=update_view)
    color_spheroid:  bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.2, 0.1, 0.5, 0.1), update=update_view)
    show_st_ellipse: bpy.props.BoolProperty(name="Longit. Ellipse", default=True, update=update_view)
    color_st_ellipse: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=(1, 1, 1, 0.8), update=update_view)
    thick_st_ellipse: bpy.props.FloatProperty(name="Thick Ellipse", default=0.05, min=0.0, max=1.0, update=update_view)
    
    show_wavefronts: bpy.props.BoolProperty(name="Wavefronts (Horiz.)", default=True, update=update_view)
    color_wavefronts: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.16, 0, 0.05, 0.4), update=update_view)
    thick_wavefronts: bpy.props.FloatProperty(name="Thick WF", default=0.4, min=0.0, max=2.0, update=update_view)

    show_circle_rings: bpy.props.BoolProperty(name="Tube Rings", default=True, update=update_view)
    color_circle_rings: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.1, 0.2, 0.3, 0.3), update=update_view)
    thick_circle_rings: bpy.props.FloatProperty(name="Thick Rings", default=0.03, min=0.0, max=1.0, update=update_view)
    
    show_skeleton: bpy.props.BoolProperty(name="Skeleton", default=True, update=update_view)
    color_skeleton: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.5, 0.5, 0.5, 0.2), update=update_view)
    skel_thick: bpy.props.FloatProperty(name="Thick Skeleton", default=0.01, min=0.0, max=1.0, update=update_view)

# ------------------------------------------------------------------------
# Drawing logic
# ------------------------------------------------------------------------
def create_curve(col, name, points, thickness, part_id, color, circular=False):
    curve = bpy.data.curves.new(name, 'CURVE'); curve.dimensions = '3D'
    obj = bpy.data.objects.new(name, curve); 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(part_id, color))
    return obj

class OBJECT_OT_DrawSpacetimeSymV5(bpy.types.Operator):
    bl_idname = "object.draw_spacetime_sym_v5"; bl_label = "Refresh View"; bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        p = context.scene.rel_sym_v5
        v, R_val = p.velocity, p.radius
        gamma = 1.0 / math.sqrt(max(0.0001, 1.0 - v**2))
        A = 2.0 * R_val * gamma
        offset = Vector((v * A / 2.0, 0, A / 2.0))
        F1, Mid, F2 = Vector((0,0,0))-offset, Vector((v*A/2,0,A/2))-offset, Vector((v*A,0,A))-offset

        def get_P_locus(deg):
            rad = math.radians(deg)
            return Vector((gamma*(R_val*math.cos(rad)+v*R_val), R_val*math.sin(rad), gamma*(R_val+v*(R_val*math.cos(rad))))) - offset

        def get_P_rigid(deg, z_v):
            t_p = z_v + offset.z; rad = math.radians(deg)
            return Vector((v*t_p - offset.x + R_val*math.cos(rad), R_val*math.sin(rad), z_v))

        col = bpy.data.collections.get(COLLECTION_NAME) or bpy.data.collections.new(COLLECTION_NAME)
        if COLLECTION_NAME not in context.scene.collection.children: context.scene.collection.children.link(col)
        for obj in col.objects: bpy.data.objects.remove(obj, do_unlink=True)

        # Longitudinal Ellipse & Spheroid
        major_2a = math.sqrt(A**2 * (v**2 + 1) + 4 * R_val**2)
        dir_vec = F2 - F1; rot_quat = Vector((1,0,0)).rotation_difference(dir_vec)

        if p.show_st_ellipse:
            v_pts = [Mid + rot_quat @ Vector((major_2a/2 * math.cos(math.radians(d)), 0, R_val * math.sin(math.radians(d)))) for d in range(0, 365, 5)]
            create_curve(col, "ST_Ellipse", v_pts, p.thick_st_ellipse, "ST_Ellipse", p.color_st_ellipse, True)
        
        if p.show_spheroid:
            mesh = bpy.data.meshes.new("Spheroid")
            obj = bpy.data.objects.new("Spheroid", mesh); col.objects.link(obj)
            import bmesh; bm = bmesh.new(); bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
            for vb in bm.verts: vb.co.x *= (major_2a/2.0); vb.co.y *= R_val; vb.co.z *= R_val
            bmesh.ops.rotate(bm, cent=(0,0,0), matrix=rot_quat.to_matrix(), verts=bm.verts)
            bmesh.ops.translate(bm, vec=Mid, verts=bm.verts)
            bm.to_mesh(mesh); bm.free()
            obj.data.materials.append(get_fixed_material("Spheroid", p.color_spheroid))

        # Wavefronts
        if p.show_wavefronts:
            for i, name in [(0, "Mid"), (-1, "Low"), (1, "Up")]:
                trans = (F2 - Mid) * i
                pts = [get_P_locus(d*5) + trans for d in range(73)]
                create_curve(col, f"WF_{name}", pts, p.thick_wavefronts, "Wavefront", p.color_wavefronts, True)

        # Rays
        if p.ray_mode == '2':
            PtA = get_P_locus(p.base_angle)
            if p.show_ray_a_out: create_curve(col, "ArmA_O", [F1, PtA], p.thick_ray_a_out, "ArmA_Out", p.color_ray_a_out)
            if p.show_ray_a_in:  create_curve(col, "ArmA_I", [PtA, F2], p.thick_ray_a_in, "ArmA_In", p.color_ray_a_in)
            PtB = get_P_locus(p.base_angle + p.arm_b_angle)
            if p.show_ray_b_out: create_curve(col, "ArmB_O", [F1, PtB], p.thick_ray_b_out, "ArmB_Out", p.color_ray_b_out)
            if p.show_ray_b_in:  create_curve(col, "ArmB_I", [PtB, F2], p.thick_ray_b_in, "ArmB_In", p.color_ray_b_in)
        else:
            for i in range(12):
                deg = p.base_angle + (i*30); Pt = get_P_locus(deg)
                if p.show_ray_classic:
                    create_curve(col, f"Cl_{i}_O", [F1, Pt], p.thick_ray_a_out, "Classic_Out", p.color_ray_a_out)
                    create_curve(col, f"Cl_{i}_I", [Pt, F2], p.thick_ray_a_in, "Classic_In", p.color_ray_a_in)
                if p.show_ray_converge:
                    Ps, Pe = get_P_rigid(deg, F1.z), get_P_rigid(deg, F2.z)
                    create_curve(col, f"Cv_{i}_O", [Ps, Mid], p.thick_ray_b_out, "Conv_Out", p.color_ray_b_out)
                    create_curve(col, f"Cv_{i}_I", [Mid, Pe], p.thick_ray_b_in, "Conv_In", p.color_ray_b_in)
                if p.show_ray_ell_conv:
                    PtL, PtU = Pt+(F1-Mid), Pt+(F2-Mid)
                    create_curve(col, f"ElCv_{i}_O", [PtL, Mid], p.thick_ray_b_out, "EllConv_Out", p.color_ray_b_out)
                    create_curve(col, f"ElCv_{i}_I", [Mid, PtU], p.thick_ray_b_in, "EllConv_In", p.color_ray_b_in)

        # Helpers
        if p.show_skeleton:
            for i in range(12):
                Pl = get_P_locus(i*30) + offset
                Ms, Me = Vector((Pl.x+v*(0-Pl.z), Pl.y, 0))-offset, Vector((Pl.x+v*(A-Pl.z), Pl.y, A))-offset
                create_curve(col, f"Sk_{i}", [Ms, Me], p.skel_thick, "Skeleton", p.color_skeleton)
        if p.show_circle_rings:
            for z_v in [F1.z, 0.0, F2.z]:
                create_curve(col, f"T_{z_v}", [get_P_rigid(i*5, z_v) for i in range(73)], p.thick_circle_rings, "Tube", p.color_circle_rings, True)

        context.scene["rel_v5_info"] = f"Velocity: {v:.6f} c\nTarget X: {F2.x+offset.x:.4f}\nA (Invariant): {A:.4f}"
        return {'FINISHED'}

# ------------------------------------------------------------------------
# UI Panels
# ------------------------------------------------------------------------
class VIEW3D_PT_RelSymV5(bpy.types.Panel):
    bl_label = "Symmetric 4D Controller"; bl_idname = "VIEW3D_PT_rel_sym_v5"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
    def draw(self, context):
        layout = self.layout; p = context.scene.rel_sym_v5
        
        layout.operator("wm.copy_full_script", text="全コードをコピー", icon='COPY_ID')

        # Physics Setup
        box = layout.box(); row = box.row(align=True); row.prop(p, "calc_mode", expand=True)
        setup = box.box(); setup.prop(p, "radius")
        if p.calc_mode == 'X': setup.prop(p, "target_x", text="Target X")
        else: setup.prop(p, "velocity", text="Velocity (v/c)")
            
        # Rays Configuration
        cfg = layout.box(); cfg.label(text="Rays Configuration", icon='LIGHT_SUN'); row = cfg.row(align=True); row.prop(p, "ray_mode", text=""); row.prop(p, "base_angle", text="Base")
        
        # Ray Visual Details
        det = layout.box(); det.label(text="Ray Details", icon='NODE_MATERIAL')
        if p.ray_mode == '2':
            for lbl, so, si, to, co, ti, ci, ang in [("Arm A (Ref)", "show_ray_a_out", "show_ray_a_in", "thick_ray_a_out", "color_ray_a_out", "thick_ray_a_in", "color_ray_a_in", None),
                                                     ("Arm B (Adj)", "show_ray_b_out", "show_ray_b_in", "thick_ray_b_out", "color_ray_b_out", "thick_ray_b_in", "color_ray_b_in", "arm_b_angle")]:
                b = det.box(); r = b.row(); r.label(text=lbl); r.prop(p, so, text="", icon='HIDE_OFF'); r.prop(p, si, text="", icon='HIDE_OFF')
                if ang: b.prop(p, ang, text="Offset")
                c = b.column(align=True); r = c.row(align=True); r.prop(p, to, text="Thick Out"); r.prop(p, co, text="")
                r = c.row(align=True); r.prop(p, ti, text="Thick In"); r.prop(p, ci, text="")

        # Wavefronts
        wf = layout.box(); wf.label(text="Wavefronts", icon='SPHERE'); row = wf.row()
        row.prop(p, "show_wavefronts", text="Show Rings"); row.prop(p, "color_wavefronts", text="")
        wf.prop(p, "thick_wavefronts", text="Thickness")

        # Spacetime Structure (ここを強化)
        st = layout.box(); st.label(text="Spacetime Structure", icon='WORLD')
        row = st.row(align=True); row.prop(p, "show_st_ellipse", text="Longit. Ellipse"); row.prop(p, "color_st_ellipse", text="")
        if p.show_st_ellipse: st.prop(p, "thick_st_ellipse", text="Thickness")
        
        row = st.row(align=True); row.prop(p, "show_spheroid", text="3D Spheroid"); row.prop(p, "color_spheroid", text="")

        # Visual Aids (ここを強化)
        vbox = layout.box(); vbox.label(text="Visual Aids", icon='MESH_GRID')
        row = vbox.row(align=True); row.prop(p, "show_circle_rings", text="Tube Rings"); row.prop(p, "color_circle_rings", text="")
        if p.show_circle_rings: vbox.prop(p, "thick_circle_rings", text="Thickness")
        
        row = vbox.row(align=True); row.prop(p, "show_skeleton", text="Skeleton"); row.prop(p, "color_skeleton", text="")
        if p.show_skeleton: vbox.prop(p, "skel_thick", text="Thickness")

        layout.operator("object.draw_spacetime_sym_v5", text="Refresh View", icon='FILE_REFRESH')

class VIEW3D_PT_RelAccounting(bpy.types.Panel):
    bl_label = "Time Accounting"; bl_idname = "VIEW3D_PT_rel_accounting"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
    def draw(self, context):
        layout = self.layout; layout.label(text="Calculated Results", icon='TIME')
        [layout.label(text=l) for l in context.scene.get("rel_v5_info", "").split("\n") if l.strip()]
        layout.operator("wm.copy_rel_info", text="数値をコピー", icon='COPYDOWN')

class VIEW3D_PT_RelLinks(bpy.types.Panel):
    bl_label = "Theory Links"; bl_idname = "VIEW3D_PT_rel_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_rel_url", text=l["label"]); op.url = l["url"]

class VIEW3D_PT_RelSystem(bpy.types.Panel):
    bl_label = "System"; bl_idname = "VIEW3D_PT_rel_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_rel_addon", icon='CANCEL', text="アドオン削除")

# ------------------------------------------------------------------------
# Operators
# ------------------------------------------------------------------------
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_sym_v5; M_START, M_END = "# <BEG" + "IN_DICT>", "# <EN" + "D_DICT>"
        texts = [t.as_string() for t in bpy.data.texts if M_START in t.as_string()]
        if not texts: return {'CANCELLED'}
        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__"): d_str += f'    "{k}": ({val[0]:.3f}, {val[1]:.3f}, {val[2]:.3f}, {val[3]:.3f}),\n'
            elif isinstance(val, float): d_str += f'    "{k}": {val:.4f},\n'
            else: d_str += f'    "{k}": {val},\n'
        d_str += "}\n"
        new_code = texts[0].split(M_START)[0] + M_START + "\n" + d_str + M_END + texts[0].split(M_END)[1]
        context.window_manager.clipboard = f"# {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Version\n" + '\n'.join(new_code.split('\n')[1:])
        return {'FINISHED'}

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

class WM_OT_CopyRelInfo(bpy.types.Operator):
    bl_idname = "wm.copy_rel_info"; bl_label = "Copy Info"
    def execute(self, context): context.window_manager.clipboard = context.scene.get("rel_v5_info", ""); return {'FINISHED'}

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

# ------------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------------
classes = (PG_RelativitySymV5, OBJECT_OT_DrawSpacetimeSymV5, WM_OT_CopyFullScript, WM_OT_OpenRelUrl, WM_OT_CopyRelInfo, WM_OT_RemoveRelAddon, 
           VIEW3D_PT_RelSymV5, VIEW3D_PT_RelAccounting, VIEW3D_PT_RelLinks, VIEW3D_PT_RelSystem)

def register():
    for cls in classes: bpy.utils.register_class(cls)
    bpy.types.Scene.rel_sym_v5 = bpy.props.PointerProperty(type=PG_RelativitySymV5)

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

if __name__ == "__main__": register()
1. 各数値の意味Velocity: $0.680620 \, c$これは移動系の速度です。
光速の約 68% で移動している状態を指します。

Target X: $18.5800$出発点 $F_1(0, 0, 0)$ から光が放たれ、
鏡で反射して戻ってきたとき、
「静止系から見て」どれだけ横($x$方向)に移動した地点に
光が戻ってくるかを示しています。

これが $F_2$ の $x$ 座標です。

A (Invariant): $27.2987$
これがあなたの理論の核となる*
*「時空の不変量(往復の合計時間・距離)」**です。

光が $F_1 \rightarrow P$(反射点)$\rightarrow F_2$ と進む際、
どの角度($y$方向や斜め)に放たれた光であっても、

この合計距離 $A$ は常に一定になります。
# 2026-02-21 05:00:00 Error Fixed & UI Integrated Master goog
# Blender 4.3+ Exclusive

bl_info = {
    "name": "Symmetric Spacetime (v5.43 Fixed)",
    "author": "zionadchat Gemini",
    "version": (5, 43),
    "blender": (4, 3, 0),
    "location": "View3D > Sidebar",
    "description": "Fixed TypeError in Skeleton and restored all UI panels",
    "category": "Physics",
}

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

# ==============================================================================
#  DYNAMIC DEFAULTS
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "calc_mode": "VEL", "velocity": 0.9600, "radius": 10.0000, "target_x": 68.5714,
    "base_angle": 0.0000, "arm_b_angle": 90.0000, "ray_mode": "2",
    "show_ray_classic": True, "show_ray_converge": True, "show_ray_ell_conv": True,
    "show_ray_a_out": True, "show_ray_a_in": True, "show_ray_b_out": True, "show_ray_b_in": True,
    "color_ray_a_out": (1.000, 0.500, 0.000, 1.000), "thick_ray_a_out": 1.41,
    "color_ray_a_in": (0.005, 0.008, 0.271, 1.000), "thick_ray_a_in": 0.73,
    "color_ray_b_out": (0.400, 0.000, 0.000, 1.000), "thick_ray_b_out": 0.80,
    "color_ray_b_in": (0.000, 0.500, 0.000, 1.000), "thick_ray_b_in": 0.30,
    "show_spheroid": True, "color_spheroid": (0.200, 0.100, 0.500, 0.10),
    "show_st_ellipse": True, "color_st_ellipse": (1.000, 1.000, 1.000, 0.80),
    "show_wavefronts": True, "color_wavefronts": (0.160, 0.000, 0.050, 0.40), "thick_wavefronts": 0.40,
    "show_circle_rings": True, "color_circle_rings": (0.100, 0.200, 0.300, 0.30), "thick_circle_rings": 0.03,
    "show_skeleton": True, "color_skeleton": (0.500, 0.500, 0.500, 0.20), "skel_thick": 0.01,
}
# <END_DICT>

TAB_NAME = "Relativity_Sym_v5"
COLLECTION_NAME = "Relativity_Sym_Output"

ADDON_LINKS = (
    {"label": "理論背景: Notion 資料", "url": "<https://www.notion.so/Einstein-from-20260119-main-2edc563be1b080bb94d9f6e5b667fdec>"},
    {"label": "Blender シミュレーション解説", "url": "<https://www.notion.so/blender-deviationtokyo-30c293bfbb2980118c25dfc02259b096>"},
)

# ------------------------------------------------------------------------
# Material Fix
# ------------------------------------------------------------------------
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'
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    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

# ------------------------------------------------------------------------
# Properties & Physics
# ------------------------------------------------------------------------
def update_physics(self, context):
    p = self
    if p.calc_mode == 'X':
        p.velocity = p.target_x / math.sqrt(p.target_x**2 + (2*p.radius)**2)
    else:
        gamma = 1.0 / math.sqrt(max(0.0001, 1.0 - p.velocity**2))
        p.target_x = p.velocity * 2 * p.radius * gamma
    update_view(self, context)

def update_view(self, context):
    try: bpy.ops.object.draw_spacetime_sym_v5('INVOKE_DEFAULT')
    except: pass

class PG_RelativitySymV5(bpy.types.PropertyGroup):
    calc_mode: bpy.props.EnumProperty(items=[('VEL', "Velocity Mode", ""), ('X', "Target X Mode", "")], default='VEL', update=update_view)
    velocity: bpy.props.FloatProperty(name="Velocity", default=0.96, min=0.0, max=0.999, update=update_view)
    target_x: bpy.props.FloatProperty(name="Target X", default=68.57, min=10.0, max=100.0, update=update_physics)
    radius: bpy.props.FloatProperty(name="Radius", default=10.0, min=0.1, update=update_physics)
    base_angle: bpy.props.FloatProperty(name="Base Angle", default=0.0, min=0, max=360, update=update_view)
    arm_b_angle: bpy.props.FloatProperty(name="Arm B Angle", default=90.0, min=0, max=360, update=update_view)
    ray_mode: bpy.props.EnumProperty(items=[('2', "2 Rays", ""), ('12', "12 Rays", "")], default='2', update=update_view)

    show_ray_classic: bpy.props.BoolProperty(name="Classic", default=True, update=update_view)
    show_ray_converge: bpy.props.BoolProperty(name="Converge", default=True, update=update_view)
    show_ray_ell_conv: bpy.props.BoolProperty(name="Ellipse Conv", default=True, update=update_view)

    color_ray_a_out: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=(1, 0.5, 0, 1), update=update_view)
    color_ray_a_in:  bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.005, 0.008, 0.27, 1), update=update_view)
    color_ray_b_out: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.4, 0, 0, 1), update=update_view)
    color_ray_b_in:  bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=(0, 0.5, 0, 1), update=update_view)
    
    thick_ray_a_out: bpy.props.FloatProperty(default=1.41, update=update_view); thick_ray_a_in: bpy.props.FloatProperty(default=0.73, update=update_view)
    thick_ray_b_out: bpy.props.FloatProperty(default=0.8, update=update_view); thick_ray_b_in: bpy.props.FloatProperty(default=0.3, update=update_view)
    
    show_ray_a_out: bpy.props.BoolProperty(default=True, update=update_view); show_ray_a_in: bpy.props.BoolProperty(default=True, update=update_view)
    show_ray_b_out: bpy.props.BoolProperty(default=True, update=update_view); show_ray_b_in: bpy.props.BoolProperty(default=True, update=update_view)

    show_spheroid:   bpy.props.BoolProperty(name="Spheroid Surface", default=True, update=update_view)
    color_spheroid:  bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.2, 0.1, 0.5, 0.1), update=update_view)
    show_st_ellipse: bpy.props.BoolProperty(name="Longit. Ellipse", default=True, update=update_view)
    color_st_ellipse: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=(1, 1, 1, 0.8), update=update_view)
    
    show_wavefronts: bpy.props.BoolProperty(name="Wavefronts (Horiz.)", default=True, update=update_view)
    color_wavefronts: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.16, 0, 0.05, 0.4), update=update_view)
    thick_wavefronts: bpy.props.FloatProperty(default=0.4, update=update_view)

    show_circle_rings: bpy.props.BoolProperty(name="Tube Rings", default=True, update=update_view)
    color_circle_rings: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.1, 0.2, 0.3, 0.3), update=update_view)
    thick_circle_rings: bpy.props.FloatProperty(default=0.03, update=update_view)
    show_skeleton: bpy.props.BoolProperty(name="Skeleton", default=True, update=update_view)
    color_skeleton: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.5, 0.5, 0.5, 0.2), update=update_view)
    skel_thick: bpy.props.FloatProperty(default=0.01, update=update_view)

# ------------------------------------------------------------------------
# Drawing logic
# ------------------------------------------------------------------------
def create_curve(col, name, points, thickness, part_id, color, circular=False):
    curve = bpy.data.curves.new(name, 'CURVE'); curve.dimensions = '3D'
    obj = bpy.data.objects.new(name, curve); 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(part_id, color))
    return obj

class OBJECT_OT_DrawSpacetimeSymV5(bpy.types.Operator):
    bl_idname = "object.draw_spacetime_sym_v5"; bl_label = "Refresh View"; bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        p = context.scene.rel_sym_v5
        v, R_val = p.velocity, p.radius
        gamma = 1.0 / math.sqrt(max(0.0001, 1.0 - v**2))
        A = 2.0 * R_val * gamma
        offset = Vector((v * A / 2.0, 0, A / 2.0))
        F1, Mid, F2 = Vector((0,0,0))-offset, Vector((v*A/2,0,A/2))-offset, Vector((v*A,0,A))-offset

        def get_P_locus(deg):
            rad = math.radians(deg)
            return Vector((gamma*(R_val*math.cos(rad)+v*R_val), R_val*math.sin(rad), gamma*(R_val+v*(R_val*math.cos(rad))))) - offset

        def get_P_rigid(deg, z_v):
            t_p = z_v + offset.z; rad = math.radians(deg)
            return Vector((v*t_p - offset.x + R_val*math.cos(rad), R_val*math.sin(rad), z_v))

        col = bpy.data.collections.get(COLLECTION_NAME) or bpy.data.collections.new(COLLECTION_NAME)
        if COLLECTION_NAME not in context.scene.collection.children: context.scene.collection.children.link(col)
        for obj in col.objects: bpy.data.objects.remove(obj, do_unlink=True)

        # Longitudinal Ellipse & Spheroid
        major_2a = math.sqrt(A**2 * (v**2 + 1) + 4 * R_val**2)
        dir_vec = F2 - F1; rot_quat = Vector((1,0,0)).rotation_difference(dir_vec)

        if p.show_st_ellipse:
            v_pts = [Mid + rot_quat @ Vector((major_2a/2 * math.cos(math.radians(d)), 0, R_val * math.sin(math.radians(d)))) for d in range(0, 365, 5)]
            create_curve(col, "ST_Ellipse", v_pts, 0.05, "ST_Ellipse", p.color_st_ellipse, True)
        
        if p.show_spheroid:
            mesh = bpy.data.meshes.new("Spheroid")
            obj = bpy.data.objects.new("Spheroid", mesh); col.objects.link(obj)
            import bmesh; bm = bmesh.new(); bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
            for vb in bm.verts: vb.co.x *= (major_2a/2.0); vb.co.y *= R_val; vb.co.z *= R_val
            bmesh.ops.rotate(bm, cent=(0,0,0), matrix=rot_quat.to_matrix(), verts=bm.verts)
            bmesh.ops.translate(bm, vec=Mid, verts=bm.verts)
            bm.to_mesh(mesh); bm.free()
            obj.data.materials.append(get_fixed_material("Spheroid", p.color_spheroid))

        # Wavefronts
        if p.show_wavefronts:
            for i, name in [(0, "Mid"), (-1, "Low"), (1, "Up")]:
                trans = (F2 - Mid) * i
                pts = [get_P_locus(d*5) + trans for d in range(73)]
                create_curve(col, f"WF_{name}", pts, p.thick_wavefronts, "Wavefront", p.color_wavefronts, True)

        # Rays
        if p.ray_mode == '2':
            PtA = get_P_locus(p.base_angle)
            if p.show_ray_a_out: create_curve(col, "ArmA_O", [F1, PtA], p.thick_ray_a_out, "ArmA_Out", p.color_ray_a_out)
            if p.show_ray_a_in:  create_curve(col, "ArmA_I", [PtA, F2], p.thick_ray_a_in, "ArmA_In", p.color_ray_a_in)
            PtB = get_P_locus(p.base_angle + p.arm_b_angle)
            if p.show_ray_b_out: create_curve(col, "ArmB_O", [F1, PtB], p.thick_ray_b_out, "ArmB_Out", p.color_ray_b_out)
            if p.show_ray_b_in:  create_curve(col, "ArmB_I", [PtB, F2], p.thick_ray_b_in, "ArmB_In", p.color_ray_b_in)
        else:
            for i in range(12):
                deg = p.base_angle + (i*30); Pt = get_P_locus(deg)
                if p.show_ray_classic:
                    create_curve(col, f"Cl_{i}_O", [F1, Pt], p.thick_ray_a_out, "Classic_Out", p.color_ray_a_out)
                    create_curve(col, f"Cl_{i}_I", [Pt, F2], p.thick_ray_a_in, "Classic_In", p.color_ray_a_in)
                if p.show_ray_converge:
                    Ps, Pe = get_P_rigid(deg, F1.z), get_P_rigid(deg, F2.z)
                    create_curve(col, f"Cv_{i}_O", [Ps, Mid], p.thick_ray_b_out, "Conv_Out", p.color_ray_b_out)
                    create_curve(col, f"Cv_{i}_I", [Mid, Pe], p.thick_ray_b_in, "Conv_In", p.color_ray_b_in)
                if p.show_ray_ell_conv:
                    PtL, PtU = Pt+(F1-Mid), Pt+(F2-Mid)
                    create_curve(col, f"ElCv_{i}_O", [PtL, Mid], p.thick_ray_b_out, "EllConv_Out", p.color_ray_b_out)
                    create_curve(col, f"ElCv_{i}_I", [Mid, PtU], p.thick_ray_b_in, "EllConv_In", p.color_ray_b_in)

        # Helpers (ERROR FIXED: added point list)
        if p.show_skeleton:
            for i in range(12):
                Pl = get_P_locus(i*30) + offset
                Ms, Me = Vector((Pl.x+v*(0-Pl.z), Pl.y, 0))-offset, Vector((Pl.x+v*(A-Pl.z), Pl.y, A))-offset
                create_curve(col, f"Sk_{i}", [Ms, Me], p.skel_thick, "Skeleton", p.color_skeleton)
        if p.show_circle_rings:
            for z_v in [F1.z, 0.0, F2.z]:
                create_curve(col, f"T_{z_v}", [get_P_rigid(i*5, z_v) for i in range(73)], p.thick_circle_rings, "Tube", p.color_circle_rings, True)

        context.scene["rel_v5_info"] = f"Velocity: {v:.6f} c\nTarget X: {F2.x+offset.x:.4f}\nA (Invariant): {A:.4f}"
        return {'FINISHED'}

# ------------------------------------------------------------------------
# UI Panels
# ------------------------------------------------------------------------
class VIEW3D_PT_RelSymV5(bpy.types.Panel):
    bl_label = "Symmetric 4D Controller"; bl_idname = "VIEW3D_PT_rel_sym_v5"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
    def draw(self, context):
        layout = self.layout; p = context.scene.rel_sym_v5
        
        # 1. Config Export (Top)
        layout.operator("wm.copy_full_script", text="全コードをコピー", icon='COPY_ID')

        # 2. Physics Setup
        box = layout.box(); row = box.row(align=True); row.prop(p, "calc_mode", expand=True)
        setup = box.box(); setup.prop(p, "radius")
        if p.calc_mode == 'X': setup.prop(p, "target_x", text="Target X (10-100)")
        else: setup.prop(p, "velocity", text="Velocity (v/c)")
            
        # 3. Rays Configuration
        cfg = layout.box(); row = cfg.row(align=True); row.prop(p, "ray_mode", text=""); row.prop(p, "base_angle", text="Base")
        if p.ray_mode == '12':
            sub = cfg.box(); sub.label(text="Pattern Selection"); sub.prop(p, "show_ray_classic"); sub.prop(p, "show_ray_converge"); sub.prop(p, "show_ray_ell_conv")
            
        # 4. Ray Visual Details
        det = layout.box(); det.label(text="Ray Details (Color & Alpha)", icon='LIGHT_SUN')
        if p.ray_mode == '2':
            for lbl, so, si, to, co, ti, ci, ang in [("Arm A (Ref)", "show_ray_a_out", "show_ray_a_in", "thick_ray_a_out", "color_ray_a_out", "thick_ray_a_in", "color_ray_a_in", None),
                                                     ("Arm B (Adj)", "show_ray_b_out", "show_ray_b_in", "thick_ray_b_out", "color_ray_b_out", "thick_ray_b_in", "color_ray_b_in", "arm_b_angle")]:
                b = det.box(); r = b.row(); r.label(text=lbl); r.prop(p, so, text="", icon='HIDE_OFF'); r.prop(p, si, text="", icon='HIDE_OFF')
                if ang: b.prop(p, ang, text="Offset")
                c = b.column(align=True); r = c.row(align=True); r.prop(p, to, text="Out"); r.prop(p, co, text="")
                r = c.row(align=True); r.prop(p, ti, text="In"); r.prop(p, ci, text="")
        else:
            r12 = det.box(); col = r12.column(align=True); col.prop(p, "color_ray_a_out", text="Classic Col"); col.prop(p, "color_ray_b_out", text="Conv Col")

        # 5. Wavefronts & Spacetime
        wf = layout.box(); wf.label(text="Wavefronts (Horizontal Rings)", icon='SPHERE'); row = wf.row()
        row.prop(p, "show_wavefronts", text="Show 3 Rings"); row.prop(p, "color_wavefronts", text=""); wf.prop(p, "thick_wavefronts", text="Thick")

        st = layout.box(); st.label(text="Spacetime Structure", icon='WORLD'); row = st.row()
        row.prop(p, "show_st_ellipse", text="Longit. Ellipse"); row.prop(p, "color_st_ellipse", text="")
        row = st.row(); row.prop(p, "show_spheroid", text="3D Spheroid"); row.prop(p, "color_spheroid", text="")

        # 6. Visual Aids
        vbox = layout.box(); vbox.label(text="Visual Aids", icon='MESH_GRID')
        row = vbox.row(align=True); row.prop(p, "show_circle_rings", text="Tube Rings"); row.prop(p, "color_circle_rings", text="")
        row = vbox.row(align=True); row.prop(p, "show_skeleton", text="Skeleton"); row.prop(p, "color_skeleton", text="")

        layout.operator("object.draw_spacetime_sym_v5", text="Refresh View", icon='FILE_REFRESH')

class VIEW3D_PT_RelAccounting(bpy.types.Panel):
    bl_label = "Time Accounting"; bl_idname = "VIEW3D_PT_rel_accounting"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
    def draw(self, context):
        layout = self.layout; layout.label(text="Calculated Results", icon='TIME')
        [layout.label(text=l) for l in context.scene.get("rel_v5_info", "").split("\n") if l.strip()]
        layout.operator("wm.copy_rel_info", text="数値をコピー", icon='COPYDOWN')

class VIEW3D_PT_RelLinks(bpy.types.Panel):
    bl_label = "Theory Links"; bl_idname = "VIEW3D_PT_rel_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_rel_url", text=l["label"]); op.url = l["url"]

class VIEW3D_PT_RelSystem(bpy.types.Panel):
    bl_label = "System"; bl_idname = "VIEW3D_PT_rel_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_rel_addon", icon='CANCEL', text="アドオン削除")

# ------------------------------------------------------------------------
# Operators
# ------------------------------------------------------------------------
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_sym_v5; M_START, M_END = "# <BEG" + "IN_DICT>", "# <EN" + "D_DICT>"
        texts = [t.as_string() for t in bpy.data.texts if M_START in t.as_string()]
        if not texts: return {'CANCELLED'}
        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__"): d_str += f'    "{k}": ({val[0]:.3f}, {val[1]:.3f}, {val[2]:.3f}, {val[3]:.3f}),\n'
            elif isinstance(val, float): d_str += f'    "{k}": {val:.4f},\n'
            else: d_str += f'    "{k}": {val},\n'
        d_str += "}\n"
        new_code = texts[0].split(M_START)[0] + M_START + "\n" + d_str + M_END + texts[0].split(M_END)[1]
        context.window_manager.clipboard = f"# {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Version\n" + '\n'.join(new_code.split('\n')[1:])
        return {'FINISHED'}

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

class WM_OT_CopyRelInfo(bpy.types.Operator):
    bl_idname = "wm.copy_rel_info"; bl_label = "Copy Info"
    def execute(self, context): context.window_manager.clipboard = context.scene.get("rel_v5_info", ""); return {'FINISHED'}

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

# ------------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------------
classes = (PG_RelativitySymV5, OBJECT_OT_DrawSpacetimeSymV5, WM_OT_CopyFullScript, WM_OT_OpenRelUrl, WM_OT_CopyRelInfo, WM_OT_RemoveRelAddon, 
           VIEW3D_PT_RelSymV5, VIEW3D_PT_RelAccounting, VIEW3D_PT_RelLinks, VIEW3D_PT_RelSystem)

def register():
    for cls in classes: bpy.utils.register_class(cls)
    bpy.types.Scene.rel_sym_v5 = bpy.props.PointerProperty(type=PG_RelativitySymV5)

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

if __name__ == "__main__": register()
# 2026-02-20 23:00:00 Prominent Mode Switch & Target X Fix
# Blender 4.3+ Exclusive

bl_info = {
    "name": "Symmetric Spacetime (Target X Mode UI Fix)",
    "author": "zionadchat Gemini",
    "version": (5, 35),
    "blender": (4, 3, 0),
    "location": "View3D > Sidebar",
    "description": "Clear UI for Target X calculation (10-100) and triple ellipses",
    "category": "Physics",
}

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

# ==============================================================================
#  DYNAMIC DEFAULTS DICTIONARY
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "calc_mode": "VEL",
    "velocity": 0.0000,
    "radius": 10.0000,
    "target_x": 10.0000,
    "base_angle": 0.0000,
    "arm_b_angle": 90.0000,
    "ray_mode": "2",
    "show_ray_classic": True,
    "show_ray_converge": True,
    "show_ray_ell_conv": True,
    "color_ray_a_out": (1.000, 0.500, 0.000, 1.000), "thick_ray_a_out": 1.4100,
    "color_ray_a_in": (0.005, 0.008, 0.271, 1.000), "thick_ray_a_in": 0.7300,
    "color_ray_b_out": (0.400, 0.000, 0.000, 1.000), "thick_ray_b_out": 0.8000,
    "color_ray_b_in": (0.000, 0.500, 0.000, 1.000), "thick_ray_b_in": 0.3000,
    "show_ellipse_ring": True, "color_ellipse_ring": (0.160, 0.000, 0.050, 0.600), "thick_ellipse_ring": 0.4000,
    "show_circle_rings": True, "color_circle_rings": (0.100, 0.200, 0.300, 0.300), "thick_circle_rings": 0.0300,
    "show_skeleton": True, "color_skeleton": (0.500, 0.500, 0.500, 0.200), "skel_thick": 0.0100,
}
# <END_DICT>

TAB_NAME = "Relativity_Sym_v5"
COLLECTION_NAME = "Relativity_Sym_Output"

ADDON_LINKS = (
    {"label": "単純トリック Einstein 氏の さぼり", "url": "<https://www.notion.so/Einstein-from-20260119-main-2edc563be1b080bb94d9f6e5b667fdec>"},
    {"label": "最新版 マイケルソン干渉計blender deviationtokyo", "url": "<https://www.notion.so/blender-deviationtokyo-30c293bfbb2980118c25dfc02259b096>"},
)

# ------------------------------------------------------------------------
# Calculation Logic
# ------------------------------------------------------------------------
def update_physics(self, context):
    p = self
    if p.calc_mode == 'X':
        # x = v * (2R * gamma) => v = x / sqrt(x^2 + (2R)^2)
        x = p.target_x
        R = p.radius
        p.velocity = x / math.sqrt(x**2 + (2*R)**2)
    else:
        # v から x を計算して同期 (x = v * 2R * gamma)
        gamma = 1.0 / math.sqrt(max(0.0001, 1.0 - p.velocity**2))
        p.target_x = p.velocity * 2 * p.radius * gamma
    
    update_view(self, context)

def update_view(self, context):
    try: bpy.ops.object.draw_spacetime_sym_v5('INVOKE_DEFAULT')
    except: pass

# ------------------------------------------------------------------------
# Property Group
# ------------------------------------------------------------------------
class PG_RelativitySymV5(bpy.types.PropertyGroup):
    calc_mode: bpy.props.EnumProperty(
        name="Calc Mode",
        items=[('VEL', "Velocity Mode", "Directly set v/c"), 
               ('X', "Target X Mode", "Calculate v from end position")],
        default=CURRENT_DEFAULTS["calc_mode"], update=update_view)

    velocity: bpy.props.FloatProperty(name="Velocity (v/c)", default=CURRENT_DEFAULTS["velocity"], min=0.0, max=0.999, update=update_view)
    target_x: bpy.props.FloatProperty(name="Target X", default=CURRENT_DEFAULTS["target_x"], min=10.0, max=100.0, update=update_physics)
    radius: bpy.props.FloatProperty(name="Radius (R)", default=CURRENT_DEFAULTS["radius"], min=0.1, update=update_physics)
    
    base_angle: bpy.props.FloatProperty(name="Base Angle", default=CURRENT_DEFAULTS["base_angle"], min=0, max=360, update=update_view)
    arm_b_angle: bpy.props.FloatProperty(name="Arm B Angle", default=CURRENT_DEFAULTS["arm_b_angle"], min=0, max=360, update=update_view)
    ray_mode: bpy.props.EnumProperty(name="Ray Mode", items=[('2', "2 Rays", ""), ('12', "12 Rays", "")], default=CURRENT_DEFAULTS["ray_mode"], update=update_view)

    show_ray_classic: bpy.props.BoolProperty(name="Classic", default=True, update=update_view)
    show_ray_converge: bpy.props.BoolProperty(name="Converge", default=True, update=update_view)
    show_ray_ell_conv: bpy.props.BoolProperty(name="Ellipse Conv", default=True, update=update_view)

    color_ray_a_out: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["color_ray_a_out"], update=update_view)
    thick_ray_a_out: bpy.props.FloatProperty(default=CURRENT_DEFAULTS["thick_ray_a_out"], update=update_view)
    color_ray_a_in: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["color_ray_a_in"], update=update_view)
    thick_ray_a_in: bpy.props.FloatProperty(default=CURRENT_DEFAULTS["thick_ray_a_in"], update=update_view)

    show_ray_a_out: bpy.props.BoolProperty(default=True, update=update_view); show_ray_a_in: bpy.props.BoolProperty(default=True, update=update_view)
    show_ray_b_out: bpy.props.BoolProperty(default=True, update=update_view); show_ray_b_in: bpy.props.BoolProperty(default=True, update=update_view)

    color_ray_b_out: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["color_ray_b_out"], update=update_view)
    thick_ray_b_out: bpy.props.FloatProperty(default=CURRENT_DEFAULTS["thick_ray_b_out"], update=update_view)
    color_ray_b_in: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["color_ray_b_in"], update=update_view)
    thick_ray_b_in: bpy.props.FloatProperty(default=CURRENT_DEFAULTS["thick_ray_b_in"], update=update_view)

    show_ellipse_ring: bpy.props.BoolProperty(name="Ellipses", default=True, update=update_view)
    color_ellipse_ring: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["color_ellipse_ring"], update=update_view)
    thick_ellipse_ring: bpy.props.FloatProperty(default=CURRENT_DEFAULTS["thick_ellipse_ring"], update=update_view)

    show_circle_rings: bpy.props.BoolProperty(name="Tube Rings", default=True, update=update_view)
    color_circle_rings: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["color_circle_rings"], update=update_view)
    thick_circle_rings: bpy.props.FloatProperty(default=CURRENT_DEFAULTS["thick_circle_rings"], update=update_view)

    show_skeleton: bpy.props.BoolProperty(name="Skeleton", default=True, update=update_view)
    color_skeleton: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["color_skeleton"], update=update_view)
    skel_thick: bpy.props.FloatProperty(default=CURRENT_DEFAULTS["skel_thick"], update=update_view)

# ------------------------------------------------------------------------
# Drawing Logic
# ------------------------------------------------------------------------
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'
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    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 = 2.0
    mat.diffuse_color = color
    return mat

def create_curve(col, name, points, thickness, part_id, color, circular=False):
    curve = bpy.data.curves.new(name, 'CURVE')
    curve.dimensions = '3D'
    obj = bpy.data.objects.new(name, curve)
    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(part_id, color))
    return obj

class OBJECT_OT_DrawSpacetimeSymV5(bpy.types.Operator):
    bl_idname = "object.draw_spacetime_sym_v5"
    bl_label = "Draw Spacetime"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        p = context.scene.rel_sym_v5
        v, R_val = p.velocity, p.radius
        gamma = 1.0 / math.sqrt(max(0.0001, 1.0 - v**2))
        A = 2.0 * R_val * gamma
        offset = Vector((v * A / 2.0, 0, A / 2.0))
        E_start, Mid_center, End = Vector((0,0,0))-offset, Vector((v*A/2,0,A/2))-offset, Vector((v*A,0,A))-offset

        def get_P_locus(deg):
            rad = math.radians(deg)
            return Vector((gamma*(R_val*math.cos(rad)+v*R_val), R_val*math.sin(rad), gamma*(R_val+v*(R_val*math.cos(rad))))) - offset

        def get_P_rigid(deg, z_v):
            t_p = z_v + offset.z
            rad = math.radians(deg)
            return Vector((v*t_p - offset.x + R_val*math.cos(rad), R_val*math.sin(rad), z_v))

        col = bpy.data.collections.get(COLLECTION_NAME) or bpy.data.collections.new(COLLECTION_NAME)
        if COLLECTION_NAME not in context.scene.collection.children: context.scene.collection.children.link(col)
        for obj in col.objects: bpy.data.objects.remove(obj, do_unlink=True)

        if p.show_ellipse_ring:
            for i, name in [(0, "Mid"), (-1, "Low"), (1, "Up")]:
                trans = (End - Mid_center) * i
                pts = [get_P_locus(d*5) + trans for d in range(73)]
                create_curve(col, f"Ell_{name}", pts, p.thick_ellipse_ring, "Ellipse", p.color_ellipse_ring, True)

        info = f"Velocity: {v:.6f} c\n"
        info += f"Calculated End X: {End.x + offset.x:.4f}\n"
        info += f"A Constant: {A:.4f}\n"

        if p.ray_mode == '2':
            for d_off, name, co, to, ci, ti, so, si, p_id in [(0, "ArmA", p.color_ray_a_out, p.thick_ray_a_out, p.color_ray_a_in, p.thick_ray_a_in, p.show_ray_a_out, p.show_ray_a_in, "ArmA"),
                                                             (p.arm_b_angle, "ArmB", p.color_ray_b_out, p.thick_ray_b_out, p.color_ray_b_in, p.thick_ray_b_in, p.show_ray_b_out, p.show_ray_b_in, "ArmB")]:
                Pt = get_P_locus(p.base_angle + d_off)
                info += f"{name}: {Pt.z+offset.z:.3f} + {A-(Pt.z+offset.z):.3f} = {A:.3f}\n"
                if so: create_curve(col, name+"_O", [E_start, Pt], to, p_id+"_Out", co)
                if si: create_curve(col, name+"_I", [Pt, End], ti, p_id+"_In", ci)
        else:
            for i in range(12):
                deg = p.base_angle + (i*30)
                if p.show_ray_classic:
                    Pt = get_P_locus(deg)
                    create_curve(col, f"Cl_{i}_O", [E_start, Pt], p.thick_ray_a_out, "Classic_Out", p.color_ray_a_out)
                    create_curve(col, f"Cl_{i}_I", [Pt, End], p.thick_ray_a_in, "Classic_In", p.color_ray_a_in)
                if p.show_ray_converge:
                    Ps, Pe = get_P_rigid(deg, E_start.z), get_P_rigid(deg, End.z)
                    create_curve(col, f"Cv_{i}_O", [Ps, Mid_center], p.thick_ray_b_out, "Conv_Out", p.color_ray_b_out)
                    create_curve(col, f"Cv_{i}_I", [Mid_center, Pe], p.thick_ray_b_in, "Conv_In", p.color_ray_b_in)
                if p.show_ray_ell_conv:
                    PtM = get_P_locus(deg)
                    PtL, PtU = PtM + (E_start - Mid_center), PtM + (End - Mid_center)
                    create_curve(col, f"ElCv_{i}_O", [PtL, Mid_center], p.thick_ray_b_out, "EllConv_Out", p.color_ray_b_out)
                    create_curve(col, f"ElCv_{i}_I", [Mid_center, PtU], p.thick_ray_b_in, "EllConv_In", p.color_ray_b_in)

        if p.show_skeleton:
            for i in range(12):
                Pl = get_P_locus(i*30) + offset
                Ms, Me = Vector((Pl.x+v*(0-Pl.z), Pl.y, 0))-offset, Vector((Pl.x+v*(A-Pl.z), Pl.y, A))-offset
                create_curve(col, f"Sk_{i}", [Ms, Me], p.skel_thick, "Skeleton", p.color_skeleton)

        if p.show_circle_rings:
            for z_v in [E_start.z, 0.0, End.z]:
                create_curve(col, f"T_{z_v}", [get_P_rigid(i*5, z_v) for i in range(73)], p.thick_circle_rings, "Tube", p.color_circle_rings, True)

        context.scene["rel_v5_info"] = info
        return {'FINISHED'}

# ------------------------------------------------------------------------
# UI & Operators
# ------------------------------------------------------------------------
class VIEW3D_PT_RelSymV5(bpy.types.Panel):
    bl_label = "Symmetric 4D Controller"; bl_idname = "VIEW3D_PT_rel_sym_v5"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
    def draw(self, context):
        layout = self.layout; p = context.scene.rel_sym_v5
        
        box = layout.box()
        box.label(text="Physics Control Mode", icon='SETTINGS')
        row = box.row(align=True)
        row.prop(p, "calc_mode", expand=True) # 文字ボタンとして表示
        
        setup = box.box()
        setup.prop(p, "radius")
        if p.calc_mode == 'X':
            setup.prop(p, "target_x", text="Target X (10-100)")
            setup.label(text=f"Calculated v: {p.velocity:.6f} c", icon='FORWARD')
        else:
            setup.prop(p, "velocity", text="Velocity (v/c)")
            setup.label(text=f"Calculated X: {p.target_x:.4f}", icon='TRACKING')
            
        cfg = layout.box(); row = cfg.row(align=True); row.prop(p, "ray_mode", text=""); row.prop(p, "base_angle", text="Base")
        if p.ray_mode == '12':
            sub = cfg.box(); sub.label(text="Pattern Selection"); sub.prop(p, "show_ray_classic"); sub.prop(p, "show_ray_converge"); sub.prop(p, "show_ray_ell_conv")
            
        res = layout.box(); res.label(text="Time Accounting", icon='TIME'); [res.label(text=l) for l in context.scene.get("rel_v5_info", "").split("\n") if l.strip()]; res.operator("wm.copy_rel_info", text="数値をコピー", icon='COPYDOWN')
        
        if p.ray_mode == '2':
            for lbl, s_out, s_in, t_o, c_o, t_i, c_i, ang in [("Arm A (Reference)", "show_ray_a_out", "show_ray_a_in", "thick_ray_a_out", "color_ray_a_out", "thick_ray_a_in", "color_ray_a_in", None),
                                                           ("Arm B (Adjustable)", "show_ray_b_out", "show_ray_b_in", "thick_ray_b_out", "color_ray_b_out", "thick_ray_b_in", "color_ray_b_in", "arm_b_angle")]:
                b = layout.box(); r = b.row(); r.label(text=lbl); r.prop(p, s_out, text="", icon='HIDE_OFF' if getattr(p, s_out) else 'HIDE_ON'); r.prop(p, s_in, text="", icon='HIDE_OFF' if getattr(p, s_in) else 'HIDE_ON')
                if ang: b.prop(p, ang, text="Relative Angle")
                col = b.column(align=True); r = col.row(align=True); r.prop(p, t_o, text="Out"); r.prop(p, c_o, text="")
                r = col.row(align=True); r.prop(p, t_i, text="In"); r.prop(p, c_i, text="")
        else:
            r12 = layout.box(); r12.label(text="12-Ray Color / Alpha (0-1)"); col = r12.column(align=True)
            col.label(text="Classic / EllConv Color"); row = col.row(align=True); row.prop(p, "thick_ray_a_out", text="Ray"); row.prop(p, "color_ray_a_out", text="")
            col.separator(); col.label(text="Converge Pattern Color"); row = col.row(align=True); row.prop(p, "thick_ray_b_out", text="Ray"); row.prop(p, "color_ray_b_out", text="")
            
        v_box = layout.box(); v_box.label(text="Visual Aids (Alpha 0-1)", icon='MESH_GRID'); col = v_box.column(align=True)
        for s, t, c, name in [("show_ellipse_ring", "thick_ellipse_ring", "color_ellipse_ring", "Triple Ellipses"), ("show_circle_rings", "thick_circle_rings", "color_circle_rings", "Tube Rings"), ("show_skeleton", "skel_thick", "color_skeleton", "Skeleton")]:
            row = col.row(align=True); row.prop(p, s, text=name); row.prop(p, t, text=""); row.prop(p, c, text="")
        layout.operator("object.draw_spacetime_sym_v5", text="Refresh View", icon='FILE_REFRESH')

class VIEW3D_PT_RelExport(bpy.types.Panel):
    bl_label = "Config Export"; bl_idname = "VIEW3D_PT_rel_export"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_order = 0
    def draw(self, context): self.layout.operator("wm.copy_full_script", text="全コードをコピー", icon='COPY_ID')

class VIEW3D_PT_RelLinks(bpy.types.Panel):
    bl_label = "Links"; bl_idname = "VIEW3D_PT_rel_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_rel_url", text=l["label"]); op.url = l["url"]

class VIEW3D_PT_RelSystem(bpy.types.Panel):
    bl_label = "System"; bl_idname = "VIEW3D_PT_rel_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_rel_addon", icon='CANCEL', text="アドオンを無効化")

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

class WM_OT_CopyRelInfo(bpy.types.Operator):
    bl_idname = "wm.copy_rel_info"; bl_label = "Copy Info"
    def execute(self, context): context.window_manager.clipboard = context.scene.get("rel_v5_info", ""); return {'FINISHED'}

class WM_OT_RemoveRelAddon(bpy.types.Operator):
    bl_idname = "wm.remove_rel_addon"; bl_label = "Remove"
    def execute(self, context): unregister(); 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_sym_v5; M_START, M_END = "# <BEG" + "IN_DICT>", "# <EN" + "D_DICT>"
        texts = [t.as_string() for t in bpy.data.texts if M_START in t.as_string()]
        if not texts: return {'CANCELLED'}
        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__"): d_str += f'    "{k}": ({val[0]:.3f}, {val[1]:.3f}, {val[2]:.3f}, {val[3]:.3f}),\n'
            elif isinstance(val, float): d_str += f'    "{k}": {val:.4f},\n'
            else: d_str += f'    "{k}": {val},\n'
        d_str += "}\n"
        new_code = texts[0].split(M_START)[0] + M_START + "\n" + d_str + M_END + texts[0].split(M_END)[1]
        context.window_manager.clipboard = f"# {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Version\n" + '\n'.join(new_code.split('\n')[1:])
        return {'FINISHED'}

classes = (PG_RelativitySymV5, OBJECT_OT_DrawSpacetimeSymV5, WM_OT_OpenRelUrl, WM_OT_CopyRelInfo, WM_OT_CopyFullScript, WM_OT_RemoveRelAddon, VIEW3D_PT_RelExport, VIEW3D_PT_RelSymV5, VIEW3D_PT_RelLinks, VIEW3D_PT_RelSystem)

def register():
    for cls in classes: bpy.utils.register_class(cls)
    bpy.types.Scene.rel_sym_v5 = bpy.props.PointerProperty(type=PG_RelativitySymV5)

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

if __name__ == "__main__": register()
# 2026-02-20 15:35:01 Version
# Blender 4.3+ Exclusive

bl_info = {
    "name": "Symmetric Spacetime (Alpha Fixed)",
    "author": "zionadchat Gemini",
    "version": (5, 33),
    "blender": (4, 3, 0),
    "location": "View3D > Sidebar",
    "description": "Strict 0-1 Alpha range and triple ellipse patterns",
    "category": "Physics",
}

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

# ==============================================================================
#  DYNAMIC DEFAULTS DICTIONARY
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "velocity": 0.0000,
    "radius": 10.0000,
    "base_angle": 0.0000,
    "arm_b_angle": 90.0000,
    "ray_mode": "2",
    "show_ray_classic": True,
    "show_ray_converge": True,
    "show_ray_ell_conv": True,
    "show_ray_a_out": True,
    "show_ray_a_in": True,
    "show_ray_b_out": True,
    "show_ray_b_in": True,
    "color_ray_a_out": (1.000, 0.500, 0.000, 1.000),
    "thick_ray_a_out": 1.4100,
    "color_ray_a_in": (0.005, 0.008, 0.271, 1.000),
    "thick_ray_a_in": 0.7300,
    "color_ray_b_out": (0.400, 0.000, 0.000, 1.000),
    "thick_ray_b_out": 0.8000,
    "color_ray_b_in": (0.000, 0.500, 0.000, 1.000),
    "thick_ray_b_in": 0.3000,
    "show_ellipse_ring": True,
    "color_ellipse_ring": (0.160, 0.000, 0.050, 0.600),
    "thick_ellipse_ring": 0.4000,
    "show_circle_rings": True,
    "color_circle_rings": (0.100, 0.200, 0.300, 0.300),
    "thick_circle_rings": 0.0300,
    "show_skeleton": True,
    "color_skeleton": (0.500, 0.500, 0.500, 0.200),
    "skel_thick": 0.0100,
}
# <END_DICT>

TAB_NAME = "Relativity_Sym_v5"
COLLECTION_NAME = "Relativity_Sym_Output"

ADDON_LINKS = (
    {"label": "単純トリック Einstein 氏の さぼり", "url": "<https://www.notion.so/Einstein-from-20260119-main-2edc563be1b080bb94d9f6e5b667fdec>"},
    {"label": "最新版 マイケルソン干渉計blender deviationtokyo", "url": "<https://www.notion.so/blender-deviationtokyo-30c293bfbb2980118c25dfc02259b096>"},
)

def update_view(self, context):
    try: bpy.ops.object.draw_spacetime_sym_v5('INVOKE_DEFAULT')
    except: pass

# ------------------------------------------------------------------------
# Material Logic
# ------------------------------------------------------------------------
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'
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    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 = 2.0
    mat.diffuse_color = color
    return mat

# ------------------------------------------------------------------------
# Property Group (Alpha 0-1 Strict Range)
# ------------------------------------------------------------------------
class PG_RelativitySymV5(bpy.types.PropertyGroup):
    velocity: bpy.props.FloatProperty(name="Velocity", default=CURRENT_DEFAULTS["velocity"], min=0.0, max=0.99, update=update_view)
    radius: bpy.props.FloatProperty(name="Radius", default=CURRENT_DEFAULTS["radius"], min=0.1, update=update_view)
    base_angle: bpy.props.FloatProperty(name="Base Angle", default=CURRENT_DEFAULTS["base_angle"], min=0, max=360, update=update_view)
    arm_b_angle: bpy.props.FloatProperty(name="Arm B Angle", default=CURRENT_DEFAULTS["arm_b_angle"], min=0, max=360, update=update_view)
    ray_mode: bpy.props.EnumProperty(name="Ray Mode", items=[('2', "2 Rays", ""), ('12', "12 Rays", "")], default=CURRENT_DEFAULTS["ray_mode"], update=update_view)

    show_ray_classic: bpy.props.BoolProperty(name="Classic", default=True, update=update_view)
    show_ray_converge: bpy.props.BoolProperty(name="Converge", default=True, update=update_view)
    show_ray_ell_conv: bpy.props.BoolProperty(name="Ellipse Conv", default=True, update=update_view)

    # All Colors/Alpha set to min=0.0, max=1.0
    show_ray_a_out: bpy.props.BoolProperty(default=True, update=update_view); show_ray_a_in: bpy.props.BoolProperty(default=True, update=update_view)
    color_ray_a_out: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["color_ray_a_out"], update=update_view)
    thick_ray_a_out: bpy.props.FloatProperty(default=1.41, update=update_view)
    color_ray_a_in: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["color_ray_a_in"], update=update_view)
    thick_ray_a_in: bpy.props.FloatProperty(default=0.73, update=update_view)

    show_ray_b_out: bpy.props.BoolProperty(default=True, update=update_view); show_ray_b_in: bpy.props.BoolProperty(default=True, update=update_view)
    color_ray_b_out: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["color_ray_b_out"], update=update_view)
    thick_ray_b_out: bpy.props.FloatProperty(default=0.8, update=update_view)
    color_ray_b_in: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["color_ray_b_in"], update=update_view)
    thick_ray_b_in: bpy.props.FloatProperty(default=0.3, update=update_view)

    show_ellipse_ring: bpy.props.BoolProperty(name="Ellipses", default=True, update=update_view)
    color_ellipse_ring: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["color_ellipse_ring"], update=update_view)
    thick_ellipse_ring: bpy.props.FloatProperty(default=0.4, update=update_view)

    show_circle_rings: bpy.props.BoolProperty(name="Tube Rings", default=True, update=update_view)
    color_circle_rings: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["color_circle_rings"], update=update_view)
    thick_circle_rings: bpy.props.FloatProperty(default=0.03, update=update_view)

    show_skeleton: bpy.props.BoolProperty(name="Skeleton", default=True, update=update_view)
    color_skeleton: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["color_skeleton"], update=update_view)
    skel_thick: bpy.props.FloatProperty(default=0.01, update=update_view)

# ------------------------------------------------------------------------
# Drawing Logic
# ------------------------------------------------------------------------
def create_curve(col, name, points, thickness, part_id, color, circular=False):
    curve = bpy.data.curves.new(name, 'CURVE')
    curve.dimensions = '3D'
    obj = bpy.data.objects.new(name, curve)
    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(part_id, color))
    return obj

class OBJECT_OT_DrawSpacetimeSymV5(bpy.types.Operator):
    bl_idname = "object.draw_spacetime_sym_v5"
    bl_label = "Draw Spacetime"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        p = context.scene.rel_sym_v5
        v, R_val = p.velocity, p.radius
        gamma = 1.0 / math.sqrt(max(0.0001, 1.0 - v**2))
        A = 2.0 * R_val * gamma
        offset = Vector((v * A / 2.0, 0, A / 2.0))
        E_start, Mid_center, End = Vector((0,0,0))-offset, Vector((v*A/2,0,A/2))-offset, Vector((v*A,0,A))-offset

        def get_P_locus(deg):
            rad = math.radians(deg)
            return Vector((gamma*(R_val*math.cos(rad)+v*R_val), R_val*math.sin(rad), gamma*(R_val+v*(R_val*math.cos(rad))))) - offset

        def get_P_rigid(deg, z_v):
            t_p = z_v + offset.z
            rad = math.radians(deg)
            return Vector((v*t_p - offset.x + R_val*math.cos(rad), R_val*math.sin(rad), z_v))

        col = bpy.data.collections.get(COLLECTION_NAME) or bpy.data.collections.new(COLLECTION_NAME)
        if COLLECTION_NAME not in context.scene.collection.children: context.scene.collection.children.link(col)
        for obj in col.objects: bpy.data.objects.remove(obj, do_unlink=True)

        if p.show_ellipse_ring:
            for i, name in [(0, "Mid"), (-1, "Low"), (1, "Up")]:
                trans = (End - Mid_center) * i
                pts = [get_P_locus(d*5) + trans for d in range(73)]
                create_curve(col, f"Ell_{name}", pts, p.thick_ellipse_ring, "Ellipse", p.color_ellipse_ring, True)

        info = f"A Constant: {A:.4f}\n"
        if p.ray_mode == '2':
            for d_off, name, co, to, ci, ti, so, si, p_id in [(0, "ArmA", p.color_ray_a_out, p.thick_ray_a_out, p.color_ray_a_in, p.thick_ray_a_in, p.show_ray_a_out, p.show_ray_a_in, "ArmA"),
                                                             (p.arm_b_angle, "ArmB", p.color_ray_b_out, p.thick_ray_b_out, p.color_ray_b_in, p.thick_ray_b_in, p.show_ray_b_out, p.show_ray_b_in, "ArmB")]:
                Pt = get_P_locus(p.base_angle + d_off)
                info += f"{name}: {Pt.z+offset.z:.3f} + {A-(Pt.z+offset.z):.3f} = {A:.3f}\n"
                if so: create_curve(col, name+"_O", [E_start, Pt], to, p_id+"_Out", co)
                if si: create_curve(col, name+"_I", [Pt, End], ti, p_id+"_In", ci)
        else:
            for i in range(12):
                deg = p.base_angle + (i*30)
                if p.show_ray_classic:
                    Pt = get_P_locus(deg)
                    create_curve(col, f"Cl_{i}_O", [E_start, Pt], p.thick_ray_a_out, "Classic_Out", p.color_ray_a_out)
                    create_curve(col, f"Cl_{i}_I", [Pt, End], p.thick_ray_a_in, "Classic_In", p.color_ray_a_in)
                if p.show_ray_converge:
                    Ps, Pe = get_P_rigid(deg, E_start.z), get_P_rigid(deg, End.z)
                    create_curve(col, f"Cv_{i}_O", [Ps, Mid_center], p.thick_ray_b_out, "Conv_Out", p.color_ray_b_out)
                    create_curve(col, f"Cv_{i}_I", [Mid_center, Pe], p.thick_ray_b_in, "Conv_In", p.color_ray_b_in)
                if p.show_ray_ell_conv:
                    PtM = get_P_locus(deg)
                    PtL, PtU = PtM + (E_start - Mid_center), PtM + (End - Mid_center)
                    create_curve(col, f"ElCv_{i}_O", [PtL, Mid_center], p.thick_ray_b_out, "EllConv_Out", p.color_ray_b_out)
                    create_curve(col, f"ElCv_{i}_I", [Mid_center, PtU], p.thick_ray_b_in, "EllConv_In", p.color_ray_b_in)

        if p.show_skeleton:
            for i in range(12):
                Pl = get_P_locus(i*30) + offset
                Ms, Me = Vector((Pl.x+v*(0-Pl.z), Pl.y, 0))-offset, Vector((Pl.x+v*(A-Pl.z), Pl.y, A))-offset
                create_curve(col, f"Sk_{i}", [Ms, Me], p.skel_thick, "Skeleton", p.color_skeleton)

        if p.show_circle_rings:
            for z_v in [E_start.z, 0.0, End.z]:
                create_curve(col, f"T_{z_v}", [get_P_rigid(i*5, z_v) for i in range(73)], p.thick_circle_rings, "Tube", p.color_circle_rings, True)

        context.scene["rel_v5_info"] = info
        return {'FINISHED'}

# ------------------------------------------------------------------------
# UI & Utils
# ------------------------------------------------------------------------
class VIEW3D_PT_RelSymV5(bpy.types.Panel):
    bl_label = "Symmetric 4D Controller"; bl_idname = "VIEW3D_PT_rel_sym_v5"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
    def draw(self, context):
        layout = self.layout; p = context.scene.rel_sym_v5
        box = layout.box(); box.prop(p, "velocity", text="Velocity (v/c)"); box.prop(p, "radius", text="Radius (R)")
        cfg = layout.box(); row = cfg.row(align=True); row.prop(p, "ray_mode", text=""); row.prop(p, "base_angle", text="Base")
        if p.ray_mode == '12':
            sub = cfg.box(); sub.label(text="Pattern Selection"); sub.prop(p, "show_ray_classic"); sub.prop(p, "show_ray_converge"); sub.prop(p, "show_ray_ell_conv")
        res = layout.box(); res.label(text="Time Accounting", icon='TIME'); [res.label(text=l) for l in context.scene.get("rel_v5_info", "").split("\n") if l.strip()]; res.operator("wm.copy_rel_info", text="数値をコピー", icon='COPYDOWN')
        if p.ray_mode == '2':
            for lbl, s_out, s_in, t_o, c_o, t_i, c_i, ang in [("Arm A (Reference)", "show_ray_a_out", "show_ray_a_in", "thick_ray_a_out", "color_ray_a_out", "thick_ray_a_in", "color_ray_a_in", None),
                                                           ("Arm B (Adjustable)", "show_ray_b_out", "show_ray_b_in", "thick_ray_b_out", "color_ray_b_out", "thick_ray_b_in", "color_ray_b_in", "arm_b_angle")]:
                b = layout.box(); r = b.row(); r.label(text=lbl); r.prop(p, s_out, text="", icon='HIDE_OFF' if getattr(p, s_out) else 'HIDE_ON'); r.prop(p, s_in, text="", icon='HIDE_OFF' if getattr(p, s_in) else 'HIDE_ON')
                if ang: b.prop(p, ang, text="Relative Angle")
                col = b.column(align=True); r = col.row(align=True); r.prop(p, t_o, text="Out"); r.prop(p, c_o, text="")
                r = col.row(align=True); r.prop(p, t_i, text="In"); r.prop(p, c_i, text="")
        else:
            r12 = layout.box(); r12.label(text="12-Ray Color / Alpha (0-1)"); col = r12.column(align=True)
            col.label(text="Classic / EllConv Color"); row = col.row(align=True); row.prop(p, "thick_ray_a_out", text="Ray"); row.prop(p, "color_ray_a_out", text="")
            col.separator(); col.label(text="Converge Pattern Color"); row = col.row(align=True); row.prop(p, "thick_ray_b_out", text="Ray"); row.prop(p, "color_ray_b_out", text="")
        v_box = layout.box(); v_box.label(text="Visual Aids (Alpha 0-1)", icon='MESH_GRID'); col = v_box.column(align=True)
        for s, t, c, name in [("show_ellipse_ring", "thick_ellipse_ring", "color_ellipse_ring", "Triple Ellipses"), ("show_circle_rings", "thick_circle_rings", "color_circle_rings", "Tube Rings"), ("show_skeleton", "skel_thick", "color_skeleton", "Skeleton")]:
            row = col.row(align=True); row.prop(p, s, text=name); row.prop(p, t, text=""); row.prop(p, c, text="")
        layout.operator("object.draw_spacetime_sym_v5", text="Refresh View", icon='FILE_REFRESH')

class VIEW3D_PT_RelExport(bpy.types.Panel):
    bl_label = "Config Export"; bl_idname = "VIEW3D_PT_rel_export"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_order = 0
    def draw(self, context): self.layout.operator("wm.copy_full_script", text="全コードをコピー", icon='COPY_ID')

class VIEW3D_PT_RelLinks(bpy.types.Panel):
    bl_label = "Links"; bl_idname = "VIEW3D_PT_rel_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_rel_url", text=l["label"]); op.url = l["url"]

class VIEW3D_PT_RelSystem(bpy.types.Panel):
    bl_label = "System"; bl_idname = "VIEW3D_PT_rel_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_rel_addon", icon='CANCEL', text="アドオンを無効化")

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

class WM_OT_CopyRelInfo(bpy.types.Operator):
    bl_idname = "wm.copy_rel_info"; bl_label = "Copy Info"
    def execute(self, context): context.window_manager.clipboard = context.scene.get("rel_v5_info", ""); return {'FINISHED'}

class WM_OT_RemoveRelAddon(bpy.types.Operator):
    bl_idname = "wm.remove_rel_addon"; bl_label = "Remove"
    def execute(self, context): unregister(); 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_sym_v5; M_START, M_END = "# <BEG" + "IN_DICT>", "# <EN" + "D_DICT>"
        texts = [t.as_string() for t in bpy.data.texts if M_START in t.as_string()]
        if not texts: return {'CANCELLED'}
        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__"): d_str += f'    "{k}": ({val[0]:.3f}, {val[1]:.3f}, {val[2]:.3f}, {val[3]:.3f}),\n'
            elif isinstance(val, float): d_str += f'    "{k}": {val:.4f},\n'
            else: d_str += f'    "{k}": {val},\n'
        d_str += "}\n"
        new_code = texts[0].split(M_START)[0] + M_START + "\n" + d_str + M_END + texts[0].split(M_END)[1]
        context.window_manager.clipboard = f"# {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Version\n" + '\n'.join(new_code.split('\n')[1:])
        return {'FINISHED'}

classes = (PG_RelativitySymV5, OBJECT_OT_DrawSpacetimeSymV5, WM_OT_OpenRelUrl, WM_OT_CopyRelInfo, WM_OT_CopyFullScript, WM_OT_RemoveRelAddon, VIEW3D_PT_RelExport, VIEW3D_PT_RelSymV5, VIEW3D_PT_RelLinks, VIEW3D_PT_RelSystem)

def register():
    for cls in classes: bpy.utils.register_class(cls)
    bpy.types.Scene.rel_sym_v5 = bpy.props.PointerProperty(type=PG_RelativitySymV5)

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

if __name__ == "__main__": register()
# 2026-02-20 18:30:00 Fixed Materials & Script Export Bugfix
# Blender 4.3+ Exclusive

bl_info = {
    "name": "Symmetric Spacetime (Fixed Materials v2)",
    "author": "zionadchat Gemini",
    "version": (5, 31),
    "blender": (4, 3, 0),
    "location": "View3D > Sidebar",
    "description": "Fixed individual materials and resolved script export errors",
    "category": "Physics",
}

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

# ==============================================================================
#  DYNAMIC DEFAULTS DICTIONARY
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "velocity": 0.0000, "radius": 10.0000, "base_angle": 0.000, "arm_b_angle": 90.000,
    "ray_mode": "2",
    "show_ray_classic": True, "show_ray_converge": True,
    "show_ray_a_out": True, "show_ray_a_in": True,
    "show_ray_b_out": True, "show_ray_b_in": True,
    "color_ray_a_out": (1.000, 0.500, 0.000, 1.000), "thick_ray_a_out": 1.410,
    "color_ray_a_in": (0.000, 1.000, 1.000, 1.000), "thick_ray_a_in": 0.730,
    "color_ray_b_out": (0.400, 0.000, 0.000, 1.000), "thick_ray_b_out": 0.800,
    "color_ray_b_in": (0.000, 0.500, 0.000, 1.000), "thick_ray_b_in": 0.300,
    "show_ellipse_ring": True, "color_ellipse_ring": (0.160, 0.000, 0.050, 1.000), "thick_ellipse_ring": 0.400,
    "show_circle_rings": True, "color_circle_rings": (0.100, 0.200, 0.300, 0.500), "thick_circle_rings": 0.030,
    "show_skeleton": True, "color_skeleton": (0.500, 0.500, 0.500, 0.200), "skel_thick": 0.010,
}
# <END_DICT>

TAB_NAME = "Relativity_Sym_v5"
COLLECTION_NAME = "Relativity_Sym_Output"

ADDON_LINKS = (
    {"label": "単純トリック Einstein 氏の さぼり", "url": "<https://www.notion.so/Einstein-from-20260119-main-2edc563be1b080bb94d9f6e5b667fdec>"},
    {"label": "最新版 マイケルソン干渉計blender deviationtokyo", "url": "<https://www.notion.so/blender-deviationtokyo-30c293bfbb2980118c25dfc02259b096>"},
)

def update_view(self, context):
    try: bpy.ops.object.draw_spacetime_sym_v5('INVOKE_DEFAULT')
    except: pass

# ------------------------------------------------------------------------
# Material Logic: パーツごとに固定名
# ------------------------------------------------------------------------
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
    bsdf = nodes.get("Principled BSDF")
    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 = 2.0
    mat.diffuse_color = color
    return mat

# ------------------------------------------------------------------------
# Property Group
# ------------------------------------------------------------------------
class PG_RelativitySymV5(bpy.types.PropertyGroup):
    velocity: bpy.props.FloatProperty(name="Velocity", default=CURRENT_DEFAULTS["velocity"], min=0.0, max=0.99, update=update_view)
    radius: bpy.props.FloatProperty(name="Radius", default=CURRENT_DEFAULTS["radius"], min=0.1, update=update_view)
    base_angle: bpy.props.FloatProperty(name="Base Angle", default=CURRENT_DEFAULTS["base_angle"], min=0, max=360, update=update_view)
    arm_b_angle: bpy.props.FloatProperty(name="Arm B Angle", default=CURRENT_DEFAULTS["arm_b_angle"], min=0, max=360, update=update_view)
    ray_mode: bpy.props.EnumProperty(name="Ray Mode", items=[('2', "2 Rays", ""), ('12', "12 Rays", "")], default=CURRENT_DEFAULTS["ray_mode"], update=update_view)

    show_ray_classic: bpy.props.BoolProperty(name="Point-Ellipse", default=True, update=update_view)
    show_ray_converge: bpy.props.BoolProperty(name="Ring-Center", default=True, update=update_view)

    show_ray_a_out: bpy.props.BoolProperty(default=True, update=update_view); show_ray_a_in: bpy.props.BoolProperty(default=True, update=update_view)
    color_ray_a_out: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_ray_a_out"], update=update_view); thick_ray_a_out: bpy.props.FloatProperty(default=1.41, update=update_view)
    color_ray_a_in: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_ray_a_in"], update=update_view); thick_ray_a_in: bpy.props.FloatProperty(default=0.73, update=update_view)

    show_ray_b_out: bpy.props.BoolProperty(default=True, update=update_view); show_ray_b_in: bpy.props.BoolProperty(default=True, update=update_view)
    color_ray_b_out: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_ray_b_out"], update=update_view); thick_ray_b_out: bpy.props.FloatProperty(default=0.8, update=update_view)
    color_ray_b_in: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_ray_b_in"], update=update_view); thick_ray_b_in: bpy.props.FloatProperty(default=0.3, update=update_view)

    show_ellipse_ring: bpy.props.BoolProperty(name="Ellipse", default=True, update=update_view); color_ellipse_ring: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_ellipse_ring"], update=update_view); thick_ellipse_ring: bpy.props.FloatProperty(default=0.4, update=update_view)
    show_circle_rings: bpy.props.BoolProperty(name="Tube Rings", default=True, update=update_view); color_circle_rings: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_circle_rings"], update=update_view); thick_circle_rings: bpy.props.FloatProperty(default=0.03, update=update_view)
    show_skeleton: bpy.props.BoolProperty(name="Skeleton", default=True, update=update_view); color_skeleton: bpy.props.FloatVectorProperty(subtype='COLOR', size=4, default=CURRENT_DEFAULTS["color_skeleton"], update=update_view); skel_thick: bpy.props.FloatProperty(default=0.01, update=update_view)

# ------------------------------------------------------------------------
# Drawing Logic
# ------------------------------------------------------------------------
def create_curve(col, name, points, thickness, part_id, color, circular=False):
    curve = bpy.data.curves.new(name, 'CURVE')
    curve.dimensions = '3D'
    obj = bpy.data.objects.new(name, curve)
    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(part_id, color))
    return obj

class OBJECT_OT_DrawSpacetimeSymV5(bpy.types.Operator):
    bl_idname = "object.draw_spacetime_sym_v5"
    bl_label = "Draw Spacetime"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        p = context.scene.rel_sym_v5
        v, R_val = p.velocity, p.radius
        gamma = 1.0 / math.sqrt(max(0.0001, 1.0 - v**2))
        A = 2.0 * R_val * gamma
        offset = Vector((v * A / 2.0, 0, A / 2.0))
        E_start, Mid, End = Vector((0,0,0))-offset, Vector((v*A/2,0,A/2))-offset, Vector((v*A,0,A))-offset

        def get_P_final(deg):
            rad = math.radians(deg)
            return Vector((gamma*(R_val*math.cos(rad)+v*R_val), R_val*math.sin(rad), gamma*(R_val+v*(R_val*math.cos(rad))))) - offset

        def get_P_rigid(deg, z_v):
            t_p = z_v + offset.z
            rad = math.radians(deg)
            return Vector((v*t_p - offset.x + R_val*math.cos(rad), R_val*math.sin(rad), z_v))

        col = bpy.data.collections.get(COLLECTION_NAME) or bpy.data.collections.new(COLLECTION_NAME)
        if COLLECTION_NAME not in context.scene.collection.children: context.scene.collection.children.link(col)
        for obj in col.objects: bpy.data.objects.remove(obj, do_unlink=True)

        if p.show_ellipse_ring: create_curve(col, "Ellipse", [get_P_final(i*5) for i in range(73)], p.thick_ellipse_ring, "Ellipse", p.color_ellipse_ring, True)

        info = f"A Constant: {A:.4f}\n"
        if p.ray_mode == '2':
            for d_off, name, co, to, ci, ti, so, si, p_id in [(0, "ArmA", p.color_ray_a_out, p.thick_ray_a_out, p.color_ray_a_in, p.thick_ray_a_in, p.show_ray_a_out, p.show_ray_a_in, "ArmA"),
                                                             (p.arm_b_angle, "ArmB", p.color_ray_b_out, p.thick_ray_b_out, p.color_ray_b_in, p.thick_ray_b_in, p.show_ray_b_out, p.show_ray_b_in, "ArmB")]:
                Pt = get_P_final(p.base_angle + d_off)
                info += f"{name}: {Pt.z+offset.z:.3f} + {A-(Pt.z+offset.z):.3f} = {A:.3f}\n"
                if so: create_curve(col, name+"_O", [E_start, Pt], to, p_id+"_Out", co)
                if si: create_curve(col, name+"_I", [Pt, End], ti, p_id+"_In", ci)
        else:
            for i in range(12):
                deg = p.base_angle + (i*30)
                Pt = get_P_final(deg)
                if p.show_ray_classic:
                    create_curve(col, f"Cl_{i}_O", [E_start, Pt], p.thick_ray_a_out, "Classic_Out", p.color_ray_a_out)
                    create_curve(col, f"Cl_{i}_I", [Pt, End], p.thick_ray_a_in, "Classic_In", p.color_ray_a_in)
                if p.show_ray_converge:
                    Ps, Pe = get_P_rigid(deg, E_start.z), get_P_rigid(deg, End.z)
                    create_curve(col, f"Cv_{i}_O", [Ps, Mid], p.thick_ray_b_out, "Conv_Out", p.color_ray_b_out)
                    create_curve(col, f"Cv_{i}_I", [Mid, Pe], p.thick_ray_b_in, "Conv_In", p.color_ray_b_in)

        if p.show_skeleton:
            for i in range(12):
                Pl = get_P_final(i*30) + offset
                Ms, Me = Vector((Pl.x+v*(0-Pl.z), Pl.y, 0))-offset, Vector((Pl.x+v*(A-Pl.z), Pl.y, A))-offset
                create_curve(col, f"Sk_{i}", [Ms, Me], p.skel_thick, "Skeleton", p.color_skeleton)

        if p.show_circle_rings:
            for z_v in [E_start.z, 0.0, End.z]: create_curve(col, f"T_{z_v}", [get_P_rigid(i*5, z_v) for i in range(73)], p.thick_circle_rings, "Tube", p.color_circle_rings, True)

        context.scene["rel_v5_info"] = info
        return {'FINISHED'}

# ------------------------------------------------------------------------
# UI & Export
# ------------------------------------------------------------------------
class VIEW3D_PT_RelSymV5(bpy.types.Panel):
    bl_label = "Symmetric 4D Controller"; bl_idname = "VIEW3D_PT_rel_sym_v5"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
    def draw(self, context):
        layout = self.layout; p = context.scene.rel_sym_v5
        box = layout.box(); box.prop(p, "velocity", text="Velocity (v/c)"); box.prop(p, "radius", text="Radius (R)")
        cfg = layout.box(); row = cfg.row(align=True); row.prop(p, "ray_mode", text=""); row.prop(p, "base_angle", text="Base")
        if p.ray_mode == '12':
            sub = cfg.box(); sub.prop(p, "show_ray_classic"); sub.prop(p, "show_ray_converge")
        res = layout.box(); res.label(text="Time Accounting", icon='TIME'); [res.label(text=l) for l in context.scene.get("rel_v5_info", "").split("\n") if l.strip()]; res.operator("wm.copy_rel_info", text="数値をコピー", icon='COPYDOWN')
        if p.ray_mode == '2':
            for lbl, s_out, s_in, t_o, c_o, t_i, c_i, ang in [("Arm A (Reference)", "show_ray_a_out", "show_ray_a_in", "thick_ray_a_out", "color_ray_a_out", "thick_ray_a_in", "color_ray_a_in", None),
                                                           ("Arm B (Adjustable)", "show_ray_b_out", "show_ray_b_in", "thick_ray_b_out", "color_ray_b_out", "thick_ray_b_in", "color_ray_b_in", "arm_b_angle")]:
                b = layout.box(); r = b.row(); r.label(text=lbl); r.prop(p, s_out, text="", icon='HIDE_OFF' if getattr(p, s_out) else 'HIDE_ON'); r.prop(p, s_in, text="", icon='HIDE_OFF' if getattr(p, s_in) else 'HIDE_ON')
                if ang: b.prop(p, ang, text="Relative Angle")
                col = b.column(align=True); r = col.row(align=True); r.prop(p, t_o, text="Out"); r.prop(p, c_o, text="")
                r = col.row(align=True); r.prop(p, t_i, text="In"); r.prop(p, c_i, text="")
        else:
            r12 = layout.box(); r12.label(text="12-Ray Visual Settings"); col = r12.column(align=True)
            col.label(text="Classic Pattern"); row = col.row(align=True); row.prop(p, "thick_ray_a_out", text="Ray"); row.prop(p, "color_ray_a_out", text="")
            col.separator(); col.label(text="Converge Pattern"); row = col.row(align=True); row.prop(p, "thick_ray_b_out", text="Ray"); row.prop(p, "color_ray_b_out", text="")
        v_box = layout.box(); v_box.label(text="Visual Aids", icon='MESH_GRID'); col = v_box.column(align=True)
        for s, t, c, name in [("show_ellipse_ring", "thick_ellipse_ring", "color_ellipse_ring", "Ellipse"), ("show_circle_rings", "thick_circle_rings", "color_circle_rings", "Tube Rings"), ("show_skeleton", "skel_thick", "color_skeleton", "Skeleton")]:
            row = col.row(align=True); row.prop(p, s, text=name); row.prop(p, t, text=""); row.prop(p, c, text="")
        layout.operator("object.draw_spacetime_sym_v5", text="Refresh View", icon='FILE_REFRESH')

class VIEW3D_PT_RelExport(bpy.types.Panel):
    bl_label = "Config Export"; bl_idname = "VIEW3D_PT_rel_export"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_order = 0
    def draw(self, context): self.layout.operator("wm.copy_full_script", text="全コードをコピー", icon='COPY_ID')

class VIEW3D_PT_RelLinks(bpy.types.Panel):
    bl_label = "Links"; bl_idname = "VIEW3D_PT_rel_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_rel_url", text=l["label"]); op.url = l["url"]

class VIEW3D_PT_RelSystem(bpy.types.Panel):
    bl_label = "System"; bl_idname = "VIEW3D_PT_rel_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_rel_addon", icon='CANCEL', text="アドオンを無効化")

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

class WM_OT_CopyRelInfo(bpy.types.Operator):
    bl_idname = "wm.copy_rel_info"; bl_label = "Copy Info"
    def execute(self, context): context.window_manager.clipboard = context.scene.get("rel_v5_info", ""); return {'FINISHED'}

class WM_OT_RemoveRelAddon(bpy.types.Operator):
    bl_idname = "wm.remove_rel_addon"; bl_label = "Remove"
    def execute(self, context): unregister(); 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_sym_v5; M_START, M_END = "# <BEG" + "IN_DICT>", "# <EN" + "D_DICT>"
        texts = [t.as_string() for t in bpy.data.texts if M_START in t.as_string()]
        if not texts: return {'CANCELLED'}
        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__"): d_str += f'    "{k}": ({val[0]:.3f}, {val[1]:.3f}, {val[2]:.3f}, {val[3]:.3f}),\n'
            elif isinstance(val, float): d_str += f'    "{k}": {val:.4f},\n'
            else: d_str += f'    "{k}": {val},\n'
        d_str += "}\n"
        new_code = texts[0].split(M_START)[0] + M_START + "\n" + d_str + M_END + texts[0].split(M_END)[1]
        context.window_manager.clipboard = f"# {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Version\n" + '\n'.join(new_code.split('\n')[1:])
        return {'FINISHED'}

# ------------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------------
classes = (PG_RelativitySymV5, OBJECT_OT_DrawSpacetimeSymV5, WM_OT_OpenRelUrl, WM_OT_CopyRelInfo, WM_OT_CopyFullScript, WM_OT_RemoveRelAddon, VIEW3D_PT_RelExport, VIEW3D_PT_RelSymV5, VIEW3D_PT_RelLinks, VIEW3D_PT_RelSystem)

def register():
    for cls in classes: bpy.utils.register_class(cls)
    bpy.types.Scene.rel_sym_v5 = bpy.props.PointerProperty(type=PG_RelativitySymV5)

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

if __name__ == "__main__": register()