blender Million 2026

prefix 一次方程式 楕円変形トーラス20260408 (1)








# Copied: 2026-04-08 12:00:00
import bpy
import time
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime

# ==============================================================================
#  【 基本設定エリア 】
# ==============================================================================

PREFIX       = "EqGen"
ADDON_NAME   = "[ Equation Gen ]"
TAB_NAME     = "[ Equation Gen ]"
PANEL_TITLE  = "Equation Lines"
AUTHOR       = "zionadchat"

# ★ このスクリプト自身のID(絶対に消さないこと)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: EQ_LINES_2026_04_08_V6_1 ###"

# ==============================================================================
#  システム初期化 & ID管理
# ==============================================================================

bl_info = {
    "name": f"{ADDON_NAME} {PREFIX}",
    "author": AUTHOR,
    "version": (6, 1, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": "Lines, Torus, Elliptic Torus, and Cross Cylinders (z=x)",
    "category": "3D View",
}

OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"

ADDON_LINKS = (
    {"label": "Prefix 20260408", "url": "<https://www.notion.so/Prefix-20260408-33cf5dacaf43807e9e35ff8cdbbc39c6>"},
)

# ==============================================================================
#  デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    # Line
    "enable_preview": False,
    "val_a": 0.6000, "val_b": 1.0000, "val_d": 10.0000,
    "x_min": -50.0, "x_max": 50.0,
    "y_min": -50.0, "y_max": 50.0,
    "z_min": -50.0, "z_max": 50.0,
    "thickness": 0.5000, "draw_plane": "XZ",
    "show_eq1": True, "show_eq2": True, "show_eq3": True,
    "color1": (1.0000, 0.2000, 0.2000, 1.0000),
    "color2": (0.2000, 1.0000, 0.2000, 1.0000),
    "color3": (0.2000, 0.2000, 1.0000, 1.0000),

    # Torus 1 (Normal)
    "t1_enable_preview": False,
    "t1_mode": "INTERVAL",
    "t1_val_a": 0.6000, "t1_val_b": 1.0000,
    "t1_z_min": -50.0, "t1_z_max": 50.0, "t1_count": 11,
    "t1_z_center": 0.0, "t1_z_interval": 1.0, "t1_up_down_count": 5,
    "t1_major_radius": 5.0, "t1_minor_radius": 1.0,
    "t1_color": (0.2000, 0.8000, 0.8000, 1.0000), 

    # Torus 2 (Elliptic / Lorentz)
    "t2_enable_preview": False,
    "t2_mode": "INTERVAL",
    "t2_val_a": 0.6000, "t2_val_b": 1.0000,
    "t2_z_min": -50.0, "t2_z_max": 50.0, "t2_count": 11,
    "t2_z_center": 0.0, "t2_z_interval": 1.0, "t2_up_down_count": 5,
    "t2_major_radius": 5.0, "t2_minor_radius": 1.0,
    "t2_f": 1.0000, "t2_g": 1.6000, # 収縮率 f/g
    "t2_color": (0.8000, 0.2000, 0.8000, 1.0000),

    # Cross 1 (z=x, z=-x)
    "c1_enable_preview": False,
    "c1_center": (0.0, 0.0, 0.0),
    "c1_length": 50.0, "c1_thickness": 0.5000,
    "c1_color1": (1.0000, 1.0000, 0.2000, 1.0000),
    "c1_color2": (1.0000, 0.5000, 0.2000, 1.0000),

    # Cross 2 (z=x, z=-x)
    "c2_enable_preview": False,
    "c2_center": (10.0, 0.0, 10.0),
    "c2_length": 30.0, "c2_thickness": 0.5000,
    "c2_color1": (0.2000, 1.0000, 0.2000, 1.0000),
    "c2_color2": (0.2000, 0.5000, 1.0000, 1.0000),
}
# <END_DICT>

PREVIEW_COL_LINE = f"{PREFIX}_Line_Preview"
PREVIEW_COL_TORUS1 = f"{PREFIX}_Torus1_Preview"
PREVIEW_COL_TORUS2 = f"{PREFIX}_Torus2_Preview"
PREVIEW_COL_CROSS1 = f"{PREFIX}_Cross1_Preview"
PREVIEW_COL_CROSS2 = f"{PREFIX}_Cross2_Preview"

# ==============================================================================
#  共通マテリアル・データ管理 ロジック
# ==============================================================================

def cleanup_preview_data():
    for name in [PREVIEW_COL_LINE, PREVIEW_COL_TORUS1, PREVIEW_COL_TORUS2, PREVIEW_COL_CROSS1, PREVIEW_COL_CROSS2]:
        col = bpy.data.collections.get(name)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0:
                    if isinstance(data, bpy.types.Curve):
                        bpy.data.curves.remove(data)
            if len(col.objects) == 0:
                bpy.data.collections.remove(col)

def apply_material_settings(mat, color):
    mat.use_nodes = True
    mat.blend_method = 'BLEND'
    mat.diffuse_color = color
    
    tree = mat.node_tree
    bsdf = tree.nodes.get("Principled BSDF")
    if not bsdf:
        tree.nodes.clear()
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        bsdf.name = "Principled BSDF"
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        tree.links.new(bsdf.outputs[0], out.inputs[0])
        
    if "Base Color" in bsdf.inputs:
        bsdf.inputs["Base Color"].default_value = color
    if "Alpha" in bsdf.inputs:
        bsdf.inputs["Alpha"].default_value = color[3]

def get_preview_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name=name)
    apply_material_settings(mat, color)
    return mat

def build_curve_circle(curve, radius, segments=32):
    if len(curve.splines) == 0:
        spline = curve.splines.new('POLY')
        spline.points.add(segments - 1)
        spline.use_cyclic_u = True
    else:
        spline = curve.splines[0]
        if len(spline.points) != segments:
            curve.splines.clear()
            spline = curve.splines.new('POLY')
            spline.points.add(segments - 1)
            spline.use_cyclic_u = True
            
    for i in range(segments):
        angle = 2 * math.pi * i / segments
        spline.points[i].co = (radius * math.cos(angle), radius * math.sin(angle), 0.0, 1.0)

# ==============================================================================
#  Line プレビューロジック
# ==============================================================================

def get_clipped_segment(m, c, x_min, x_max, v_min, v_max):
    if m == 0:
        if v_min <= c <= v_max: return x_min, x_max
        return None, None
    else:
        x_from_v1, x_from_v2 = (v_min - c) / m, (v_max - c) / m
        valid_x_min, valid_x_max = min(x_from_v1, x_from_v2), max(x_from_v1, x_from_v2)
        act_x_min, act_x_max = max(x_min, valid_x_min), min(x_max, valid_x_max)
        if act_x_min > act_x_max: return None, None
        return act_x_min, act_x_max

def calc_points(props, m, c):
    x_min, x_max = min(props.x_min, props.x_max), max(props.x_min, props.x_max)
    y_min, y_max = min(props.y_min, props.y_max), max(props.y_min, props.y_max)
    z_min, z_max = min(props.z_min, props.z_max), max(props.z_min, props.z_max)
    
    if props.draw_plane == 'XZ':
        act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, z_min, z_max)
        if act_x_min is None: return None, None
        return (act_x_min, 0.0, m * act_x_min + c), (act_x_max, 0.0, m * act_x_max + c)
    else:
        act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, y_min, y_max)
        if act_x_min is None: return None, None
        return (act_x_min, m * act_x_min + c, 0.0), (act_x_max, m * act_x_max + c, 0.0)

def update_line_preview(context, props):
    if not props.enable_preview:
        col = bpy.data.collections.get(PREVIEW_COL_LINE)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0: bpy.data.curves.remove(data)
            bpy.data.collections.remove(col)
        return

    col = bpy.data.collections.get(PREVIEW_COL_LINE)
    if not col:
        col = bpy.data.collections.new(PREVIEW_COL_LINE)
        context.scene.collection.children.link(col)

    a = props.val_a if abs(props.val_a) > 0.0001 else 0.0001
    b, d = props.val_b, props.val_d
    m = b / a

    equations = [
        {"id": 1, "show": props.show_eq1, "color": props.color1, "offset": 0.0},
        {"id": 2, "show": props.show_eq2, "color": props.color2, "offset": -d},
        {"id": 3, "show": props.show_eq3, "color": props.color3, "offset": d},
    ]

    for eq in equations:
        obj_name = f"[Preview] EqLine_{eq['id']}"
        obj = bpy.data.objects.get(obj_name)

        if not eq["show"]:
            if obj: obj.hide_viewport = obj.hide_render = True
            continue

        p1, p2 = calc_points(props, m, eq["offset"])
        if p1 is None:
            if obj: obj.hide_viewport = obj.hide_render = True
            continue

        if not obj:
            curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
            curve.dimensions = '3D'; curve.fill_mode = 'FULL'
            spline = curve.splines.new('POLY'); spline.points.add(1)
            obj = bpy.data.objects.new(obj_name, curve)
            col.objects.link(obj)
        else:
            curve = obj.data; spline = curve.splines[0]

        curve.bevel_depth = props.thickness; curve.bevel_resolution = 6
        spline.points[0].co = (*p1, 1.0); spline.points[1].co = (*p2, 1.0)
        obj.hide_viewport = obj.hide_render = False

        mat = get_preview_material(f"Preview_Mat_EqLine{eq['id']}", eq["color"])
        if not obj.data.materials: obj.data.materials.append(mat)
        else: obj.data.materials[0] = mat

# ==============================================================================
#  Torus プレビューロジック (Normal & Elliptic)
# ==============================================================================

def update_torus1_preview(context, props):
    if not props.t1_enable_preview:
        col = bpy.data.collections.get(PREVIEW_COL_TORUS1)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0: bpy.data.curves.remove(data)
            bpy.data.collections.remove(col)
        return

    col = bpy.data.collections.get(PREVIEW_COL_TORUS1)
    if not col:
        col = bpy.data.collections.new(PREVIEW_COL_TORUS1)
        context.scene.collection.children.link(col)

    a = props.t1_val_a
    b_val = props.t1_val_b if abs(props.t1_val_b) > 0.0001 else 0.0001
    
    z_list = []
    if props.t1_mode == 'RANGE':
        count = props.t1_count
        for i in range(count):
            t = i / (count - 1) if count > 1 else 0.5
            z_list.append(props.t1_z_min + t * (props.t1_z_max - props.t1_z_min))
    else: 
        c, interval, ud = props.t1_z_center, props.t1_z_interval, props.t1_up_down_count
        for i in range(-ud, ud + 1): z_list.append(c + i * interval)

    existing_objs = list(col.objects)
    mat = get_preview_material("Preview_Mat_Torus1", props.t1_color)

    for i, z in enumerate(z_list):
        obj_name = f"[Preview] Normal_Torus_{i+1}"
        x = z * (a / b_val); y = 0.0 
        
        if i < len(existing_objs):
            obj = existing_objs[i]
            curve = obj.data
        else:
            curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
            curve.dimensions = '3D'; curve.fill_mode = 'FULL'
            obj = bpy.data.objects.new(obj_name, curve)
            col.objects.link(obj)
            
        curve.bevel_depth = props.t1_minor_radius; curve.bevel_resolution = 8
        build_curve_circle(curve, props.t1_major_radius)
        
        obj.location = (x, y, z); obj.scale = (1.0, 1.0, 1.0)
        obj.hide_viewport = obj.hide_render = False
        
        if not obj.data.materials: obj.data.materials.append(mat)
        else: obj.data.materials[0] = mat

    if len(existing_objs) > len(z_list):
        for obj in existing_objs[len(z_list):]:
            data = obj.data
            bpy.data.objects.remove(obj, do_unlink=True)
            if data and data.users == 0: bpy.data.curves.remove(data)

def update_torus2_preview(context, props):
    if not props.t2_enable_preview:
        col = bpy.data.collections.get(PREVIEW_COL_TORUS2)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0: bpy.data.curves.remove(data)
            bpy.data.collections.remove(col)
        return

    col = bpy.data.collections.get(PREVIEW_COL_TORUS2)
    if not col:
        col = bpy.data.collections.new(PREVIEW_COL_TORUS2)
        context.scene.collection.children.link(col)

    a = props.t2_val_a
    b_val = props.t2_val_b if abs(props.t2_val_b) > 0.0001 else 0.0001
    
    # 収縮率計算
    g_val = props.t2_g if abs(props.t2_g) > 0.0001 else 0.0001
    scale_x = props.t2_f / g_val
    
    z_list = []
    if props.t2_mode == 'RANGE':
        count = props.t2_count
        for i in range(count):
            t = i / (count - 1) if count > 1 else 0.5
            z_list.append(props.t2_z_min + t * (props.t2_z_max - props.t2_z_min))
    else: 
        c, interval, ud = props.t2_z_center, props.t2_z_interval, props.t2_up_down_count
        for i in range(-ud, ud + 1): z_list.append(c + i * interval)

    existing_objs = list(col.objects)
    mat = get_preview_material("Preview_Mat_Torus2", props.t2_color)

    for i, z in enumerate(z_list):
        obj_name = f"[Preview] Elliptic_Torus_{i+1}"
        x = z * (a / b_val); y = 0.0 
        
        if i < len(existing_objs):
            obj = existing_objs[i]
            curve = obj.data
        else:
            curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
            curve.dimensions = '3D'; curve.fill_mode = 'FULL'
            obj = bpy.data.objects.new(obj_name, curve)
            col.objects.link(obj)
            
        curve.bevel_depth = props.t2_minor_radius; curve.bevel_resolution = 8
        build_curve_circle(curve, props.t2_major_radius)
        
        obj.location = (x, y, z)
        obj.scale = (scale_x, 1.0, 1.0) # Lorentz Contraction
        obj.hide_viewport = obj.hide_render = False
        
        if not obj.data.materials: obj.data.materials.append(mat)
        else: obj.data.materials[0] = mat

    if len(existing_objs) > len(z_list):
        for obj in existing_objs[len(z_list):]:
            data = obj.data
            bpy.data.objects.remove(obj, do_unlink=True)
            if data and data.users == 0: bpy.data.curves.remove(data)

# ==============================================================================
#  Cross (z=x, z=-x) プレビューロジック
# ==============================================================================

def draw_single_cross(context, props, prefix):
    enable = getattr(props, f"{prefix}_enable_preview")
    col_name = PREVIEW_COL_CROSS1 if prefix == "c1" else PREVIEW_COL_CROSS2
    
    if not enable:
        col = bpy.data.collections.get(col_name)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0: bpy.data.curves.remove(data)
            bpy.data.collections.remove(col)
        return

    col = bpy.data.collections.get(col_name)
    if not col:
        col = bpy.data.collections.new(col_name)
        context.scene.collection.children.link(col)

    center = getattr(props, f"{prefix}_center")
    length = getattr(props, f"{prefix}_length")
    thickness = getattr(props, f"{prefix}_thickness")
    c1 = getattr(props, f"{prefix}_color1")
    c2 = getattr(props, f"{prefix}_color2")

    cx, cy, cz = center[0], center[1], center[2]

    # 線分の計算 (XZ平面上で z=x, z=-x)
    pts = [
        ((cx-length, cy, cz-length), (cx+length, cy, cz+length)), # z = x
        ((cx-length, cy, cz+length), (cx+length, cy, cz-length))  # z = -x
    ]
        
    colors = [c1, c2]
    
    for i in range(2):
        obj_name = f"[Preview] {prefix.capitalize()}_Line{i+1}"
        obj = bpy.data.objects.get(obj_name)

        if not obj:
            curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
            curve.dimensions = '3D'; curve.fill_mode = 'FULL'
            spline = curve.splines.new('POLY'); spline.points.add(1)
            obj = bpy.data.objects.new(obj_name, curve)
            col.objects.link(obj)
        else:
            curve = obj.data; spline = curve.splines[0]

        curve.bevel_depth = thickness; curve.bevel_resolution = 6
        spline.points[0].co = (*pts[i][0], 1.0)
        spline.points[1].co = (*pts[i][1], 1.0)
        obj.hide_viewport = obj.hide_render = False

        mat = get_preview_material(f"Preview_Mat_{prefix.capitalize()}_L{i+1}", colors[i])
        if not obj.data.materials: obj.data.materials.append(mat)
        else: obj.data.materials[0] = mat

# ==============================================================================
#  タイマー管理
# ==============================================================================

_timer = None
_last_update_time = 0

def delayed_update():
    global _timer, _last_update_time
    _timer = None
    now = time.time()
    if now - _last_update_time < 0.05:
        if _timer is None:
            _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
        return None
    _last_update_time = now
    
    ctx = bpy.context
    if not ctx or not ctx.scene: return None
    if ctx.object and ctx.object.mode != 'OBJECT': return None

    props = getattr(ctx.scene, PROPS_NAME, None)
    if props:
        update_line_preview(ctx, props)
        update_torus1_preview(ctx, props)
        update_torus2_preview(ctx, props)
        draw_single_cross(ctx, props, "c1")
        draw_single_cross(ctx, props, "c2")
    return None

def on_update(self, context):
    global _timer
    if _timer is None: 
        _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)

# ==============================================================================
#  PROPERTIES
# ==============================================================================

class PG_EquationProps(PropertyGroup):
    # Line Properties
    enable_preview: BoolProperty(name="Enable Line Preview", default=CURRENT_DEFAULTS['enable_preview'], update=on_update)
    val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['val_a'], update=on_update)
    val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['val_b'], update=on_update)
    val_d: FloatProperty(name="Value d", default=CURRENT_DEFAULTS['val_d'], update=on_update)
    x_min: FloatProperty(name="X Min", default=CURRENT_DEFAULTS['x_min'], update=on_update)
    x_max: FloatProperty(name="X Max", default=CURRENT_DEFAULTS['x_max'], update=on_update)
    y_min: FloatProperty(name="Y Min", default=CURRENT_DEFAULTS['y_min'], update=on_update)
    y_max: FloatProperty(name="Y Max", default=CURRENT_DEFAULTS['y_max'], update=on_update)
    z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['z_min'], update=on_update)
    z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['z_max'], update=on_update)
    thickness: FloatProperty(name="Line Thickness", default=CURRENT_DEFAULTS['thickness'], min=0.01, max=10.0, update=on_update)
    draw_plane: EnumProperty(name="Draw Plane", items=[('XZ', "Front (XZ)", "XZ Plane"), ('XY', "Top (XY)", "XY Plane")], default=CURRENT_DEFAULTS['draw_plane'], update=on_update)
    show_eq1: BoolProperty(name="Show Eq 1", default=CURRENT_DEFAULTS['show_eq1'], update=on_update)
    show_eq2: BoolProperty(name="Show Eq 2", default=CURRENT_DEFAULTS['show_eq2'], update=on_update)
    show_eq3: BoolProperty(name="Show Eq 3", default=CURRENT_DEFAULTS['show_eq3'], update=on_update)
    color1: FloatVectorProperty(name="Color 1", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color1'], update=on_update)
    color2: FloatVectorProperty(name="Color 2", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color2'], update=on_update)
    color3: FloatVectorProperty(name="Color 3", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color3'], update=on_update)

    # Torus 1 (Normal)
    t1_enable_preview: BoolProperty(name="Enable Torus 1 Preview", default=CURRENT_DEFAULTS['t1_enable_preview'], update=on_update)
    t1_mode: EnumProperty(name="Mode", items=[('INTERVAL', "Interval Mode", ""), ('RANGE', "Range Mode", "")], default=CURRENT_DEFAULTS['t1_mode'], update=on_update)
    t1_val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['t1_val_a'], update=on_update)
    t1_val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['t1_val_b'], update=on_update)
    t1_z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['t1_z_min'], update=on_update)
    t1_z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['t1_z_max'], update=on_update)
    t1_count: IntProperty(name="Total Count", default=CURRENT_DEFAULTS['t1_count'], min=1, max=500, update=on_update)
    t1_z_center: FloatProperty(name="Z Center", default=CURRENT_DEFAULTS['t1_z_center'], update=on_update)
    t1_z_interval: FloatProperty(name="Z Interval", default=CURRENT_DEFAULTS['t1_z_interval'], update=on_update)
    t1_up_down_count: IntProperty(name="Up/Down Count", default=CURRENT_DEFAULTS['t1_up_down_count'], min=0, max=100, update=on_update)
    t1_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['t1_major_radius'], min=0.1, max=100.0, update=on_update)
    t1_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['t1_minor_radius'], min=0.01, max=50.0, update=on_update)
    t1_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['t1_color'], update=on_update)

    # Torus 2 (Elliptic)
    t2_enable_preview: BoolProperty(name="Enable Torus 2 Preview", default=CURRENT_DEFAULTS['t2_enable_preview'], update=on_update)
    t2_mode: EnumProperty(name="Mode", items=[('INTERVAL', "Interval Mode", ""), ('RANGE', "Range Mode", "")], default=CURRENT_DEFAULTS['t2_mode'], update=on_update)
    t2_val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['t2_val_a'], update=on_update)
    t2_val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['t2_val_b'], update=on_update)
    t2_z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['t2_z_min'], update=on_update)
    t2_z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['t2_z_max'], update=on_update)
    t2_count: IntProperty(name="Total Count", default=CURRENT_DEFAULTS['t2_count'], min=1, max=500, update=on_update)
    t2_z_center: FloatProperty(name="Z Center", default=CURRENT_DEFAULTS['t2_z_center'], update=on_update)
    t2_z_interval: FloatProperty(name="Z Interval", default=CURRENT_DEFAULTS['t2_z_interval'], update=on_update)
    t2_up_down_count: IntProperty(name="Up/Down Count", default=CURRENT_DEFAULTS['t2_up_down_count'], min=0, max=100, update=on_update)
    t2_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['t2_major_radius'], min=0.1, max=100.0, update=on_update)
    t2_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['t2_minor_radius'], min=0.01, max=50.0, update=on_update)
    t2_f: FloatProperty(name="f (Numerator)", default=CURRENT_DEFAULTS['t2_f'], min=0.01, update=on_update)
    t2_g: FloatProperty(name="g (Denominator)", default=CURRENT_DEFAULTS['t2_g'], min=0.01, update=on_update)
    t2_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['t2_color'], update=on_update)

    # Cross 1
    c1_enable_preview: BoolProperty(name="Enable Cross 1 Preview", default=CURRENT_DEFAULTS['c1_enable_preview'], update=on_update)
    c1_center: FloatVectorProperty(name="Center Position", size=3, default=CURRENT_DEFAULTS['c1_center'], update=on_update)
    c1_length: FloatProperty(name="Length (Radius)", default=CURRENT_DEFAULTS['c1_length'], min=0.1, update=on_update)
    c1_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['c1_thickness'], min=0.01, update=on_update)
    c1_color1: FloatVectorProperty(name="Color z=x", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['c1_color1'], update=on_update)
    c1_color2: FloatVectorProperty(name="Color z=-x", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['c1_color2'], update=on_update)

    # Cross 2
    c2_enable_preview: BoolProperty(name="Enable Cross 2 Preview", default=CURRENT_DEFAULTS['c2_enable_preview'], update=on_update)
    c2_center: FloatVectorProperty(name="Center Position", size=3, default=CURRENT_DEFAULTS['c2_center'], update=on_update)
    c2_length: FloatProperty(name="Length (Radius)", default=CURRENT_DEFAULTS['c2_length'], min=0.1, update=on_update)
    c2_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['c2_thickness'], min=0.01, update=on_update)
    c2_color1: FloatVectorProperty(name="Color z=x", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['c2_color1'], update=on_update)
    c2_color2: FloatVectorProperty(name="Color z=-x", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['c2_color2'], update=on_update)

# ==============================================================================
#  OPERATORS
# ==============================================================================

class OT_ShowLinePreview(Operator):
    bl_idname = f"{OP_PREFIX}.show_line_preview"; bl_label = "Show Line Preview"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props: props.enable_preview = True; update_line_preview(context, props)
        return {'FINISHED'}

class OT_DetachLines(Operator):
    bl_idname = f"{OP_PREFIX}.detach_lines"; bl_label = "Detach Lines"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col_preview = bpy.data.collections.get(PREVIEW_COL_LINE)
        if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
        target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
        bpy.ops.object.select_all(action='DESELECT')
        for obj in list(col_preview.objects):
            if obj.hide_viewport: continue
            if obj.name not in target_col.objects: target_col.objects.link(obj)
            col_preview.objects.unlink(obj)
            obj.name = obj.name.replace("[Preview]", "Solid_EqLine") + f"_{timestamp}"
            if obj.data.materials:
                mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
                obj.data.materials[0] = new_mat
            obj.select_set(True)
        props = getattr(context.scene, PROPS_NAME, None)
        if props: update_line_preview(context, props)
        return {'FINISHED'}

class OT_ShowTorus1Preview(Operator):
    bl_idname = f"{OP_PREFIX}.show_torus1_preview"; bl_label = "Show Normal Torus"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props: props.t1_enable_preview = True; update_torus1_preview(context, props)
        return {'FINISHED'}

class OT_DetachTorus1(Operator):
    bl_idname = f"{OP_PREFIX}.detach_torus1"; bl_label = "Detach Normal Torus"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col_preview = bpy.data.collections.get(PREVIEW_COL_TORUS1)
        if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
        target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
        bpy.ops.object.select_all(action='DESELECT')
        for obj in list(col_preview.objects):
            if obj.name not in target_col.objects: target_col.objects.link(obj)
            col_preview.objects.unlink(obj)
            obj.name = obj.name.replace("[Preview]", "Solid_Normal_Torus") + f"_{timestamp}"
            if obj.data.materials:
                mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
                obj.data.materials[0] = new_mat
            bpy.context.view_layer.objects.active = obj; obj.select_set(True)
            bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
            obj.select_set(False)
        props = getattr(context.scene, PROPS_NAME, None)
        if props: update_torus1_preview(context, props)
        return {'FINISHED'}

class OT_ShowTorus2Preview(Operator):
    bl_idname = f"{OP_PREFIX}.show_torus2_preview"; bl_label = "Show Elliptic Torus"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props: props.t2_enable_preview = True; update_torus2_preview(context, props)
        return {'FINISHED'}

class OT_DetachTorus2(Operator):
    bl_idname = f"{OP_PREFIX}.detach_torus2"; bl_label = "Detach Elliptic Torus"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col_preview = bpy.data.collections.get(PREVIEW_COL_TORUS2)
        if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
        target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
        bpy.ops.object.select_all(action='DESELECT')
        for obj in list(col_preview.objects):
            if obj.name not in target_col.objects: target_col.objects.link(obj)
            col_preview.objects.unlink(obj)
            obj.name = obj.name.replace("[Preview]", "Solid_Elliptic_Torus") + f"_{timestamp}"
            if obj.data.materials:
                mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
                obj.data.materials[0] = new_mat
            bpy.context.view_layer.objects.active = obj; obj.select_set(True)
            bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
            obj.select_set(False)
        props = getattr(context.scene, PROPS_NAME, None)
        if props: update_torus2_preview(context, props)
        return {'FINISHED'}

class OT_ShowCross1Preview(Operator):
    bl_idname = f"{OP_PREFIX}.show_cross1_preview"; bl_label = "Show Cross 1"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props: props.c1_enable_preview = True; draw_single_cross(context, props, "c1")
        return {'FINISHED'}

class OT_DetachCross1(Operator):
    bl_idname = f"{OP_PREFIX}.detach_cross1"; bl_label = "Detach Cross 1"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col_preview = bpy.data.collections.get(PREVIEW_COL_CROSS1)
        if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
        target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
        bpy.ops.object.select_all(action='DESELECT')
        for obj in list(col_preview.objects):
            if obj.name not in target_col.objects: target_col.objects.link(obj)
            col_preview.objects.unlink(obj)
            obj.name = obj.name.replace("[Preview]", "Solid_Cross1") + f"_{timestamp}"
            if obj.data.materials:
                mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
                obj.data.materials[0] = new_mat
        props = getattr(context.scene, PROPS_NAME, None)
        if props: draw_single_cross(context, props, "c1")
        return {'FINISHED'}

class OT_ShowCross2Preview(Operator):
    bl_idname = f"{OP_PREFIX}.show_cross2_preview"; bl_label = "Show Cross 2"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props: props.c2_enable_preview = True; draw_single_cross(context, props, "c2")
        return {'FINISHED'}

class OT_DetachCross2(Operator):
    bl_idname = f"{OP_PREFIX}.detach_cross2"; bl_label = "Detach Cross 2"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col_preview = bpy.data.collections.get(PREVIEW_COL_CROSS2)
        if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
        target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
        bpy.ops.object.select_all(action='DESELECT')
        for obj in list(col_preview.objects):
            if obj.name not in target_col.objects: target_col.objects.link(obj)
            col_preview.objects.unlink(obj)
            obj.name = obj.name.replace("[Preview]", "Solid_Cross2") + f"_{timestamp}"
            if obj.data.materials:
                mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
                obj.data.materials[0] = new_mat
        props = getattr(context.scene, PROPS_NAME, None)
        if props: draw_single_cross(context, props, "c2")
        return {'FINISHED'}

class OT_CopyFullScript(Operator):
    bl_idname = f"{OP_PREFIX}.copy_script"
    bl_label = "Copy Script"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        target_text = None
        for t in bpy.data.texts:
            if SOURCE_ID_TAG in t.as_string(): target_text = t; break
        if not target_text: return {'CANCELLED'}

        code = target_text.as_string()
        c1, c2, c3 = props.color1, props.color2, props.color3
        t1c, t2c = props.t1_color, props.t2_color
        cc1_1, cc1_2 = props.c1_color1, props.c1_color2
        cc2_1, cc2_2 = props.c2_color1, props.c2_color2
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    # Line\n'
        new_dict += f'    "enable_preview": {props.enable_preview},\n'
        new_dict += f'    "val_a": {props.val_a:.4f}, "val_b": {props.val_b:.4f}, "val_d": {props.val_d:.4f},\n'
        new_dict += f'    "x_min": {props.x_min:.4f}, "x_max": {props.x_max:.4f},\n'
        new_dict += f'    "y_min": {props.y_min:.4f}, "y_max": {props.y_max:.4f},\n'
        new_dict += f'    "z_min": {props.z_min:.4f}, "z_max": {props.z_max:.4f},\n'
        new_dict += f'    "thickness": {props.thickness:.4f}, "draw_plane": "{props.draw_plane}",\n'
        new_dict += f'    "show_eq1": {props.show_eq1}, "show_eq2": {props.show_eq2}, "show_eq3": {props.show_eq3},\n'
        new_dict += f'    "color1": ({c1[0]:.4f}, {c1[1]:.4f}, {c1[2]:.4f}, {c1[3]:.4f}),\n'
        new_dict += f'    "color2": ({c2[0]:.4f}, {c2[1]:.4f}, {c2[2]:.4f}, {c2[3]:.4f}),\n'
        new_dict += f'    "color3": ({c3[0]:.4f}, {c3[1]:.4f}, {c3[2]:.4f}, {c3[3]:.4f}),\n\n'
        
        new_dict += f'    # Torus 1 (Normal)\n'
        new_dict += f'    "t1_enable_preview": {props.t1_enable_preview}, "t1_mode": "{props.t1_mode}",\n'
        new_dict += f'    "t1_val_a": {props.t1_val_a:.4f}, "t1_val_b": {props.t1_val_b:.4f},\n'
        new_dict += f'    "t1_z_min": {props.t1_z_min:.4f}, "t1_z_max": {props.t1_z_max:.4f}, "t1_count": {props.t1_count},\n'
        new_dict += f'    "t1_z_center": {props.t1_z_center:.4f}, "t1_z_interval": {props.t1_z_interval:.4f}, "t1_up_down_count": {props.t1_up_down_count},\n'
        new_dict += f'    "t1_major_radius": {props.t1_major_radius:.4f}, "t1_minor_radius": {props.t1_minor_radius:.4f},\n'
        new_dict += f'    "t1_color": ({t1c[0]:.4f}, {t1c[1]:.4f}, {t1c[2]:.4f}, {t1c[3]:.4f}),\n\n'

        new_dict += f'    # Torus 2 (Elliptic / Lorentz)\n'
        new_dict += f'    "t2_enable_preview": {props.t2_enable_preview}, "t2_mode": "{props.t2_mode}",\n'
        new_dict += f'    "t2_val_a": {props.t2_val_a:.4f}, "t2_val_b": {props.t2_val_b:.4f},\n'
        new_dict += f'    "t2_z_min": {props.t2_z_min:.4f}, "t2_z_max": {props.t2_z_max:.4f}, "t2_count": {props.t2_count},\n'
        new_dict += f'    "t2_z_center": {props.t2_z_center:.4f}, "t2_z_interval": {props.t2_z_interval:.4f}, "t2_up_down_count": {props.t2_up_down_count},\n'
        new_dict += f'    "t2_major_radius": {props.t2_major_radius:.4f}, "t2_minor_radius": {props.t2_minor_radius:.4f},\n'
        new_dict += f'    "t2_f": {props.t2_f:.4f}, "t2_g": {props.t2_g:.4f},\n'
        new_dict += f'    "t2_color": ({t2c[0]:.4f}, {t2c[1]:.4f}, {t2c[2]:.4f}, {t2c[3]:.4f}),\n\n'
        
        new_dict += f'    # Cross 1\n'
        new_dict += f'    "c1_enable_preview": {props.c1_enable_preview},\n'
        new_dict += f'    "c1_center": ({props.c1_center[0]:.4f}, {props.c1_center[1]:.4f}, {props.c1_center[2]:.4f}),\n'
        new_dict += f'    "c1_length": {props.c1_length:.4f}, "c1_thickness": {props.c1_thickness:.4f},\n'
        new_dict += f'    "c1_color1": ({cc1_1[0]:.4f}, {cc1_1[1]:.4f}, {cc1_1[2]:.4f}, {cc1_1[3]:.4f}),\n'
        new_dict += f'    "c1_color2": ({cc1_2[0]:.4f}, {cc1_2[1]:.4f}, {cc1_2[2]:.4f}, {cc1_2[3]:.4f}),\n\n'

        new_dict += f'    # Cross 2\n'
        new_dict += f'    "c2_enable_preview": {props.c2_enable_preview},\n'
        new_dict += f'    "c2_center": ({props.c2_center[0]:.4f}, {props.c2_center[1]:.4f}, {props.c2_center[2]:.4f}),\n'
        new_dict += f'    "c2_length": {props.c2_length:.4f}, "c2_thickness": {props.c2_thickness:.4f},\n'
        new_dict += f'    "c2_color1": ({cc2_1[0]:.4f}, {cc2_1[1]:.4f}, {cc2_1[2]:.4f}, {cc2_1[3]:.4f}),\n'
        new_dict += f'    "c2_color2": ({cc2_2[0]:.4f}, {cc2_2[1]:.4f}, {cc2_2[2]:.4f}, {cc2_2[3]:.4f}),\n'
        new_dict += "}\n"

        try:
            tag_start = "# <BEGIN" + "_DICT>"
            tag_end = "# <END" + "_DICT>"
            pre_code, rest = code.split(tag_start, 1)
            _, post_code = rest.split(tag_end, 1)
            final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
            context.window_manager.clipboard = final_code
            self.report({'INFO'}, "Code copied!")
        except: return {'CANCELLED'}
        return {'FINISHED'}

class OT_OpenUrl(Operator):
    bl_idname = f"{OP_PREFIX}.open_url"; bl_label = "Open URL"; url: StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

class OT_RemoveAddon(Operator):
    bl_idname = f"{OP_PREFIX}.remove_addon"; bl_label = "Close Addon"
    def execute(self, context):
        bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
        self.report({'INFO'}, "アドオンを終了しました。")
        return {'FINISHED'}

# ==============================================================================
#  PANELS
# ==============================================================================

class PT_MainPanel(Panel):
    bl_label = PANEL_TITLE
    bl_idname = f"{PREFIX}_PT_main"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        row_prev = layout.row(); row_prev.scale_y = 1.5
        if not props.enable_preview: row_prev.operator(OT_ShowLinePreview.bl_idname, icon='PLAY', text="Show Line Preview")
        else: row_prev.prop(props, "enable_preview", text="Line Preview Active", toggle=True, icon='PAUSE')

        box_info = layout.box()
        box_info.label(text="【 Line Info 】", icon='INFO')
        a_str = f"{props.val_a:.2f}" if abs(props.val_a) > 0.0001 else "0.00(Err)"
        box_info.label(text=f"y = ({props.val_b:.2f} / {a_str}) x")
        
        box_values = layout.box()
        col_v = box_values.column(align=True)
        col_v.prop(props, "val_a"); col_v.prop(props, "val_b"); col_v.prop(props, "val_d")
        
        box_limits = layout.box()
        box_limits.label(text="Limits (X, Y, Z)", icon='MOD_HULL')
        for axis in ['x', 'y', 'z']:
            r = box_limits.row(align=True)
            r.prop(props, f"{axis}_min", text=f"{axis.upper()} Min")
            r.prop(props, f"{axis}_max", text="Max")

class PT_VisibilityPanel(Panel):
    bl_label = "Line Design & Detach"
    bl_idname = f"{PREFIX}_PT_visibility"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_parent_id = f"{PREFIX}_PT_main"

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return
        layout.prop(props, "thickness"); layout.prop(props, "draw_plane"); layout.separator()
        for i in range(1, 4):
            r = layout.row(align=True)
            r.prop(props, f"show_eq{i}", text=f"Eq {i}", toggle=True); r.prop(props, f"color{i}", text="")
        
        layout.separator()
        col_exec = layout.column(); col_exec.scale_y = 1.5 
        col_exec.operator(OT_DetachLines.bl_idname, icon='MESH_CYLINDER', text="Detach Lines (固定化)")

class PT_Torus1Panel(Panel):
    bl_label = "Torus (Normal)"
    bl_idname = f"{PREFIX}_PT_torus1"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        row_prev = layout.row(); row_prev.scale_y = 1.5
        if not props.t1_enable_preview: row_prev.operator(OT_ShowTorus1Preview.bl_idname, icon='PLAY', text="Show Normal Torus")
        else: row_prev.prop(props, "t1_enable_preview", text="Normal Torus Active", toggle=True, icon='PAUSE')

        box_eq = layout.box()
        box_eq.label(text="Center Line: z = (b/a)x", icon='IPO_CONSTANT')
        col = box_eq.column(align=True)
        col.prop(props, "t1_val_a"); col.prop(props, "t1_val_b")

        box_r = layout.box()
        box_r.prop(props, "t1_mode", text="")
        if props.t1_mode == 'INTERVAL':
            c_int = box_r.column(align=True)
            c_int.prop(props, "t1_z_center"); c_int.prop(props, "t1_z_interval"); c_int.prop(props, "t1_up_down_count")
        else:
            c_rng = box_r.column(align=True)
            c_rng.prop(props, "t1_z_min"); c_rng.prop(props, "t1_z_max"); c_rng.prop(props, "t1_count")

        box_s = layout.box()
        box_s.prop(props, "t1_major_radius"); box_s.prop(props, "t1_minor_radius"); box_s.prop(props, "t1_color")

        col_exec = layout.column(); col_exec.scale_y = 1.5
        col_exec.operator(OT_DetachTorus1.bl_idname, icon='MESH_TORUS', text="Detach Normal Torus")

class PT_Torus2Panel(Panel):
    bl_label = "Torus (Elliptic / Lorentz)"
    bl_idname = f"{PREFIX}_PT_torus2"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        row_prev = layout.row(); row_prev.scale_y = 1.5
        if not props.t2_enable_preview: row_prev.operator(OT_ShowTorus2Preview.bl_idname, icon='PLAY', text="Show Elliptic Torus")
        else: row_prev.prop(props, "t2_enable_preview", text="Elliptic Torus Active", toggle=True, icon='PAUSE')

        box_eq = layout.box()
        box_eq.label(text="Center Line: z = (b/a)x", icon='IPO_CONSTANT')
        col = box_eq.column(align=True)
        col.prop(props, "t2_val_a"); col.prop(props, "t2_val_b")

        box_r = layout.box()
        box_r.prop(props, "t2_mode", text="")
        if props.t2_mode == 'INTERVAL':
            c_int = box_r.column(align=True)
            c_int.prop(props, "t2_z_center"); c_int.prop(props, "t2_z_interval"); c_int.prop(props, "t2_up_down_count")
        else:
            c_rng = box_r.column(align=True)
            c_rng.prop(props, "t2_z_min"); c_rng.prop(props, "t2_z_max"); c_rng.prop(props, "t2_count")

        box_s = layout.box()
        box_s.prop(props, "t2_major_radius"); box_s.prop(props, "t2_minor_radius")
        box_s.separator()
        
        box_s.label(text="楕円トーラスの収縮割合 (f/g)", icon='CON_SIZELIKE')
        col_fg = box_s.column(align=True)
        col_fg.prop(props, "t2_f")
        col_fg.prop(props, "t2_g")
        g_val = props.t2_g if abs(props.t2_g) > 0.0001 else 0.0001
        box_s.label(text=f"収縮率: {props.t2_f / g_val:.4f}")
        
        box_s.prop(props, "t2_color")

        col_exec = layout.column(); col_exec.scale_y = 1.5
        col_exec.operator(OT_DetachTorus2.bl_idname, icon='MESH_TORUS', text="Detach Elliptic Torus")

class PT_Cross1Panel(Panel):
    bl_label = "Cross Cylinders 1"
    bl_idname = f"{PREFIX}_PT_cross1"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        row_prev = layout.row(); row_prev.scale_y = 1.5
        if not props.c1_enable_preview: row_prev.operator(OT_ShowCross1Preview.bl_idname, icon='PLAY', text="Show Cross 1")
        else: row_prev.prop(props, "c1_enable_preview", text="Cross 1 Active", toggle=True, icon='PAUSE')

        box_s = layout.box()
        box_s.prop(props, "c1_center")
        box_s.prop(props, "c1_length")
        box_s.prop(props, "c1_thickness")
        
        box_s.separator()
        r1 = box_s.row(align=True)
        r1.label(text="z = x"); r1.prop(props, "c1_color1", text="")
        r2 = box_s.row(align=True)
        r2.label(text="z = -x"); r2.prop(props, "c1_color2", text="")

        col_exec = layout.column(); col_exec.scale_y = 1.5
        col_exec.operator(OT_DetachCross1.bl_idname, icon='MESH_DATA', text="Detach Cross 1")

class PT_Cross2Panel(Panel):
    bl_label = "Cross Cylinders 2"
    bl_idname = f"{PREFIX}_PT_cross2"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        row_prev = layout.row(); row_prev.scale_y = 1.5
        if not props.c2_enable_preview: row_prev.operator(OT_ShowCross2Preview.bl_idname, icon='PLAY', text="Show Cross 2")
        else: row_prev.prop(props, "c2_enable_preview", text="Cross 2 Active", toggle=True, icon='PAUSE')

        box_s = layout.box()
        box_s.prop(props, "c2_center")
        box_s.prop(props, "c2_length")
        box_s.prop(props, "c2_thickness")
        
        box_s.separator()
        r1 = box_s.row(align=True)
        r1.label(text="z = x"); r1.prop(props, "c2_color1", text="")
        r2 = box_s.row(align=True)
        r2.label(text="z = -x"); r2.prop(props, "c2_color2", text="")

        col_exec = layout.column(); col_exec.scale_y = 1.5
        col_exec.operator(OT_DetachCross2.bl_idname, icon='MESH_DATA', text="Detach Cross 2")

class PT_SystemPanel(Panel):
    bl_label = "System (Copy / Close)"
    bl_idname = f"{PREFIX}_PT_system"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        r_c = layout.row(); r_c.scale_y = 1.2
        r_c.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
        r_r = layout.row(); r_r.scale_y = 1.2; r_r.alert = True 
        r_r.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Close Addon (完全終了)")

class PT_LinksPanel(Panel):
    bl_label = "Links"
    bl_idname = f"{PREFIX}_PT_links"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        for l in ADDON_LINKS:
            self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"], icon='URL').url = l["url"]

# ==============================================================================
#  REGISTER
# ==============================================================================

classes = (
    PG_EquationProps, 
    OT_ShowLinePreview, OT_DetachLines, 
    OT_ShowTorus1Preview, OT_DetachTorus1,
    OT_ShowTorus2Preview, OT_DetachTorus2,
    OT_ShowCross1Preview, OT_DetachCross1,
    OT_ShowCross2Preview, OT_DetachCross2,
    OT_CopyFullScript, OT_OpenUrl, OT_RemoveAddon, 
    PT_MainPanel, PT_VisibilityPanel, 
    PT_Torus1Panel, PT_Torus2Panel, 
    PT_Cross1Panel, PT_Cross2Panel,
    PT_SystemPanel, PT_LinksPanel
)

def auto_open_sidebar():
    try:
        for window in bpy.context.window_manager.windows:
            for area in window.screen.areas:
                if area.type == 'VIEW_3D':
                    for space in area.spaces:
                        if space.type == 'VIEW_3D':
                            if not space.show_region_ui: space.show_region_ui = True
    except: pass
    return None

def register():
    for c in classes: 
        try: bpy.utils.register_class(c)
        except ValueError: pass
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_EquationProps))
    bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)

def unregister():
    global _timer
    if _timer is not None:
        try: bpy.app.timers.unregister(_timer)
        except Exception: pass
        _timer = None
    cleanup_preview_data()
    if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
    for c in reversed(classes): 
        try: bpy.utils.unregister_class(c)
        except ValueError: pass

if __name__ == "__main__": 
    register()
# Copied: 2026-04-08 12:00:00
import bpy
import time
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime

# ==============================================================================
#  【 基本設定エリア 】
# ==============================================================================

PREFIX       = "EqGen"
ADDON_NAME   = "[ Equation Gen ]"
TAB_NAME     = "[ Equation Gen ]"
PANEL_TITLE  = "Equation Lines"
AUTHOR       = "zionadchat"

# ★ このスクリプト自身のID(絶対に消さないこと)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: EQ_LINES_2026_04_08_V6 ###"

# ==============================================================================
#  システム初期化 & ID管理
# ==============================================================================

bl_info = {
    "name": f"{ADDON_NAME} {PREFIX}",
    "author": AUTHOR,
    "version": (6, 0, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": "Lines, Torus, Elliptic Torus, and Cross Cylinders Generators",
    "category": "3D View",
}

OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"

ADDON_LINKS = (
    {"label": "Prefix 20260408", "url": "<https://www.notion.so/Prefix-20260408-33cf5dacaf43807e9e35ff8cdbbc39c6>"},
)

# ==============================================================================
#  デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    # Line
    "enable_preview": False,
    "val_a": 0.6000, "val_b": 1.0000, "val_d": 10.0000,
    "x_min": -50.0, "x_max": 50.0,
    "y_min": -50.0, "y_max": 50.0,
    "z_min": -50.0, "z_max": 50.0,
    "thickness": 0.5000, "draw_plane": "XZ",
    "show_eq1": True, "show_eq2": True, "show_eq3": True,
    "color1": (1.0000, 0.2000, 0.2000, 1.0000),
    "color2": (0.2000, 1.0000, 0.2000, 1.0000),
    "color3": (0.2000, 0.2000, 1.0000, 1.0000),

    # Torus 1 (Normal)
    "t1_enable_preview": False,
    "t1_mode": "INTERVAL",
    "t1_val_a": 0.6000, "t1_val_b": 1.0000,
    "t1_z_min": -50.0, "t1_z_max": 50.0, "t1_count": 11,
    "t1_z_center": 0.0, "t1_z_interval": 1.0, "t1_up_down_count": 5,
    "t1_major_radius": 5.0, "t1_minor_radius": 1.0,
    "t1_color": (0.2000, 0.8000, 0.8000, 1.0000), 

    # Torus 2 (Elliptic / Lorentz)
    "t2_enable_preview": False,
    "t2_mode": "INTERVAL",
    "t2_val_a": 0.6000, "t2_val_b": 1.0000,
    "t2_z_min": -50.0, "t2_z_max": 50.0, "t2_count": 11,
    "t2_z_center": 0.0, "t2_z_interval": 1.0, "t2_up_down_count": 5,
    "t2_major_radius": 5.0, "t2_minor_radius": 1.0,
    "t2_f": 1.0000, "t2_g": 1.6000, # 収縮率 f/g
    "t2_color": (0.8000, 0.2000, 0.8000, 1.0000),

    # Cross 1 (y=x, y=-x)
    "c1_enable_preview": False,
    "c1_center": (0.0, 0.0, 0.0),
    "c1_length": 50.0, "c1_thickness": 0.5000,
    "c1_plane": "XY",
    "c1_color1": (1.0000, 1.0000, 0.2000, 1.0000),
    "c1_color2": (1.0000, 0.5000, 0.2000, 1.0000),

    # Cross 2 (y=x, y=-x)
    "c2_enable_preview": False,
    "c2_center": (10.0, 10.0, 0.0),
    "c2_length": 30.0, "c2_thickness": 0.5000,
    "c2_plane": "XY",
    "c2_color1": (0.2000, 1.0000, 0.2000, 1.0000),
    "c2_color2": (0.2000, 0.5000, 1.0000, 1.0000),
}
# <END_DICT>

PREVIEW_COL_LINE = f"{PREFIX}_Line_Preview"
PREVIEW_COL_TORUS1 = f"{PREFIX}_Torus1_Preview"
PREVIEW_COL_TORUS2 = f"{PREFIX}_Torus2_Preview"
PREVIEW_COL_CROSS1 = f"{PREFIX}_Cross1_Preview"
PREVIEW_COL_CROSS2 = f"{PREFIX}_Cross2_Preview"

# ==============================================================================
#  共通マテリアル・データ管理 ロジック
# ==============================================================================

def cleanup_preview_data():
    for name in [PREVIEW_COL_LINE, PREVIEW_COL_TORUS1, PREVIEW_COL_TORUS2, PREVIEW_COL_CROSS1, PREVIEW_COL_CROSS2]:
        col = bpy.data.collections.get(name)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0:
                    if isinstance(data, bpy.types.Curve):
                        bpy.data.curves.remove(data)
            if len(col.objects) == 0:
                bpy.data.collections.remove(col)

def apply_material_settings(mat, color):
    mat.use_nodes = True
    mat.blend_method = 'BLEND'
    mat.diffuse_color = color
    
    tree = mat.node_tree
    bsdf = tree.nodes.get("Principled BSDF")
    if not bsdf:
        tree.nodes.clear()
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        bsdf.name = "Principled BSDF"
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        tree.links.new(bsdf.outputs[0], out.inputs[0])
        
    if "Base Color" in bsdf.inputs:
        bsdf.inputs["Base Color"].default_value = color
    if "Alpha" in bsdf.inputs:
        bsdf.inputs["Alpha"].default_value = color[3]

def get_preview_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name=name)
    apply_material_settings(mat, color)
    return mat

def build_curve_circle(curve, radius, segments=32):
    if len(curve.splines) == 0:
        spline = curve.splines.new('POLY')
        spline.points.add(segments - 1)
        spline.use_cyclic_u = True
    else:
        spline = curve.splines[0]
        if len(spline.points) != segments:
            curve.splines.clear()
            spline = curve.splines.new('POLY')
            spline.points.add(segments - 1)
            spline.use_cyclic_u = True
            
    for i in range(segments):
        angle = 2 * math.pi * i / segments
        spline.points[i].co = (radius * math.cos(angle), radius * math.sin(angle), 0.0, 1.0)

# ==============================================================================
#  Line プレビューロジック
# ==============================================================================

def get_clipped_segment(m, c, x_min, x_max, v_min, v_max):
    if m == 0:
        if v_min <= c <= v_max: return x_min, x_max
        return None, None
    else:
        x_from_v1, x_from_v2 = (v_min - c) / m, (v_max - c) / m
        valid_x_min, valid_x_max = min(x_from_v1, x_from_v2), max(x_from_v1, x_from_v2)
        act_x_min, act_x_max = max(x_min, valid_x_min), min(x_max, valid_x_max)
        if act_x_min > act_x_max: return None, None
        return act_x_min, act_x_max

def calc_points(props, m, c):
    x_min, x_max = min(props.x_min, props.x_max), max(props.x_min, props.x_max)
    y_min, y_max = min(props.y_min, props.y_max), max(props.y_min, props.y_max)
    z_min, z_max = min(props.z_min, props.z_max), max(props.z_min, props.z_max)
    
    if props.draw_plane == 'XZ':
        act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, z_min, z_max)
        if act_x_min is None: return None, None
        return (act_x_min, 0.0, m * act_x_min + c), (act_x_max, 0.0, m * act_x_max + c)
    else:
        act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, y_min, y_max)
        if act_x_min is None: return None, None
        return (act_x_min, m * act_x_min + c, 0.0), (act_x_max, m * act_x_max + c, 0.0)

def update_line_preview(context, props):
    if not props.enable_preview:
        col = bpy.data.collections.get(PREVIEW_COL_LINE)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0: bpy.data.curves.remove(data)
            bpy.data.collections.remove(col)
        return

    col = bpy.data.collections.get(PREVIEW_COL_LINE)
    if not col:
        col = bpy.data.collections.new(PREVIEW_COL_LINE)
        context.scene.collection.children.link(col)

    a = props.val_a if abs(props.val_a) > 0.0001 else 0.0001
    b, d = props.val_b, props.val_d
    m = b / a

    equations = [
        {"id": 1, "show": props.show_eq1, "color": props.color1, "offset": 0.0},
        {"id": 2, "show": props.show_eq2, "color": props.color2, "offset": -d},
        {"id": 3, "show": props.show_eq3, "color": props.color3, "offset": d},
    ]

    for eq in equations:
        obj_name = f"[Preview] EqLine_{eq['id']}"
        obj = bpy.data.objects.get(obj_name)

        if not eq["show"]:
            if obj: obj.hide_viewport = obj.hide_render = True
            continue

        p1, p2 = calc_points(props, m, eq["offset"])
        if p1 is None:
            if obj: obj.hide_viewport = obj.hide_render = True
            continue

        if not obj:
            curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
            curve.dimensions = '3D'; curve.fill_mode = 'FULL'
            spline = curve.splines.new('POLY'); spline.points.add(1)
            obj = bpy.data.objects.new(obj_name, curve)
            col.objects.link(obj)
        else:
            curve = obj.data; spline = curve.splines[0]

        curve.bevel_depth = props.thickness; curve.bevel_resolution = 6
        spline.points[0].co = (*p1, 1.0); spline.points[1].co = (*p2, 1.0)
        obj.hide_viewport = obj.hide_render = False

        mat = get_preview_material(f"Preview_Mat_EqLine{eq['id']}", eq["color"])
        if not obj.data.materials: obj.data.materials.append(mat)
        else: obj.data.materials[0] = mat

# ==============================================================================
#  Torus プレビューロジック (Normal & Elliptic)
# ==============================================================================

def update_torus1_preview(context, props):
    if not props.t1_enable_preview:
        col = bpy.data.collections.get(PREVIEW_COL_TORUS1)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0: bpy.data.curves.remove(data)
            bpy.data.collections.remove(col)
        return

    col = bpy.data.collections.get(PREVIEW_COL_TORUS1)
    if not col:
        col = bpy.data.collections.new(PREVIEW_COL_TORUS1)
        context.scene.collection.children.link(col)

    a = props.t1_val_a
    b_val = props.t1_val_b if abs(props.t1_val_b) > 0.0001 else 0.0001
    
    z_list = []
    if props.t1_mode == 'RANGE':
        count = props.t1_count
        for i in range(count):
            t = i / (count - 1) if count > 1 else 0.5
            z_list.append(props.t1_z_min + t * (props.t1_z_max - props.t1_z_min))
    else: 
        c, interval, ud = props.t1_z_center, props.t1_z_interval, props.t1_up_down_count
        for i in range(-ud, ud + 1): z_list.append(c + i * interval)

    existing_objs = list(col.objects)
    mat = get_preview_material("Preview_Mat_Torus1", props.t1_color)

    for i, z in enumerate(z_list):
        obj_name = f"[Preview] Normal_Torus_{i+1}"
        x = z * (a / b_val); y = 0.0 
        
        if i < len(existing_objs):
            obj = existing_objs[i]
            curve = obj.data
        else:
            curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
            curve.dimensions = '3D'; curve.fill_mode = 'FULL'
            obj = bpy.data.objects.new(obj_name, curve)
            col.objects.link(obj)
            
        curve.bevel_depth = props.t1_minor_radius; curve.bevel_resolution = 8
        build_curve_circle(curve, props.t1_major_radius)
        
        obj.location = (x, y, z); obj.scale = (1.0, 1.0, 1.0)
        obj.hide_viewport = obj.hide_render = False
        
        if not obj.data.materials: obj.data.materials.append(mat)
        else: obj.data.materials[0] = mat

    if len(existing_objs) > len(z_list):
        for obj in existing_objs[len(z_list):]:
            data = obj.data
            bpy.data.objects.remove(obj, do_unlink=True)
            if data and data.users == 0: bpy.data.curves.remove(data)

def update_torus2_preview(context, props):
    if not props.t2_enable_preview:
        col = bpy.data.collections.get(PREVIEW_COL_TORUS2)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0: bpy.data.curves.remove(data)
            bpy.data.collections.remove(col)
        return

    col = bpy.data.collections.get(PREVIEW_COL_TORUS2)
    if not col:
        col = bpy.data.collections.new(PREVIEW_COL_TORUS2)
        context.scene.collection.children.link(col)

    a = props.t2_val_a
    b_val = props.t2_val_b if abs(props.t2_val_b) > 0.0001 else 0.0001
    
    # 収縮率計算
    g_val = props.t2_g if abs(props.t2_g) > 0.0001 else 0.0001
    scale_x = props.t2_f / g_val
    
    z_list = []
    if props.t2_mode == 'RANGE':
        count = props.t2_count
        for i in range(count):
            t = i / (count - 1) if count > 1 else 0.5
            z_list.append(props.t2_z_min + t * (props.t2_z_max - props.t2_z_min))
    else: 
        c, interval, ud = props.t2_z_center, props.t2_z_interval, props.t2_up_down_count
        for i in range(-ud, ud + 1): z_list.append(c + i * interval)

    existing_objs = list(col.objects)
    mat = get_preview_material("Preview_Mat_Torus2", props.t2_color)

    for i, z in enumerate(z_list):
        obj_name = f"[Preview] Elliptic_Torus_{i+1}"
        x = z * (a / b_val); y = 0.0 
        
        if i < len(existing_objs):
            obj = existing_objs[i]
            curve = obj.data
        else:
            curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
            curve.dimensions = '3D'; curve.fill_mode = 'FULL'
            obj = bpy.data.objects.new(obj_name, curve)
            col.objects.link(obj)
            
        curve.bevel_depth = props.t2_minor_radius; curve.bevel_resolution = 8
        build_curve_circle(curve, props.t2_major_radius)
        
        obj.location = (x, y, z)
        obj.scale = (scale_x, 1.0, 1.0) # Lorentz Contraction
        obj.hide_viewport = obj.hide_render = False
        
        if not obj.data.materials: obj.data.materials.append(mat)
        else: obj.data.materials[0] = mat

    if len(existing_objs) > len(z_list):
        for obj in existing_objs[len(z_list):]:
            data = obj.data
            bpy.data.objects.remove(obj, do_unlink=True)
            if data and data.users == 0: bpy.data.curves.remove(data)

# ==============================================================================
#  Cross (y=x, y=-x) プレビューロジック
# ==============================================================================

def draw_single_cross(context, props, prefix):
    enable = getattr(props, f"{prefix}_enable_preview")
    col_name = PREVIEW_COL_CROSS1 if prefix == "c1" else PREVIEW_COL_CROSS2
    
    if not enable:
        col = bpy.data.collections.get(col_name)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0: bpy.data.curves.remove(data)
            bpy.data.collections.remove(col)
        return

    col = bpy.data.collections.get(col_name)
    if not col:
        col = bpy.data.collections.new(col_name)
        context.scene.collection.children.link(col)

    center = getattr(props, f"{prefix}_center")
    length = getattr(props, f"{prefix}_length")
    thickness = getattr(props, f"{prefix}_thickness")
    plane = getattr(props, f"{prefix}_plane")
    c1 = getattr(props, f"{prefix}_color1")
    c2 = getattr(props, f"{prefix}_color2")

    cx, cy, cz = center[0], center[1], center[2]

    # 線分の計算
    if plane == 'XY':
        pts = [
            ((cx-length, cy-length, cz), (cx+length, cy+length, cz)), # y = x
            ((cx-length, cy+length, cz), (cx+length, cy-length, cz))  # y = -x
        ]
    else: # XZ
        pts = [
            ((cx-length, cy, cz-length), (cx+length, cy, cz+length)), # z = x
            ((cx-length, cy, cz+length), (cx+length, cy, cz-length))  # z = -x
        ]
        
    colors = [c1, c2]
    
    for i in range(2):
        obj_name = f"[Preview] {prefix.capitalize()}_Line{i+1}"
        obj = bpy.data.objects.get(obj_name)

        if not obj:
            curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
            curve.dimensions = '3D'; curve.fill_mode = 'FULL'
            spline = curve.splines.new('POLY'); spline.points.add(1)
            obj = bpy.data.objects.new(obj_name, curve)
            col.objects.link(obj)
        else:
            curve = obj.data; spline = curve.splines[0]

        curve.bevel_depth = thickness; curve.bevel_resolution = 6
        spline.points[0].co = (*pts[i][0], 1.0)
        spline.points[1].co = (*pts[i][1], 1.0)
        obj.hide_viewport = obj.hide_render = False

        mat = get_preview_material(f"Preview_Mat_{prefix.capitalize()}_L{i+1}", colors[i])
        if not obj.data.materials: obj.data.materials.append(mat)
        else: obj.data.materials[0] = mat

# ==============================================================================
#  タイマー管理
# ==============================================================================

_timer = None
_last_update_time = 0

def delayed_update():
    global _timer, _last_update_time
    _timer = None
    now = time.time()
    if now - _last_update_time < 0.05:
        if _timer is None:
            _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
        return None
    _last_update_time = now
    
    ctx = bpy.context
    if not ctx or not ctx.scene: return None
    if ctx.object and ctx.object.mode != 'OBJECT': return None

    props = getattr(ctx.scene, PROPS_NAME, None)
    if props:
        update_line_preview(ctx, props)
        update_torus1_preview(ctx, props)
        update_torus2_preview(ctx, props)
        draw_single_cross(ctx, props, "c1")
        draw_single_cross(ctx, props, "c2")
    return None

def on_update(self, context):
    global _timer
    if _timer is None: 
        _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)

# ==============================================================================
#  PROPERTIES
# ==============================================================================

class PG_EquationProps(PropertyGroup):
    # Line Properties
    enable_preview: BoolProperty(name="Enable Line Preview", default=CURRENT_DEFAULTS['enable_preview'], update=on_update)
    val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['val_a'], update=on_update)
    val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['val_b'], update=on_update)
    val_d: FloatProperty(name="Value d", default=CURRENT_DEFAULTS['val_d'], update=on_update)
    x_min: FloatProperty(name="X Min", default=CURRENT_DEFAULTS['x_min'], update=on_update)
    x_max: FloatProperty(name="X Max", default=CURRENT_DEFAULTS['x_max'], update=on_update)
    y_min: FloatProperty(name="Y Min", default=CURRENT_DEFAULTS['y_min'], update=on_update)
    y_max: FloatProperty(name="Y Max", default=CURRENT_DEFAULTS['y_max'], update=on_update)
    z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['z_min'], update=on_update)
    z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['z_max'], update=on_update)
    thickness: FloatProperty(name="Line Thickness", default=CURRENT_DEFAULTS['thickness'], min=0.01, max=10.0, update=on_update)
    draw_plane: EnumProperty(name="Draw Plane", items=[('XZ', "Front (XZ)", "XZ Plane"), ('XY', "Top (XY)", "XY Plane")], default=CURRENT_DEFAULTS['draw_plane'], update=on_update)
    show_eq1: BoolProperty(name="Show Eq 1", default=CURRENT_DEFAULTS['show_eq1'], update=on_update)
    show_eq2: BoolProperty(name="Show Eq 2", default=CURRENT_DEFAULTS['show_eq2'], update=on_update)
    show_eq3: BoolProperty(name="Show Eq 3", default=CURRENT_DEFAULTS['show_eq3'], update=on_update)
    color1: FloatVectorProperty(name="Color 1", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color1'], update=on_update)
    color2: FloatVectorProperty(name="Color 2", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color2'], update=on_update)
    color3: FloatVectorProperty(name="Color 3", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color3'], update=on_update)

    # Torus 1 (Normal)
    t1_enable_preview: BoolProperty(name="Enable Torus 1 Preview", default=CURRENT_DEFAULTS['t1_enable_preview'], update=on_update)
    t1_mode: EnumProperty(name="Mode", items=[('INTERVAL', "Interval Mode", ""), ('RANGE', "Range Mode", "")], default=CURRENT_DEFAULTS['t1_mode'], update=on_update)
    t1_val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['t1_val_a'], update=on_update)
    t1_val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['t1_val_b'], update=on_update)
    t1_z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['t1_z_min'], update=on_update)
    t1_z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['t1_z_max'], update=on_update)
    t1_count: IntProperty(name="Total Count", default=CURRENT_DEFAULTS['t1_count'], min=1, max=500, update=on_update)
    t1_z_center: FloatProperty(name="Z Center", default=CURRENT_DEFAULTS['t1_z_center'], update=on_update)
    t1_z_interval: FloatProperty(name="Z Interval", default=CURRENT_DEFAULTS['t1_z_interval'], update=on_update)
    t1_up_down_count: IntProperty(name="Up/Down Count", default=CURRENT_DEFAULTS['t1_up_down_count'], min=0, max=100, update=on_update)
    t1_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['t1_major_radius'], min=0.1, max=100.0, update=on_update)
    t1_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['t1_minor_radius'], min=0.01, max=50.0, update=on_update)
    t1_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['t1_color'], update=on_update)

    # Torus 2 (Elliptic)
    t2_enable_preview: BoolProperty(name="Enable Torus 2 Preview", default=CURRENT_DEFAULTS['t2_enable_preview'], update=on_update)
    t2_mode: EnumProperty(name="Mode", items=[('INTERVAL', "Interval Mode", ""), ('RANGE', "Range Mode", "")], default=CURRENT_DEFAULTS['t2_mode'], update=on_update)
    t2_val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['t2_val_a'], update=on_update)
    t2_val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['t2_val_b'], update=on_update)
    t2_z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['t2_z_min'], update=on_update)
    t2_z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['t2_z_max'], update=on_update)
    t2_count: IntProperty(name="Total Count", default=CURRENT_DEFAULTS['t2_count'], min=1, max=500, update=on_update)
    t2_z_center: FloatProperty(name="Z Center", default=CURRENT_DEFAULTS['t2_z_center'], update=on_update)
    t2_z_interval: FloatProperty(name="Z Interval", default=CURRENT_DEFAULTS['t2_z_interval'], update=on_update)
    t2_up_down_count: IntProperty(name="Up/Down Count", default=CURRENT_DEFAULTS['t2_up_down_count'], min=0, max=100, update=on_update)
    t2_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['t2_major_radius'], min=0.1, max=100.0, update=on_update)
    t2_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['t2_minor_radius'], min=0.01, max=50.0, update=on_update)
    t2_f: FloatProperty(name="f (Numerator)", default=CURRENT_DEFAULTS['t2_f'], min=0.01, update=on_update)
    t2_g: FloatProperty(name="g (Denominator)", default=CURRENT_DEFAULTS['t2_g'], min=0.01, update=on_update)
    t2_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['t2_color'], update=on_update)

    # Cross 1
    c1_enable_preview: BoolProperty(name="Enable Cross 1 Preview", default=CURRENT_DEFAULTS['c1_enable_preview'], update=on_update)
    c1_center: FloatVectorProperty(name="Center Position", size=3, default=CURRENT_DEFAULTS['c1_center'], update=on_update)
    c1_length: FloatProperty(name="Length (Radius)", default=CURRENT_DEFAULTS['c1_length'], min=0.1, update=on_update)
    c1_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['c1_thickness'], min=0.01, update=on_update)
    c1_plane: EnumProperty(name="Draw Plane", items=[('XY', "XY Plane (y=x)", ""), ('XZ', "XZ Plane (z=x)", "")], default=CURRENT_DEFAULTS['c1_plane'], update=on_update)
    c1_color1: FloatVectorProperty(name="Color y=x", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['c1_color1'], update=on_update)
    c1_color2: FloatVectorProperty(name="Color y=-x", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['c1_color2'], update=on_update)

    # Cross 2
    c2_enable_preview: BoolProperty(name="Enable Cross 2 Preview", default=CURRENT_DEFAULTS['c2_enable_preview'], update=on_update)
    c2_center: FloatVectorProperty(name="Center Position", size=3, default=CURRENT_DEFAULTS['c2_center'], update=on_update)
    c2_length: FloatProperty(name="Length (Radius)", default=CURRENT_DEFAULTS['c2_length'], min=0.1, update=on_update)
    c2_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['c2_thickness'], min=0.01, update=on_update)
    c2_plane: EnumProperty(name="Draw Plane", items=[('XY', "XY Plane (y=x)", ""), ('XZ', "XZ Plane (z=x)", "")], default=CURRENT_DEFAULTS['c2_plane'], update=on_update)
    c2_color1: FloatVectorProperty(name="Color y=x", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['c2_color1'], update=on_update)
    c2_color2: FloatVectorProperty(name="Color y=-x", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['c2_color2'], update=on_update)

# ==============================================================================
#  OPERATORS
# ==============================================================================

class OT_ShowLinePreview(Operator):
    bl_idname = f"{OP_PREFIX}.show_line_preview"; bl_label = "Show Line Preview"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props: props.enable_preview = True; update_line_preview(context, props)
        return {'FINISHED'}

class OT_DetachLines(Operator):
    bl_idname = f"{OP_PREFIX}.detach_lines"; bl_label = "Detach Lines"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col_preview = bpy.data.collections.get(PREVIEW_COL_LINE)
        if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
        target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
        bpy.ops.object.select_all(action='DESELECT')
        for obj in list(col_preview.objects):
            if obj.hide_viewport: continue
            if obj.name not in target_col.objects: target_col.objects.link(obj)
            col_preview.objects.unlink(obj)
            obj.name = obj.name.replace("[Preview]", "Solid_EqLine") + f"_{timestamp}"
            if obj.data.materials:
                mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
                obj.data.materials[0] = new_mat
            obj.select_set(True)
        props = getattr(context.scene, PROPS_NAME, None)
        if props: update_line_preview(context, props)
        return {'FINISHED'}

class OT_ShowTorus1Preview(Operator):
    bl_idname = f"{OP_PREFIX}.show_torus1_preview"; bl_label = "Show Normal Torus"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props: props.t1_enable_preview = True; update_torus1_preview(context, props)
        return {'FINISHED'}

class OT_DetachTorus1(Operator):
    bl_idname = f"{OP_PREFIX}.detach_torus1"; bl_label = "Detach Normal Torus"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col_preview = bpy.data.collections.get(PREVIEW_COL_TORUS1)
        if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
        target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
        bpy.ops.object.select_all(action='DESELECT')
        for obj in list(col_preview.objects):
            if obj.name not in target_col.objects: target_col.objects.link(obj)
            col_preview.objects.unlink(obj)
            obj.name = obj.name.replace("[Preview]", "Solid_Normal_Torus") + f"_{timestamp}"
            if obj.data.materials:
                mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
                obj.data.materials[0] = new_mat
            bpy.context.view_layer.objects.active = obj; obj.select_set(True)
            bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
            obj.select_set(False)
        props = getattr(context.scene, PROPS_NAME, None)
        if props: update_torus1_preview(context, props)
        return {'FINISHED'}

class OT_ShowTorus2Preview(Operator):
    bl_idname = f"{OP_PREFIX}.show_torus2_preview"; bl_label = "Show Elliptic Torus"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props: props.t2_enable_preview = True; update_torus2_preview(context, props)
        return {'FINISHED'}

class OT_DetachTorus2(Operator):
    bl_idname = f"{OP_PREFIX}.detach_torus2"; bl_label = "Detach Elliptic Torus"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col_preview = bpy.data.collections.get(PREVIEW_COL_TORUS2)
        if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
        target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
        bpy.ops.object.select_all(action='DESELECT')
        for obj in list(col_preview.objects):
            if obj.name not in target_col.objects: target_col.objects.link(obj)
            col_preview.objects.unlink(obj)
            obj.name = obj.name.replace("[Preview]", "Solid_Elliptic_Torus") + f"_{timestamp}"
            if obj.data.materials:
                mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
                obj.data.materials[0] = new_mat
            bpy.context.view_layer.objects.active = obj; obj.select_set(True)
            bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
            obj.select_set(False)
        props = getattr(context.scene, PROPS_NAME, None)
        if props: update_torus2_preview(context, props)
        return {'FINISHED'}

class OT_ShowCross1Preview(Operator):
    bl_idname = f"{OP_PREFIX}.show_cross1_preview"; bl_label = "Show Cross 1"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props: props.c1_enable_preview = True; draw_single_cross(context, props, "c1")
        return {'FINISHED'}

class OT_DetachCross1(Operator):
    bl_idname = f"{OP_PREFIX}.detach_cross1"; bl_label = "Detach Cross 1"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col_preview = bpy.data.collections.get(PREVIEW_COL_CROSS1)
        if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
        target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
        bpy.ops.object.select_all(action='DESELECT')
        for obj in list(col_preview.objects):
            if obj.name not in target_col.objects: target_col.objects.link(obj)
            col_preview.objects.unlink(obj)
            obj.name = obj.name.replace("[Preview]", "Solid_Cross1") + f"_{timestamp}"
            if obj.data.materials:
                mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
                obj.data.materials[0] = new_mat
        props = getattr(context.scene, PROPS_NAME, None)
        if props: draw_single_cross(context, props, "c1")
        return {'FINISHED'}

class OT_ShowCross2Preview(Operator):
    bl_idname = f"{OP_PREFIX}.show_cross2_preview"; bl_label = "Show Cross 2"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props: props.c2_enable_preview = True; draw_single_cross(context, props, "c2")
        return {'FINISHED'}

class OT_DetachCross2(Operator):
    bl_idname = f"{OP_PREFIX}.detach_cross2"; bl_label = "Detach Cross 2"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col_preview = bpy.data.collections.get(PREVIEW_COL_CROSS2)
        if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
        target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
        bpy.ops.object.select_all(action='DESELECT')
        for obj in list(col_preview.objects):
            if obj.name not in target_col.objects: target_col.objects.link(obj)
            col_preview.objects.unlink(obj)
            obj.name = obj.name.replace("[Preview]", "Solid_Cross2") + f"_{timestamp}"
            if obj.data.materials:
                mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
                obj.data.materials[0] = new_mat
        props = getattr(context.scene, PROPS_NAME, None)
        if props: draw_single_cross(context, props, "c2")
        return {'FINISHED'}

class OT_CopyFullScript(Operator):
    bl_idname = f"{OP_PREFIX}.copy_script"
    bl_label = "Copy Script"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        target_text = None
        for t in bpy.data.texts:
            if SOURCE_ID_TAG in t.as_string(): target_text = t; break
        if not target_text: return {'CANCELLED'}

        code = target_text.as_string()
        c1, c2, c3 = props.color1, props.color2, props.color3
        t1c, t2c = props.t1_color, props.t2_color
        cc1_1, cc1_2 = props.c1_color1, props.c1_color2
        cc2_1, cc2_2 = props.c2_color1, props.c2_color2
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    # Line\n'
        new_dict += f'    "enable_preview": {props.enable_preview},\n'
        new_dict += f'    "val_a": {props.val_a:.4f}, "val_b": {props.val_b:.4f}, "val_d": {props.val_d:.4f},\n'
        new_dict += f'    "x_min": {props.x_min:.4f}, "x_max": {props.x_max:.4f},\n'
        new_dict += f'    "y_min": {props.y_min:.4f}, "y_max": {props.y_max:.4f},\n'
        new_dict += f'    "z_min": {props.z_min:.4f}, "z_max": {props.z_max:.4f},\n'
        new_dict += f'    "thickness": {props.thickness:.4f}, "draw_plane": "{props.draw_plane}",\n'
        new_dict += f'    "show_eq1": {props.show_eq1}, "show_eq2": {props.show_eq2}, "show_eq3": {props.show_eq3},\n'
        new_dict += f'    "color1": ({c1[0]:.4f}, {c1[1]:.4f}, {c1[2]:.4f}, {c1[3]:.4f}),\n'
        new_dict += f'    "color2": ({c2[0]:.4f}, {c2[1]:.4f}, {c2[2]:.4f}, {c2[3]:.4f}),\n'
        new_dict += f'    "color3": ({c3[0]:.4f}, {c3[1]:.4f}, {c3[2]:.4f}, {c3[3]:.4f}),\n\n'
        
        new_dict += f'    # Torus 1 (Normal)\n'
        new_dict += f'    "t1_enable_preview": {props.t1_enable_preview}, "t1_mode": "{props.t1_mode}",\n'
        new_dict += f'    "t1_val_a": {props.t1_val_a:.4f}, "t1_val_b": {props.t1_val_b:.4f},\n'
        new_dict += f'    "t1_z_min": {props.t1_z_min:.4f}, "t1_z_max": {props.t1_z_max:.4f}, "t1_count": {props.t1_count},\n'
        new_dict += f'    "t1_z_center": {props.t1_z_center:.4f}, "t1_z_interval": {props.t1_z_interval:.4f}, "t1_up_down_count": {props.t1_up_down_count},\n'
        new_dict += f'    "t1_major_radius": {props.t1_major_radius:.4f}, "t1_minor_radius": {props.t1_minor_radius:.4f},\n'
        new_dict += f'    "t1_color": ({t1c[0]:.4f}, {t1c[1]:.4f}, {t1c[2]:.4f}, {t1c[3]:.4f}),\n\n'

        new_dict += f'    # Torus 2 (Elliptic / Lorentz)\n'
        new_dict += f'    "t2_enable_preview": {props.t2_enable_preview}, "t2_mode": "{props.t2_mode}",\n'
        new_dict += f'    "t2_val_a": {props.t2_val_a:.4f}, "t2_val_b": {props.t2_val_b:.4f},\n'
        new_dict += f'    "t2_z_min": {props.t2_z_min:.4f}, "t2_z_max": {props.t2_z_max:.4f}, "t2_count": {props.t2_count},\n'
        new_dict += f'    "t2_z_center": {props.t2_z_center:.4f}, "t2_z_interval": {props.t2_z_interval:.4f}, "t2_up_down_count": {props.t2_up_down_count},\n'
        new_dict += f'    "t2_major_radius": {props.t2_major_radius:.4f}, "t2_minor_radius": {props.t2_minor_radius:.4f},\n'
        new_dict += f'    "t2_f": {props.t2_f:.4f}, "t2_g": {props.t2_g:.4f},\n'
        new_dict += f'    "t2_color": ({t2c[0]:.4f}, {t2c[1]:.4f}, {t2c[2]:.4f}, {t2c[3]:.4f}),\n\n'
        
        new_dict += f'    # Cross 1\n'
        new_dict += f'    "c1_enable_preview": {props.c1_enable_preview},\n'
        new_dict += f'    "c1_center": ({props.c1_center[0]:.4f}, {props.c1_center[1]:.4f}, {props.c1_center[2]:.4f}),\n'
        new_dict += f'    "c1_length": {props.c1_length:.4f}, "c1_thickness": {props.c1_thickness:.4f}, "c1_plane": "{props.c1_plane}",\n'
        new_dict += f'    "c1_color1": ({cc1_1[0]:.4f}, {cc1_1[1]:.4f}, {cc1_1[2]:.4f}, {cc1_1[3]:.4f}),\n'
        new_dict += f'    "c1_color2": ({cc1_2[0]:.4f}, {cc1_2[1]:.4f}, {cc1_2[2]:.4f}, {cc1_2[3]:.4f}),\n\n'

        new_dict += f'    # Cross 2\n'
        new_dict += f'    "c2_enable_preview": {props.c2_enable_preview},\n'
        new_dict += f'    "c2_center": ({props.c2_center[0]:.4f}, {props.c2_center[1]:.4f}, {props.c2_center[2]:.4f}),\n'
        new_dict += f'    "c2_length": {props.c2_length:.4f}, "c2_thickness": {props.c2_thickness:.4f}, "c2_plane": "{props.c2_plane}",\n'
        new_dict += f'    "c2_color1": ({cc2_1[0]:.4f}, {cc2_1[1]:.4f}, {cc2_1[2]:.4f}, {cc2_1[3]:.4f}),\n'
        new_dict += f'    "c2_color2": ({cc2_2[0]:.4f}, {cc2_2[1]:.4f}, {cc2_2[2]:.4f}, {cc2_2[3]:.4f}),\n'
        new_dict += "}\n"

        try:
            tag_start = "# <BEGIN" + "_DICT>"
            tag_end = "# <END" + "_DICT>"
            pre_code, rest = code.split(tag_start, 1)
            _, post_code = rest.split(tag_end, 1)
            final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
            context.window_manager.clipboard = final_code
            self.report({'INFO'}, "Code copied!")
        except: return {'CANCELLED'}
        return {'FINISHED'}

class OT_OpenUrl(Operator):
    bl_idname = f"{OP_PREFIX}.open_url"; bl_label = "Open URL"; url: StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

class OT_RemoveAddon(Operator):
    bl_idname = f"{OP_PREFIX}.remove_addon"; bl_label = "Close Addon"
    def execute(self, context):
        bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
        self.report({'INFO'}, "アドオンを終了しました。")
        return {'FINISHED'}

# ==============================================================================
#  PANELS
# ==============================================================================

class PT_MainPanel(Panel):
    bl_label = PANEL_TITLE
    bl_idname = f"{PREFIX}_PT_main"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        row_prev = layout.row(); row_prev.scale_y = 1.5
        if not props.enable_preview: row_prev.operator(OT_ShowLinePreview.bl_idname, icon='PLAY', text="Show Line Preview")
        else: row_prev.prop(props, "enable_preview", text="Line Preview Active", toggle=True, icon='PAUSE')

        box_info = layout.box()
        box_info.label(text="【 Line Info 】", icon='INFO')
        a_str = f"{props.val_a:.2f}" if abs(props.val_a) > 0.0001 else "0.00(Err)"
        box_info.label(text=f"y = ({props.val_b:.2f} / {a_str}) x")
        
        box_values = layout.box()
        col_v = box_values.column(align=True)
        col_v.prop(props, "val_a"); col_v.prop(props, "val_b"); col_v.prop(props, "val_d")
        
        box_limits = layout.box()
        box_limits.label(text="Limits (X, Y, Z)", icon='MOD_HULL')
        for axis in ['x', 'y', 'z']:
            r = box_limits.row(align=True)
            r.prop(props, f"{axis}_min", text=f"{axis.upper()} Min")
            r.prop(props, f"{axis}_max", text="Max")

class PT_VisibilityPanel(Panel):
    bl_label = "Line Design & Detach"
    bl_idname = f"{PREFIX}_PT_visibility"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_parent_id = f"{PREFIX}_PT_main"

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return
        layout.prop(props, "thickness"); layout.prop(props, "draw_plane"); layout.separator()
        for i in range(1, 4):
            r = layout.row(align=True)
            r.prop(props, f"show_eq{i}", text=f"Eq {i}", toggle=True); r.prop(props, f"color{i}", text="")
        
        layout.separator()
        col_exec = layout.column(); col_exec.scale_y = 1.5 
        col_exec.operator(OT_DetachLines.bl_idname, icon='MESH_CYLINDER', text="Detach Lines (固定化)")

class PT_Torus1Panel(Panel):
    bl_label = "Torus (Normal)"
    bl_idname = f"{PREFIX}_PT_torus1"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        row_prev = layout.row(); row_prev.scale_y = 1.5
        if not props.t1_enable_preview: row_prev.operator(OT_ShowTorus1Preview.bl_idname, icon='PLAY', text="Show Normal Torus")
        else: row_prev.prop(props, "t1_enable_preview", text="Normal Torus Active", toggle=True, icon='PAUSE')

        box_eq = layout.box()
        box_eq.label(text="Center Line: z = (b/a)x", icon='IPO_CONSTANT')
        col = box_eq.column(align=True)
        col.prop(props, "t1_val_a"); col.prop(props, "t1_val_b")

        box_r = layout.box()
        box_r.prop(props, "t1_mode", text="")
        if props.t1_mode == 'INTERVAL':
            c_int = box_r.column(align=True)
            c_int.prop(props, "t1_z_center"); c_int.prop(props, "t1_z_interval"); c_int.prop(props, "t1_up_down_count")
        else:
            c_rng = box_r.column(align=True)
            c_rng.prop(props, "t1_z_min"); c_rng.prop(props, "t1_z_max"); c_rng.prop(props, "t1_count")

        box_s = layout.box()
        box_s.prop(props, "t1_major_radius"); box_s.prop(props, "t1_minor_radius"); box_s.prop(props, "t1_color")

        col_exec = layout.column(); col_exec.scale_y = 1.5
        col_exec.operator(OT_DetachTorus1.bl_idname, icon='MESH_TORUS', text="Detach Normal Torus")

class PT_Torus2Panel(Panel):
    bl_label = "Torus (Elliptic / Lorentz)"
    bl_idname = f"{PREFIX}_PT_torus2"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        row_prev = layout.row(); row_prev.scale_y = 1.5
        if not props.t2_enable_preview: row_prev.operator(OT_ShowTorus2Preview.bl_idname, icon='PLAY', text="Show Elliptic Torus")
        else: row_prev.prop(props, "t2_enable_preview", text="Elliptic Torus Active", toggle=True, icon='PAUSE')

        box_eq = layout.box()
        box_eq.label(text="Center Line: z = (b/a)x", icon='IPO_CONSTANT')
        col = box_eq.column(align=True)
        col.prop(props, "t2_val_a"); col.prop(props, "t2_val_b")

        box_r = layout.box()
        box_r.prop(props, "t2_mode", text="")
        if props.t2_mode == 'INTERVAL':
            c_int = box_r.column(align=True)
            c_int.prop(props, "t2_z_center"); c_int.prop(props, "t2_z_interval"); c_int.prop(props, "t2_up_down_count")
        else:
            c_rng = box_r.column(align=True)
            c_rng.prop(props, "t2_z_min"); c_rng.prop(props, "t2_z_max"); c_rng.prop(props, "t2_count")

        box_s = layout.box()
        box_s.prop(props, "t2_major_radius"); box_s.prop(props, "t2_minor_radius")
        box_s.separator()
        
        box_s.label(text="楕円トーラスの収縮割合 (f/g)", icon='CON_SIZELIKE')
        col_fg = box_s.column(align=True)
        col_fg.prop(props, "t2_f")
        col_fg.prop(props, "t2_g")
        g_val = props.t2_g if abs(props.t2_g) > 0.0001 else 0.0001
        box_s.label(text=f"収縮率: {props.t2_f / g_val:.4f}")
        
        box_s.prop(props, "t2_color")

        col_exec = layout.column(); col_exec.scale_y = 1.5
        col_exec.operator(OT_DetachTorus2.bl_idname, icon='MESH_TORUS', text="Detach Elliptic Torus")

class PT_Cross1Panel(Panel):
    bl_label = "Cross Cylinders 1"
    bl_idname = f"{PREFIX}_PT_cross1"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        row_prev = layout.row(); row_prev.scale_y = 1.5
        if not props.c1_enable_preview: row_prev.operator(OT_ShowCross1Preview.bl_idname, icon='PLAY', text="Show Cross 1")
        else: row_prev.prop(props, "c1_enable_preview", text="Cross 1 Active", toggle=True, icon='PAUSE')

        box_s = layout.box()
        box_s.prop(props, "c1_center")
        box_s.prop(props, "c1_length")
        box_s.prop(props, "c1_thickness")
        box_s.prop(props, "c1_plane")
        
        box_s.separator()
        r1 = box_s.row(align=True)
        r1.label(text="y = x"); r1.prop(props, "c1_color1", text="")
        r2 = box_s.row(align=True)
        r2.label(text="y = -x"); r2.prop(props, "c1_color2", text="")

        col_exec = layout.column(); col_exec.scale_y = 1.5
        col_exec.operator(OT_DetachCross1.bl_idname, icon='MESH_DATA', text="Detach Cross 1")

class PT_Cross2Panel(Panel):
    bl_label = "Cross Cylinders 2"
    bl_idname = f"{PREFIX}_PT_cross2"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        row_prev = layout.row(); row_prev.scale_y = 1.5
        if not props.c2_enable_preview: row_prev.operator(OT_ShowCross2Preview.bl_idname, icon='PLAY', text="Show Cross 2")
        else: row_prev.prop(props, "c2_enable_preview", text="Cross 2 Active", toggle=True, icon='PAUSE')

        box_s = layout.box()
        box_s.prop(props, "c2_center")
        box_s.prop(props, "c2_length")
        box_s.prop(props, "c2_thickness")
        box_s.prop(props, "c2_plane")
        
        box_s.separator()
        r1 = box_s.row(align=True)
        r1.label(text="y = x"); r1.prop(props, "c2_color1", text="")
        r2 = box_s.row(align=True)
        r2.label(text="y = -x"); r2.prop(props, "c2_color2", text="")

        col_exec = layout.column(); col_exec.scale_y = 1.5
        col_exec.operator(OT_DetachCross2.bl_idname, icon='MESH_DATA', text="Detach Cross 2")

class PT_SystemPanel(Panel):
    bl_label = "System (Copy / Close)"
    bl_idname = f"{PREFIX}_PT_system"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        r_c = layout.row(); r_c.scale_y = 1.2
        r_c.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
        r_r = layout.row(); r_r.scale_y = 1.2; r_r.alert = True 
        r_r.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Close Addon (完全終了)")

class PT_LinksPanel(Panel):
    bl_label = "Links"
    bl_idname = f"{PREFIX}_PT_links"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        for l in ADDON_LINKS:
            self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"], icon='URL').url = l["url"]

# ==============================================================================
#  REGISTER
# ==============================================================================

classes = (
    PG_EquationProps, 
    OT_ShowLinePreview, OT_DetachLines, 
    OT_ShowTorus1Preview, OT_DetachTorus1,
    OT_ShowTorus2Preview, OT_DetachTorus2,
    OT_ShowCross1Preview, OT_DetachCross1,
    OT_ShowCross2Preview, OT_DetachCross2,
    OT_CopyFullScript, OT_OpenUrl, OT_RemoveAddon, 
    PT_MainPanel, PT_VisibilityPanel, 
    PT_Torus1Panel, PT_Torus2Panel, 
    PT_Cross1Panel, PT_Cross2Panel,
    PT_SystemPanel, PT_LinksPanel
)

def auto_open_sidebar():
    try:
        for window in bpy.context.window_manager.windows:
            for area in window.screen.areas:
                if area.type == 'VIEW_3D':
                    for space in area.spaces:
                        if space.type == 'VIEW_3D':
                            if not space.show_region_ui: space.show_region_ui = True
    except: pass
    return None

def register():
    for c in classes: 
        try: bpy.utils.register_class(c)
        except ValueError: pass
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_EquationProps))
    bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)

def unregister():
    global _timer
    if _timer is not None:
        try: bpy.app.timers.unregister(_timer)
        except Exception: pass
        _timer = None
    cleanup_preview_data()
    if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
    for c in reversed(classes): 
        try: bpy.utils.unregister_class(c)
        except ValueError: pass

if __name__ == "__main__": 
    register()
# Copied: 2026-04-08 12:00:00
import bpy
import time
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime

# ==============================================================================
#  【 基本設定エリア 】
# ==============================================================================

PREFIX       = "EqGen"
ADDON_NAME   = "[ Equation Gen ]"
TAB_NAME     = "[ Equation Gen ]"
PANEL_TITLE  = "Equation Lines"
AUTHOR       = "zionadchat"

# ★ このスクリプト自身のID(絶対に消さないこと)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: EQ_LINES_2026_04_08_V5_3 ###"

# ==============================================================================
#  システム初期化 & ID管理
# ==============================================================================

bl_info = {
    "name": f"{ADDON_NAME} {PREFIX}",
    "author": AUTHOR,
    "version": (5, 3, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": "Lines, Normal Torus, and Elliptic Torus Generators (Independent)",
    "category": "3D View",
}

OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"

ADDON_LINKS = (
    {"label": "Prefix 20260408", "url": "<https://www.notion.so/Prefix-20260408-33cf5dacaf43807e9e35ff8cdbbc39c6>"},
)

# ==============================================================================
#  デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    # Line
    "enable_preview": False,
    "val_a": 0.6000,
    "val_b": 1.0000,
    "val_d": 10.0000,
    "x_min": -50.0, "x_max": 50.0,
    "y_min": -50.0, "y_max": 50.0,
    "z_min": -50.0, "z_max": 50.0,
    "thickness": 0.5000,
    "draw_plane": "XZ",
    "show_eq1": True, "show_eq2": True, "show_eq3": True,
    "color1": (1.0000, 0.2000, 0.2000, 1.0000),
    "color2": (0.2000, 1.0000, 0.2000, 1.0000),
    "color3": (0.2000, 0.2000, 1.0000, 1.0000),

    # Torus 1 (Normal)
    "t1_enable_preview": False,
    "t1_mode": "INTERVAL",
    "t1_val_a": 0.6000, "t1_val_b": 1.0000,
    "t1_z_min": -50.0, "t1_z_max": 50.0, "t1_count": 11,
    "t1_z_center": 0.0, "t1_z_interval": 1.0, "t1_up_down_count": 5,
    "t1_major_radius": 5.0, "t1_minor_radius": 1.0,
    "t1_color": (0.2000, 0.8000, 0.8000, 1.0000), # Cyan

    # Torus 2 (Elliptic / Lorentz)
    "t2_enable_preview": False,
    "t2_mode": "INTERVAL",
    "t2_val_a": 0.6000, "t2_val_b": 1.0000,
    "t2_z_min": -50.0, "t2_z_max": 50.0, "t2_count": 11,
    "t2_z_center": 0.0, "t2_z_interval": 1.0, "t2_up_down_count": 5,
    "t2_major_radius": 5.0, "t2_minor_radius": 1.0,
    "t2_scale_x": 0.8000, # Lorentz contraction at v=0.6c
    "t2_color": (0.8000, 0.2000, 0.8000, 1.0000), # Magenta
}
# <END_DICT>

PREVIEW_COL_LINE = f"{PREFIX}_Line_Preview"
PREVIEW_COL_TORUS1 = f"{PREFIX}_Torus1_Preview"
PREVIEW_COL_TORUS2 = f"{PREFIX}_Torus2_Preview"

# ==============================================================================
#  共通マテリアル・データ管理 ロジック
# ==============================================================================

def cleanup_preview_data():
    for name in [PREVIEW_COL_LINE, PREVIEW_COL_TORUS1, PREVIEW_COL_TORUS2]:
        col = bpy.data.collections.get(name)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0:
                    if isinstance(data, bpy.types.Curve):
                        bpy.data.curves.remove(data)
            if len(col.objects) == 0:
                bpy.data.collections.remove(col)

def apply_material_settings(mat, color):
    mat.use_nodes = True
    mat.blend_method = 'BLEND'
    mat.diffuse_color = color
    
    tree = mat.node_tree
    bsdf = tree.nodes.get("Principled BSDF")
    if not bsdf:
        tree.nodes.clear()
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        bsdf.name = "Principled BSDF"
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        tree.links.new(bsdf.outputs[0], out.inputs[0])
        
    if "Base Color" in bsdf.inputs:
        bsdf.inputs["Base Color"].default_value = color
    if "Alpha" in bsdf.inputs:
        bsdf.inputs["Alpha"].default_value = color[3]

def get_preview_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name=name)
    apply_material_settings(mat, color)
    return mat

def build_curve_circle(curve, radius, segments=32):
    if len(curve.splines) == 0:
        spline = curve.splines.new('POLY')
        spline.points.add(segments - 1)
        spline.use_cyclic_u = True
    else:
        spline = curve.splines[0]
        if len(spline.points) != segments:
            curve.splines.clear()
            spline = curve.splines.new('POLY')
            spline.points.add(segments - 1)
            spline.use_cyclic_u = True
            
    for i in range(segments):
        angle = 2 * math.pi * i / segments
        spline.points[i].co = (radius * math.cos(angle), radius * math.sin(angle), 0.0, 1.0)

# ==============================================================================
#  Line プレビューロジック
# ==============================================================================

def get_clipped_segment(m, c, x_min, x_max, v_min, v_max):
    if m == 0:
        if v_min <= c <= v_max:
            return x_min, x_max
        return None, None
    else:
        x_from_v1, x_from_v2 = (v_min - c) / m, (v_max - c) / m
        valid_x_min, valid_x_max = min(x_from_v1, x_from_v2), max(x_from_v1, x_from_v2)
        act_x_min, act_x_max = max(x_min, valid_x_min), min(x_max, valid_x_max)
        if act_x_min > act_x_max: 
            return None, None
        return act_x_min, act_x_max

def calc_points(props, m, c):
    x_min, x_max = min(props.x_min, props.x_max), max(props.x_min, props.x_max)
    y_min, y_max = min(props.y_min, props.y_max), max(props.y_min, props.y_max)
    z_min, z_max = min(props.z_min, props.z_max), max(props.z_min, props.z_max)
    
    if props.draw_plane == 'XZ':
        act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, z_min, z_max)
        if act_x_min is None: return None, None
        return (act_x_min, 0.0, m * act_x_min + c), (act_x_max, 0.0, m * act_x_max + c)
    else:
        act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, y_min, y_max)
        if act_x_min is None: return None, None
        return (act_x_min, m * act_x_min + c, 0.0), (act_x_max, m * act_x_max + c, 0.0)

def update_line_preview(context, props):
    if not props.enable_preview:
        col = bpy.data.collections.get(PREVIEW_COL_LINE)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0: bpy.data.curves.remove(data)
            bpy.data.collections.remove(col)
        return

    col = bpy.data.collections.get(PREVIEW_COL_LINE)
    if not col:
        col = bpy.data.collections.new(PREVIEW_COL_LINE)
        context.scene.collection.children.link(col)

    a = props.val_a if abs(props.val_a) > 0.0001 else 0.0001
    b, d = props.val_b, props.val_d
    m = b / a

    equations = [
        {"id": 1, "show": props.show_eq1, "color": props.color1, "offset": 0.0},
        {"id": 2, "show": props.show_eq2, "color": props.color2, "offset": -d},
        {"id": 3, "show": props.show_eq3, "color": props.color3, "offset": d},
    ]

    for eq in equations:
        obj_name = f"[Preview] EqLine_{eq['id']}"
        obj = bpy.data.objects.get(obj_name)

        if not eq["show"]:
            if obj: obj.hide_viewport = obj.hide_render = True
            continue

        p1, p2 = calc_points(props, m, eq["offset"])
        if p1 is None:
            if obj: obj.hide_viewport = obj.hide_render = True
            continue

        if not obj:
            curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
            curve.dimensions = '3D'
            curve.fill_mode = 'FULL'
            spline = curve.splines.new('POLY')
            spline.points.add(1)
            obj = bpy.data.objects.new(obj_name, curve)
            col.objects.link(obj)
        else:
            curve = obj.data
            spline = curve.splines[0]

        curve.bevel_depth = props.thickness
        curve.bevel_resolution = 6
        spline.points[0].co = (*p1, 1.0)
        spline.points[1].co = (*p2, 1.0)
        obj.hide_viewport = obj.hide_render = False

        mat = get_preview_material(f"Preview_Mat_EqLine{eq['id']}", eq["color"])
        if not obj.data.materials: obj.data.materials.append(mat)
        else: obj.data.materials[0] = mat

# ==============================================================================
#  Torus 1 (Normal) プレビューロジック
# ==============================================================================

def update_torus1_preview(context, props):
    if not props.t1_enable_preview:
        col = bpy.data.collections.get(PREVIEW_COL_TORUS1)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0: bpy.data.curves.remove(data)
            bpy.data.collections.remove(col)
        return

    col = bpy.data.collections.get(PREVIEW_COL_TORUS1)
    if not col:
        col = bpy.data.collections.new(PREVIEW_COL_TORUS1)
        context.scene.collection.children.link(col)

    a = props.t1_val_a
    b_val = props.t1_val_b if abs(props.t1_val_b) > 0.0001 else (0.0001 if props.t1_val_b >= 0 else -0.0001)
    
    z_list = []
    if props.t1_mode == 'RANGE':
        count = props.t1_count
        for i in range(count):
            t = i / (count - 1) if count > 1 else 0.5
            z = props.t1_z_min + t * (props.t1_z_max - props.t1_z_min)
            z_list.append(z)
    else: 
        center = props.t1_z_center
        interval = props.t1_z_interval
        ud_count = props.t1_up_down_count
        for i in range(-ud_count, ud_count + 1):
            z_list.append(center + i * interval)

    existing_objs = list(col.objects)
    mat = get_preview_material("Preview_Mat_Torus1", props.t1_color)

    for i, z in enumerate(z_list):
        obj_name = f"[Preview] Normal_Torus_{i+1}"
        x = z * (a / b_val)
        y = 0.0 
        
        if i < len(existing_objs):
            obj = existing_objs[i]
            curve = obj.data
        else:
            curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
            curve.dimensions = '3D'
            curve.fill_mode = 'FULL'
            obj = bpy.data.objects.new(obj_name, curve)
            col.objects.link(obj)
            
        curve.bevel_depth = props.t1_minor_radius
        curve.bevel_resolution = 8
        build_curve_circle(curve, props.t1_major_radius)
        
        obj.location = (x, y, z)
        obj.scale = (1.0, 1.0, 1.0)
        obj.hide_viewport = False
        obj.hide_render = False
        
        if not obj.data.materials: obj.data.materials.append(mat)
        else: obj.data.materials[0] = mat

    if len(existing_objs) > len(z_list):
        for obj in existing_objs[len(z_list):]:
            data = obj.data
            bpy.data.objects.remove(obj, do_unlink=True)
            if data and data.users == 0: bpy.data.curves.remove(data)

# ==============================================================================
#  Torus 2 (Elliptic) プレビューロジック
# ==============================================================================

def update_torus2_preview(context, props):
    if not props.t2_enable_preview:
        col = bpy.data.collections.get(PREVIEW_COL_TORUS2)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0: bpy.data.curves.remove(data)
            bpy.data.collections.remove(col)
        return

    col = bpy.data.collections.get(PREVIEW_COL_TORUS2)
    if not col:
        col = bpy.data.collections.new(PREVIEW_COL_TORUS2)
        context.scene.collection.children.link(col)

    a = props.t2_val_a
    b_val = props.t2_val_b if abs(props.t2_val_b) > 0.0001 else (0.0001 if props.t2_val_b >= 0 else -0.0001)
    
    z_list = []
    if props.t2_mode == 'RANGE':
        count = props.t2_count
        for i in range(count):
            t = i / (count - 1) if count > 1 else 0.5
            z = props.t2_z_min + t * (props.t2_z_max - props.t2_z_min)
            z_list.append(z)
    else: 
        center = props.t2_z_center
        interval = props.t2_z_interval
        ud_count = props.t2_up_down_count
        for i in range(-ud_count, ud_count + 1):
            z_list.append(center + i * interval)

    existing_objs = list(col.objects)
    mat = get_preview_material("Preview_Mat_Torus2", props.t2_color)

    for i, z in enumerate(z_list):
        obj_name = f"[Preview] Elliptic_Torus_{i+1}"
        x = z * (a / b_val)
        y = 0.0 
        
        if i < len(existing_objs):
            obj = existing_objs[i]
            curve = obj.data
        else:
            curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
            curve.dimensions = '3D'
            curve.fill_mode = 'FULL'
            obj = bpy.data.objects.new(obj_name, curve)
            col.objects.link(obj)
            
        curve.bevel_depth = props.t2_minor_radius
        curve.bevel_resolution = 8
        build_curve_circle(curve, props.t2_major_radius)
        
        obj.location = (x, y, z)
        obj.scale = (props.t2_scale_x, 1.0, 1.0) # Lorentz Contraction
        obj.hide_viewport = False
        obj.hide_render = False
        
        if not obj.data.materials: obj.data.materials.append(mat)
        else: obj.data.materials[0] = mat

    if len(existing_objs) > len(z_list):
        for obj in existing_objs[len(z_list):]:
            data = obj.data
            bpy.data.objects.remove(obj, do_unlink=True)
            if data and data.users == 0: bpy.data.curves.remove(data)

# ==============================================================================
#  タイマー管理
# ==============================================================================

_timer = None
_last_update_time = 0

def delayed_update():
    global _timer, _last_update_time
    _timer = None
    now = time.time()
    if now - _last_update_time < 0.05:
        if _timer is None:
            _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
        return None
    _last_update_time = now
    
    ctx = bpy.context
    if not ctx or not ctx.scene: return None
    if ctx.object and ctx.object.mode != 'OBJECT': return None

    props = getattr(ctx.scene, PROPS_NAME, None)
    if props:
        update_line_preview(ctx, props)
        update_torus1_preview(ctx, props)
        update_torus2_preview(ctx, props)
    return None

def on_update(self, context):
    global _timer
    if _timer is None: 
        _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)

# ==============================================================================
#  PROPERTIES
# ==============================================================================

class PG_EquationProps(PropertyGroup):
    # Line Properties
    enable_preview: BoolProperty(name="Enable Line Preview", default=CURRENT_DEFAULTS['enable_preview'], update=on_update)
    val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['val_a'], update=on_update)
    val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['val_b'], update=on_update)
    val_d: FloatProperty(name="Value d", default=CURRENT_DEFAULTS['val_d'], update=on_update)
    x_min: FloatProperty(name="X Min", default=CURRENT_DEFAULTS['x_min'], update=on_update)
    x_max: FloatProperty(name="X Max", default=CURRENT_DEFAULTS['x_max'], update=on_update)
    y_min: FloatProperty(name="Y Min", default=CURRENT_DEFAULTS['y_min'], update=on_update)
    y_max: FloatProperty(name="Y Max", default=CURRENT_DEFAULTS['y_max'], update=on_update)
    z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['z_min'], update=on_update)
    z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['z_max'], update=on_update)
    thickness: FloatProperty(name="Line Thickness", default=CURRENT_DEFAULTS['thickness'], min=0.01, max=10.0, update=on_update)
    draw_plane: EnumProperty(name="Draw Plane", items=[('XZ', "Front (XZ)", "XZ Plane"), ('XY', "Top (XY)", "XY Plane")], default=CURRENT_DEFAULTS['draw_plane'], update=on_update)
    show_eq1: BoolProperty(name="Show Eq 1", default=CURRENT_DEFAULTS['show_eq1'], update=on_update)
    show_eq2: BoolProperty(name="Show Eq 2", default=CURRENT_DEFAULTS['show_eq2'], update=on_update)
    show_eq3: BoolProperty(name="Show Eq 3", default=CURRENT_DEFAULTS['show_eq3'], update=on_update)
    color1: FloatVectorProperty(name="Color 1", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color1'], update=on_update)
    color2: FloatVectorProperty(name="Color 2", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color2'], update=on_update)
    color3: FloatVectorProperty(name="Color 3", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color3'], update=on_update)

    # Torus 1 (Normal)
    t1_enable_preview: BoolProperty(name="Enable Torus 1 Preview", default=CURRENT_DEFAULTS['t1_enable_preview'], update=on_update)
    t1_mode: EnumProperty(name="Mode", items=[('INTERVAL', "Interval Mode", ""), ('RANGE', "Range Mode", "")], default=CURRENT_DEFAULTS['t1_mode'], update=on_update)
    t1_val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['t1_val_a'], update=on_update)
    t1_val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['t1_val_b'], update=on_update)
    t1_z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['t1_z_min'], update=on_update)
    t1_z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['t1_z_max'], update=on_update)
    t1_count: IntProperty(name="Total Count", default=CURRENT_DEFAULTS['t1_count'], min=1, max=500, update=on_update)
    t1_z_center: FloatProperty(name="Z Center", default=CURRENT_DEFAULTS['t1_z_center'], update=on_update)
    t1_z_interval: FloatProperty(name="Z Interval", default=CURRENT_DEFAULTS['t1_z_interval'], update=on_update)
    t1_up_down_count: IntProperty(name="Up/Down Count", default=CURRENT_DEFAULTS['t1_up_down_count'], min=0, max=100, update=on_update)
    t1_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['t1_major_radius'], min=0.1, max=100.0, update=on_update)
    t1_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['t1_minor_radius'], min=0.01, max=50.0, update=on_update)
    t1_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['t1_color'], update=on_update)

    # Torus 2 (Elliptic)
    t2_enable_preview: BoolProperty(name="Enable Torus 2 Preview", default=CURRENT_DEFAULTS['t2_enable_preview'], update=on_update)
    t2_mode: EnumProperty(name="Mode", items=[('INTERVAL', "Interval Mode", ""), ('RANGE', "Range Mode", "")], default=CURRENT_DEFAULTS['t2_mode'], update=on_update)
    t2_val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['t2_val_a'], update=on_update)
    t2_val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['t2_val_b'], update=on_update)
    t2_z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['t2_z_min'], update=on_update)
    t2_z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['t2_z_max'], update=on_update)
    t2_count: IntProperty(name="Total Count", default=CURRENT_DEFAULTS['t2_count'], min=1, max=500, update=on_update)
    t2_z_center: FloatProperty(name="Z Center", default=CURRENT_DEFAULTS['t2_z_center'], update=on_update)
    t2_z_interval: FloatProperty(name="Z Interval", default=CURRENT_DEFAULTS['t2_z_interval'], update=on_update)
    t2_up_down_count: IntProperty(name="Up/Down Count", default=CURRENT_DEFAULTS['t2_up_down_count'], min=0, max=100, update=on_update)
    t2_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['t2_major_radius'], min=0.1, max=100.0, update=on_update)
    t2_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['t2_minor_radius'], min=0.01, max=50.0, update=on_update)
    t2_scale_x: FloatProperty(name="X Scale", default=CURRENT_DEFAULTS['t2_scale_x'], min=0.01, max=5.0, update=on_update)
    t2_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['t2_color'], update=on_update)

# ==============================================================================
#  OPERATORS
# ==============================================================================

class OT_ShowLinePreview(Operator):
    bl_idname = f"{OP_PREFIX}.show_line_preview"; bl_label = "Show Line Preview"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props: props.enable_preview = True; update_line_preview(context, props)
        return {'FINISHED'}

class OT_DetachLines(Operator):
    bl_idname = f"{OP_PREFIX}.detach_lines"; bl_label = "Detach Lines"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col_preview = bpy.data.collections.get(PREVIEW_COL_LINE)
        if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
        target_col = context.collection
        timestamp = datetime.now().strftime('%H%M%S')
        bpy.ops.object.select_all(action='DESELECT')
        for obj in list(col_preview.objects):
            if obj.hide_viewport: continue
            if obj.name not in target_col.objects: target_col.objects.link(obj)
            col_preview.objects.unlink(obj)
            obj.name = obj.name.replace("[Preview]", "Solid_EqLine") + f"_{timestamp}"
            if obj.data.materials:
                mat = obj.data.materials[0]
                new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
                obj.data.materials[0] = new_mat
            obj.select_set(True)
        props = getattr(context.scene, PROPS_NAME, None)
        if props: update_line_preview(context, props)
        return {'FINISHED'}

class OT_ShowTorus1Preview(Operator):
    bl_idname = f"{OP_PREFIX}.show_torus1_preview"; bl_label = "Show Normal Torus Preview"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props: props.t1_enable_preview = True; update_torus1_preview(context, props)
        return {'FINISHED'}

class OT_DetachTorus1(Operator):
    bl_idname = f"{OP_PREFIX}.detach_torus1"; bl_label = "Detach Normal Torus"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col_preview = bpy.data.collections.get(PREVIEW_COL_TORUS1)
        if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
        target_col = context.collection
        timestamp = datetime.now().strftime('%H%M%S')
        bpy.ops.object.select_all(action='DESELECT')
        for obj in list(col_preview.objects):
            if obj.name not in target_col.objects: target_col.objects.link(obj)
            col_preview.objects.unlink(obj)
            obj.name = obj.name.replace("[Preview]", "Solid_Normal_Torus") + f"_{timestamp}"
            if obj.data.materials:
                mat = obj.data.materials[0]
                new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
                obj.data.materials[0] = new_mat
            
            bpy.context.view_layer.objects.active = obj; obj.select_set(True)
            bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
            obj.select_set(False)

        props = getattr(context.scene, PROPS_NAME, None)
        if props: update_torus1_preview(context, props)
        return {'FINISHED'}

class OT_ShowTorus2Preview(Operator):
    bl_idname = f"{OP_PREFIX}.show_torus2_preview"; bl_label = "Show Elliptic Torus Preview"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props: props.t2_enable_preview = True; update_torus2_preview(context, props)
        return {'FINISHED'}

class OT_DetachTorus2(Operator):
    bl_idname = f"{OP_PREFIX}.detach_torus2"; bl_label = "Detach Elliptic Torus"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col_preview = bpy.data.collections.get(PREVIEW_COL_TORUS2)
        if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
        target_col = context.collection
        timestamp = datetime.now().strftime('%H%M%S')
        bpy.ops.object.select_all(action='DESELECT')
        for obj in list(col_preview.objects):
            if obj.name not in target_col.objects: target_col.objects.link(obj)
            col_preview.objects.unlink(obj)
            obj.name = obj.name.replace("[Preview]", "Solid_Elliptic_Torus") + f"_{timestamp}"
            if obj.data.materials:
                mat = obj.data.materials[0]
                new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
                obj.data.materials[0] = new_mat
            
            bpy.context.view_layer.objects.active = obj; obj.select_set(True)
            bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
            obj.select_set(False)

        props = getattr(context.scene, PROPS_NAME, None)
        if props: update_torus2_preview(context, props)
        return {'FINISHED'}

class OT_CopyFullScript(Operator):
    bl_idname = f"{OP_PREFIX}.copy_script"
    bl_label = "Copy Script"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        target_text = None
        for t in bpy.data.texts:
            if SOURCE_ID_TAG in t.as_string(): target_text = t; break
        if not target_text: return {'CANCELLED'}

        code = target_text.as_string()
        c1, c2, c3, t1c, t2c = props.color1, props.color2, props.color3, props.t1_color, props.t2_color
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "enable_preview": {props.enable_preview},\n'
        new_dict += f'    "val_a": {props.val_a:.4f}, "val_b": {props.val_b:.4f}, "val_d": {props.val_d:.4f},\n'
        new_dict += f'    "x_min": {props.x_min:.4f}, "x_max": {props.x_max:.4f},\n'
        new_dict += f'    "y_min": {props.y_min:.4f}, "y_max": {props.y_max:.4f},\n'
        new_dict += f'    "z_min": {props.z_min:.4f}, "z_max": {props.z_max:.4f},\n'
        new_dict += f'    "thickness": {props.thickness:.4f}, "draw_plane": "{props.draw_plane}",\n'
        new_dict += f'    "show_eq1": {props.show_eq1}, "show_eq2": {props.show_eq2}, "show_eq3": {props.show_eq3},\n'
        new_dict += f'    "color1": ({c1[0]:.4f}, {c1[1]:.4f}, {c1[2]:.4f}, {c1[3]:.4f}),\n'
        new_dict += f'    "color2": ({c2[0]:.4f}, {c2[1]:.4f}, {c2[2]:.4f}, {c2[3]:.4f}),\n'
        new_dict += f'    "color3": ({c3[0]:.4f}, {c3[1]:.4f}, {c3[2]:.4f}, {c3[3]:.4f}),\n\n'
        
        new_dict += f'    "t1_enable_preview": {props.t1_enable_preview}, "t1_mode": "{props.t1_mode}",\n'
        new_dict += f'    "t1_val_a": {props.t1_val_a:.4f}, "t1_val_b": {props.t1_val_b:.4f},\n'
        new_dict += f'    "t1_z_min": {props.t1_z_min:.4f}, "t1_z_max": {props.t1_z_max:.4f}, "t1_count": {props.t1_count},\n'
        new_dict += f'    "t1_z_center": {props.t1_z_center:.4f}, "t1_z_interval": {props.t1_z_interval:.4f}, "t1_up_down_count": {props.t1_up_down_count},\n'
        new_dict += f'    "t1_major_radius": {props.t1_major_radius:.4f}, "t1_minor_radius": {props.t1_minor_radius:.4f},\n'
        new_dict += f'    "t1_color": ({t1c[0]:.4f}, {t1c[1]:.4f}, {t1c[2]:.4f}, {t1c[3]:.4f}),\n\n'

        new_dict += f'    "t2_enable_preview": {props.t2_enable_preview}, "t2_mode": "{props.t2_mode}",\n'
        new_dict += f'    "t2_val_a": {props.t2_val_a:.4f}, "t2_val_b": {props.t2_val_b:.4f},\n'
        new_dict += f'    "t2_z_min": {props.t2_z_min:.4f}, "t2_z_max": {props.t2_z_max:.4f}, "t2_count": {props.t2_count},\n'
        new_dict += f'    "t2_z_center": {props.t2_z_center:.4f}, "t2_z_interval": {props.t2_z_interval:.4f}, "t2_up_down_count": {props.t2_up_down_count},\n'
        new_dict += f'    "t2_major_radius": {props.t2_major_radius:.4f}, "t2_minor_radius": {props.t2_minor_radius:.4f},\n'
        new_dict += f'    "t2_scale_x": {props.t2_scale_x:.4f},\n'
        new_dict += f'    "t2_color": ({t2c[0]:.4f}, {t2c[1]:.4f}, {t2c[2]:.4f}, {t2c[3]:.4f}),\n'
        new_dict += "}\n"

        try:
            tag_start = "# <BEGIN" + "_DICT>"
            tag_end = "# <END" + "_DICT>"
            pre_code, rest = code.split(tag_start, 1)
            _, post_code = rest.split(tag_end, 1)
            final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
            context.window_manager.clipboard = final_code
            self.report({'INFO'}, "Code copied!")
        except: return {'CANCELLED'}
        return {'FINISHED'}

class OT_OpenUrl(Operator):
    bl_idname = f"{OP_PREFIX}.open_url"; bl_label = "Open URL"; url: StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

class OT_RemoveAddon(Operator):
    bl_idname = f"{OP_PREFIX}.remove_addon"; bl_label = "Close Addon"
    def execute(self, context):
        bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
        self.report({'INFO'}, "アドオンを終了しました。")
        return {'FINISHED'}

# ==============================================================================
#  PANELS
# ==============================================================================

class PT_MainPanel(Panel):
    bl_label = PANEL_TITLE
    bl_idname = f"{PREFIX}_PT_main"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        row_prev = layout.row()
        row_prev.scale_y = 1.5
        if not props.enable_preview: row_prev.operator(OT_ShowLinePreview.bl_idname, icon='PLAY', text="Show Line Preview (表示開始)")
        else: row_prev.prop(props, "enable_preview", text="Line Preview Active", toggle=True, icon='PAUSE')

        box_info = layout.box()
        box_info.label(text="【 Line Info 】", icon='INFO')
        a_str = f"{props.val_a:.2f}" if abs(props.val_a) > 0.0001 else "0.00(Err)"
        box_info.label(text=f"y = ({props.val_b:.2f} / {a_str}) x")
        
        box_values = layout.box()
        col_v = box_values.column(align=True)
        col_v.prop(props, "val_a"); col_v.prop(props, "val_b"); col_v.prop(props, "val_d")
        
        box_limits = layout.box()
        box_limits.label(text="Limits (X, Y, Z)", icon='MOD_HULL')
        for axis in ['x', 'y', 'z']:
            r = box_limits.row(align=True)
            r.prop(props, f"{axis}_min", text=f"{axis.upper()} Min")
            r.prop(props, f"{axis}_max", text="Max")

class PT_VisibilityPanel(Panel):
    bl_label = "Line Design & Visibility"
    bl_idname = f"{PREFIX}_PT_visibility"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_parent_id = f"{PREFIX}_PT_main"

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return
        layout.prop(props, "thickness"); layout.prop(props, "draw_plane"); layout.separator()
        for i in range(1, 4):
            r = layout.row(align=True)
            r.prop(props, f"show_eq{i}", text=f"Eq {i}", toggle=True); r.prop(props, f"color{i}", text="")

class PT_CreatePanel(Panel):
    bl_label = "Line Detach"
    bl_idname = f"{PREFIX}_PT_create"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_parent_id = f"{PREFIX}_PT_main"

    def draw(self, context):
        col_exec = self.layout.column(); col_exec.scale_y = 2.0 
        col_exec.operator(OT_DetachLines.bl_idname, icon='MESH_CYLINDER', text="Detach Lines (位置を固定して切り離し)")

class PT_Torus1Panel(Panel):
    bl_label = "Torus Generator (Normal)"
    bl_idname = f"{PREFIX}_PT_torus1"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        row_prev = layout.row(); row_prev.scale_y = 1.5
        if not props.t1_enable_preview: row_prev.operator(OT_ShowTorus1Preview.bl_idname, icon='PLAY', text="Show Normal Torus Preview")
        else: row_prev.prop(props, "t1_enable_preview", text="Normal Torus Active", toggle=True, icon='PAUSE')

        box_eq = layout.box()
        box_eq.label(text="Center Line: z = (b/a)x", icon='IPO_CONSTANT')
        col = box_eq.column(align=True)
        col.prop(props, "t1_val_a"); col.prop(props, "t1_val_b")

        box_r = layout.box()
        box_r.prop(props, "t1_mode", text="")
        if props.t1_mode == 'INTERVAL':
            c_int = box_r.column(align=True)
            c_int.prop(props, "t1_z_center"); c_int.prop(props, "t1_z_interval"); c_int.prop(props, "t1_up_down_count")
        else:
            c_rng = box_r.column(align=True)
            c_rng.prop(props, "t1_z_min"); c_rng.prop(props, "t1_z_max"); c_rng.prop(props, "t1_count")

        box_s = layout.box()
        box_s.prop(props, "t1_major_radius"); box_s.prop(props, "t1_minor_radius"); box_s.prop(props, "t1_color")

        col_exec = layout.column(); col_exec.scale_y = 1.5
        col_exec.operator(OT_DetachTorus1.bl_idname, icon='MESH_TORUS', text="Detach Normal Torus")

class PT_Torus2Panel(Panel):
    bl_label = "Torus Generator (Elliptic / Lorentz)"
    bl_idname = f"{PREFIX}_PT_torus2"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        row_prev = layout.row(); row_prev.scale_y = 1.5
        if not props.t2_enable_preview: row_prev.operator(OT_ShowTorus2Preview.bl_idname, icon='PLAY', text="Show Elliptic Torus Preview")
        else: row_prev.prop(props, "t2_enable_preview", text="Elliptic Torus Active", toggle=True, icon='PAUSE')

        box_eq = layout.box()
        box_eq.label(text="Center Line: z = (b/a)x", icon='IPO_CONSTANT')
        col = box_eq.column(align=True)
        col.prop(props, "t2_val_a"); col.prop(props, "t2_val_b")

        box_r = layout.box()
        box_r.prop(props, "t2_mode", text="")
        if props.t2_mode == 'INTERVAL':
            c_int = box_r.column(align=True)
            c_int.prop(props, "t2_z_center"); c_int.prop(props, "t2_z_interval"); c_int.prop(props, "t2_up_down_count")
        else:
            c_rng = box_r.column(align=True)
            c_rng.prop(props, "t2_z_min"); c_rng.prop(props, "t2_z_max"); c_rng.prop(props, "t2_count")

        box_s = layout.box()
        box_s.prop(props, "t2_major_radius"); box_s.prop(props, "t2_minor_radius")
        box_s.separator()
        box_s.label(text="Lorentz Contraction (Scale)", icon='CON_SIZELIKE')
        box_s.prop(props, "t2_scale_x", text="X Scale (v=0.6c -> 0.8)")
        box_s.prop(props, "t2_color")

        col_exec = layout.column(); col_exec.scale_y = 1.5
        col_exec.operator(OT_DetachTorus2.bl_idname, icon='MESH_TORUS', text="Detach Elliptic Torus")

class PT_SystemPanel(Panel):
    bl_label = "System (Copy / Close)"
    bl_idname = f"{PREFIX}_PT_system"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        r_c = layout.row(); r_c.scale_y = 1.2
        r_c.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
        r_r = layout.row(); r_r.scale_y = 1.2; r_r.alert = True 
        r_r.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Close Addon (完全終了)")

class PT_LinksPanel(Panel):
    bl_label = "Links"
    bl_idname = f"{PREFIX}_PT_links"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        for l in ADDON_LINKS:
            self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"], icon='URL').url = l["url"]

# ==============================================================================
#  REGISTER
# ==============================================================================

classes = (
    PG_EquationProps, 
    OT_ShowLinePreview, OT_DetachLines, 
    OT_ShowTorus1Preview, OT_DetachTorus1,
    OT_ShowTorus2Preview, OT_DetachTorus2,
    OT_CopyFullScript, OT_OpenUrl, OT_RemoveAddon, 
    PT_MainPanel, PT_VisibilityPanel, PT_CreatePanel, 
    PT_Torus1Panel, PT_Torus2Panel, PT_SystemPanel, PT_LinksPanel
)

def auto_open_sidebar():
    try:
        for window in bpy.context.window_manager.windows:
            for area in window.screen.areas:
                if area.type == 'VIEW_3D':
                    for space in area.spaces:
                        if space.type == 'VIEW_3D':
                            if not space.show_region_ui: space.show_region_ui = True
    except: pass
    return None

def register():
    for c in classes: 
        try: bpy.utils.register_class(c)
        except ValueError: pass
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_EquationProps))
    bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)

def unregister():
    global _timer
    if _timer is not None:
        try: bpy.app.timers.unregister(_timer)
        except Exception: pass
        _timer = None
    cleanup_preview_data()
    if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
    for c in reversed(classes): 
        try: bpy.utils.unregister_class(c)
        except ValueError: pass

if __name__ == "__main__": 
    register()
# Copied: 2026-04-08 12:00:00
import bpy
import time
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime

# ==============================================================================
#  【 基本設定エリア 】
# ==============================================================================

PREFIX       = "EqGen"
ADDON_NAME   = "[ Equation Gen ]"
TAB_NAME     = "[ Equation Gen ]"
PANEL_TITLE  = "Equation Lines"
AUTHOR       = "zionadchat"

# ★ このスクリプト自身のID(絶対に消さないこと)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: EQ_LINES_2026_04_08_V5_1 ###"

# ==============================================================================
#  システム初期化 & ID管理
# ==============================================================================

bl_info = {
    "name": f"{ADDON_NAME} {PREFIX}",
    "author": AUTHOR,
    "version": (5, 1, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": "Equation Lines & Torus Generator with independent calculations",
    "category": "3D View",
}

OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"

ADDON_LINKS = (
    {"label": "Prefix 20260408", "url": "<https://www.notion.so/Prefix-20260408-33cf5dacaf43807e9e35ff8cdbbc39c6>"},
)

# ==============================================================================
#  デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "enable_preview": False,
    "val_a": 0.6000,
    "val_b": 1.0000,
    "val_d": 10.0000,
    "x_min": -50.0,
    "x_max": 50.0,
    "y_min": -50.0,
    "y_max": 50.0,
    "z_min": -50.0,
    "z_max": 50.0,
    "thickness": 0.5000,
    "draw_plane": "XZ",
    "show_eq1": True,
    "show_eq2": True,
    "show_eq3": True,
    "color1": (1.0000, 0.2000, 0.2000, 1.0000),
    "color2": (0.2000, 1.0000, 0.2000, 1.0000),
    "color3": (0.2000, 0.2000, 1.0000, 1.0000),

    # Torus Properties
    "t_enable_preview": False,
    "t_mode": "INTERVAL", # 'RANGE' または 'INTERVAL'
    "t_val_a": 0.6000,
    "t_val_b": 1.0000,
    
    # RANGE Mode defaults
    "t_z_min": -50.0,
    "t_z_max": 50.0,
    "t_count": 11,
    
    # INTERVAL Mode defaults
    "t_z_center": 0.0,
    "t_z_interval": 1.0,
    "t_up_down_count": 5,

    "t_major_radius": 5.0,
    "t_minor_radius": 1.0,
    "t_color": (0.2000, 0.8000, 0.8000, 1.0000),
}
# <END_DICT>

PREVIEW_COL_LINE = f"{PREFIX}_Line_Preview"
PREVIEW_COL_TORUS = f"{PREFIX}_Torus_Preview"

# ==============================================================================
#  共通マテリアル・データ管理 ロジック
# ==============================================================================

def cleanup_preview_data():
    for name in [PREVIEW_COL_LINE, PREVIEW_COL_TORUS]:
        col = bpy.data.collections.get(name)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0:
                    if isinstance(data, bpy.types.Curve):
                        bpy.data.curves.remove(data)
            
            if len(col.objects) == 0:
                bpy.data.collections.remove(col)

def apply_material_settings(mat, color):
    mat.use_nodes = True
    mat.blend_method = 'BLEND'
    mat.diffuse_color = color
    
    tree = mat.node_tree
    bsdf = tree.nodes.get("Principled BSDF")
    if not bsdf:
        tree.nodes.clear()
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        bsdf.name = "Principled BSDF"
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        tree.links.new(bsdf.outputs[0], out.inputs[0])
        
    if "Base Color" in bsdf.inputs:
        bsdf.inputs["Base Color"].default_value = color
    if "Alpha" in bsdf.inputs:
        bsdf.inputs["Alpha"].default_value = color[3]

def get_preview_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name=name)
    apply_material_settings(mat, color)
    return mat

# ==============================================================================
#  Line プレビューロジック
# ==============================================================================

def get_clipped_segment(m, c, x_min, x_max, v_min, v_max):
    if m == 0:
        if v_min <= c <= v_max:
            return x_min, x_max
        return None, None
    else:
        x_from_v1, x_from_v2 = (v_min - c) / m, (v_max - c) / m
        valid_x_min, valid_x_max = min(x_from_v1, x_from_v2), max(x_from_v1, x_from_v2)
        act_x_min, act_x_max = max(x_min, valid_x_min), min(x_max, valid_x_max)
        if act_x_min > act_x_max: 
            return None, None
        return act_x_min, act_x_max

def calc_points(props, m, c):
    x_min, x_max = min(props.x_min, props.x_max), max(props.x_min, props.x_max)
    y_min, y_max = min(props.y_min, props.y_max), max(props.y_min, props.y_max)
    z_min, z_max = min(props.z_min, props.z_max), max(props.z_min, props.z_max)
    
    if props.draw_plane == 'XZ':
        act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, z_min, z_max)
        if act_x_min is None: return None, None
        return (act_x_min, 0.0, m * act_x_min + c), (act_x_max, 0.0, m * act_x_max + c)
    else:
        act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, y_min, y_max)
        if act_x_min is None: return None, None
        return (act_x_min, m * act_x_min + c, 0.0), (act_x_max, m * act_x_max + c, 0.0)

def update_line_preview(context, props):
    if not props.enable_preview:
        col = bpy.data.collections.get(PREVIEW_COL_LINE)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0: bpy.data.curves.remove(data)
            bpy.data.collections.remove(col)
        return

    col = bpy.data.collections.get(PREVIEW_COL_LINE)
    if not col:
        col = bpy.data.collections.new(PREVIEW_COL_LINE)
        context.scene.collection.children.link(col)

    a = props.val_a if abs(props.val_a) > 0.0001 else 0.0001
    b, d = props.val_b, props.val_d
    m = b / a

    equations = [
        {"id": 1, "show": props.show_eq1, "color": props.color1, "offset": 0.0},
        {"id": 2, "show": props.show_eq2, "color": props.color2, "offset": -d},
        {"id": 3, "show": props.show_eq3, "color": props.color3, "offset": d},
    ]

    for eq in equations:
        obj_name = f"[Preview] EqLine_{eq['id']}"
        obj = bpy.data.objects.get(obj_name)

        if not eq["show"]:
            if obj: obj.hide_viewport = obj.hide_render = True
            continue

        p1, p2 = calc_points(props, m, eq["offset"])
        if p1 is None:
            if obj: obj.hide_viewport = obj.hide_render = True
            continue

        if not obj:
            curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
            curve.dimensions = '3D'
            curve.fill_mode = 'FULL'
            spline = curve.splines.new('POLY')
            spline.points.add(1)
            obj = bpy.data.objects.new(obj_name, curve)
            col.objects.link(obj)
        else:
            curve = obj.data
            spline = curve.splines[0]

        curve.bevel_depth = props.thickness
        curve.bevel_resolution = 6
        spline.points[0].co = (*p1, 1.0)
        spline.points[1].co = (*p2, 1.0)
        obj.hide_viewport = obj.hide_render = False

        mat = get_preview_material(f"Preview_Mat_EqLine{eq['id']}", eq["color"])
        if not obj.data.materials: obj.data.materials.append(mat)
        else: obj.data.materials[0] = mat

# ==============================================================================
#  Torus プレビューロジック (Z=0平面に平行)
# ==============================================================================

def build_curve_circle(curve, radius, segments=32):
    if len(curve.splines) == 0:
        spline = curve.splines.new('POLY')
        spline.points.add(segments - 1)
        spline.use_cyclic_u = True
    else:
        spline = curve.splines[0]
        if len(spline.points) != segments:
            curve.splines.clear()
            spline = curve.splines.new('POLY')
            spline.points.add(segments - 1)
            spline.use_cyclic_u = True
            
    for i in range(segments):
        angle = 2 * math.pi * i / segments
        # XY平面上の円を作成 (Z=0に平行)
        spline.points[i].co = (radius * math.cos(angle), radius * math.sin(angle), 0.0, 1.0)

def update_torus_preview(context, props):
    if not props.t_enable_preview:
        col = bpy.data.collections.get(PREVIEW_COL_TORUS)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0: bpy.data.curves.remove(data)
            bpy.data.collections.remove(col)
        return

    col = bpy.data.collections.get(PREVIEW_COL_TORUS)
    if not col:
        col = bpy.data.collections.new(PREVIEW_COL_TORUS)
        context.scene.collection.children.link(col)

    a = props.t_val_a
    # Z から X を求めるため、bが分母になる (z = (b/a)x -> x = z * a / b)
    b_val = props.t_val_b if abs(props.t_val_b) > 0.0001 else (0.0001 if props.t_val_b >= 0 else -0.0001)
    
    # 選択モードに応じてZ座標のリストを生成
    z_list = []
    if props.t_mode == 'RANGE':
        count = props.t_count
        for i in range(count):
            t = i / (count - 1) if count > 1 else 0.5
            z = props.t_z_min + t * (props.t_z_max - props.t_z_min)
            z_list.append(z)
    else: # INTERVAL
        center = props.t_z_center
        interval = props.t_z_interval
        ud_count = props.t_up_down_count
        # -ud_count から +ud_count まで (例: 5なら -5から+5の計11個)
        for i in range(-ud_count, ud_count + 1):
            z = center + i * interval
            z_list.append(z)

    existing_objs = list(col.objects)
    mat = get_preview_material("Preview_Mat_Torus", props.t_color)

    for i, z in enumerate(z_list):
        obj_name = f"[Preview] Torus_{i+1}"
        
        # 座標計算 z = (b/a)x  =>  x = z * a / b
        x = z * (a / b_val)
        y = 0.0 # XZ平面上に配置
        
        if i < len(existing_objs):
            obj = existing_objs[i]
            curve = obj.data
        else:
            curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
            curve.dimensions = '3D'
            curve.fill_mode = 'FULL'
            obj = bpy.data.objects.new(obj_name, curve)
            col.objects.link(obj)
            
        curve.bevel_depth = props.t_minor_radius
        curve.bevel_resolution = 8
        build_curve_circle(curve, props.t_major_radius)
        
        obj.location = (x, y, z)
        obj.hide_viewport = False
        obj.hide_render = False
        
        if not obj.data.materials: obj.data.materials.append(mat)
        else: obj.data.materials[0] = mat

    # 個数が減った場合、余分なオブジェクトを削除
    if len(existing_objs) > len(z_list):
        for obj in existing_objs[len(z_list):]:
            data = obj.data
            bpy.data.objects.remove(obj, do_unlink=True)
            if data and data.users == 0:
                bpy.data.curves.remove(data)

# ==============================================================================
#  タイマー管理
# ==============================================================================

_timer = None
_last_update_time = 0

def delayed_update():
    global _timer, _last_update_time
    _timer = None
    now = time.time()
    if now - _last_update_time < 0.05:
        if _timer is None:
            _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
        return None
    _last_update_time = now
    
    ctx = bpy.context
    if not ctx or not ctx.scene: return None
    if ctx.object and ctx.object.mode != 'OBJECT': return None

    props = getattr(ctx.scene, PROPS_NAME, None)
    if props:
        update_line_preview(ctx, props)
        update_torus_preview(ctx, props)
    return None

def on_update(self, context):
    global _timer
    if _timer is None: 
        _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)

# ==============================================================================
#  PROPERTIES
# ==============================================================================

class PG_EquationProps(PropertyGroup):
    # Line Properties
    enable_preview: BoolProperty(name="Enable Line Preview", default=CURRENT_DEFAULTS['enable_preview'], update=on_update)
    val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['val_a'], update=on_update)
    val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['val_b'], update=on_update)
    val_d: FloatProperty(name="Value d", default=CURRENT_DEFAULTS['val_d'], update=on_update)
    x_min: FloatProperty(name="X Min", default=CURRENT_DEFAULTS['x_min'], update=on_update)
    x_max: FloatProperty(name="X Max", default=CURRENT_DEFAULTS['x_max'], update=on_update)
    y_min: FloatProperty(name="Y Min", default=CURRENT_DEFAULTS['y_min'], update=on_update)
    y_max: FloatProperty(name="Y Max", default=CURRENT_DEFAULTS['y_max'], update=on_update)
    z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['z_min'], update=on_update)
    z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['z_max'], update=on_update)
    thickness: FloatProperty(name="Line Thickness", default=CURRENT_DEFAULTS['thickness'], min=0.01, max=10.0, update=on_update)
    draw_plane: EnumProperty(name="Draw Plane", items=[('XZ', "Front (XZ)", "XZ Plane"), ('XY', "Top (XY)", "XY Plane")], default=CURRENT_DEFAULTS['draw_plane'], update=on_update)
    show_eq1: BoolProperty(name="Show Eq 1", default=CURRENT_DEFAULTS['show_eq1'], update=on_update)
    show_eq2: BoolProperty(name="Show Eq 2", default=CURRENT_DEFAULTS['show_eq2'], update=on_update)
    show_eq3: BoolProperty(name="Show Eq 3", default=CURRENT_DEFAULTS['show_eq3'], update=on_update)
    color1: FloatVectorProperty(name="Color 1", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color1'], update=on_update)
    color2: FloatVectorProperty(name="Color 2", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color2'], update=on_update)
    color3: FloatVectorProperty(name="Color 3", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color3'], update=on_update)

    # Torus Properties
    t_enable_preview: BoolProperty(name="Enable Torus Preview", default=CURRENT_DEFAULTS['t_enable_preview'], update=on_update)
    t_mode: EnumProperty(
        name="Placement Mode",
        items=[
            ('INTERVAL', "Interval Mode", "Set Center Z, Interval, and Up/Down counts"),
            ('RANGE', "Range Mode", "Set Min Z, Max Z, and Total Count")
        ],
        default=CURRENT_DEFAULTS['t_mode'], update=on_update
    )
    t_val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['t_val_a'], update=on_update)
    t_val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['t_val_b'], update=on_update)
    
    # Range mode
    t_z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['t_z_min'], update=on_update)
    t_z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['t_z_max'], update=on_update)
    t_count: IntProperty(name="Total Count", default=CURRENT_DEFAULTS['t_count'], min=1, max=500, update=on_update)
    
    # Interval mode
    t_z_center: FloatProperty(name="Z Center", default=CURRENT_DEFAULTS['t_z_center'], update=on_update)
    t_z_interval: FloatProperty(name="Z Interval", default=CURRENT_DEFAULTS['t_z_interval'], update=on_update)
    t_up_down_count: IntProperty(name="Up/Down Count", default=CURRENT_DEFAULTS['t_up_down_count'], min=0, max=100, update=on_update)

    t_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['t_major_radius'], min=0.1, max=100.0, update=on_update)
    t_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['t_minor_radius'], min=0.01, max=50.0, update=on_update)
    t_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['t_color'], update=on_update)

# ==============================================================================
#  OPERATORS
# ==============================================================================

class OT_ShowLinePreview(Operator):
    bl_idname = f"{OP_PREFIX}.show_line_preview"
    bl_label = "Show Line Preview"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props:
            props.enable_preview = True
            update_line_preview(context, props)
        return {'FINISHED'}

class OT_DetachLines(Operator):
    bl_idname = f"{OP_PREFIX}.detach_lines"
    bl_label = "Detach Lines"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col_preview = bpy.data.collections.get(PREVIEW_COL_LINE)
        if not col_preview or len(col_preview.objects) == 0:
            self.report({'WARNING'}, "切り離すプレビュー線が見つかりません。")
            return {'CANCELLED'}
        target_col = context.collection
        timestamp = datetime.now().strftime('%H%M%S')
        bpy.ops.object.select_all(action='DESELECT')
        for obj in list(col_preview.objects):
            if obj.hide_viewport: continue
            if obj.name not in target_col.objects: target_col.objects.link(obj)
            col_preview.objects.unlink(obj)
            obj.name = obj.name.replace("[Preview]", "Solid_EqLine") + f"_{timestamp}"
            if obj.data.materials:
                mat = obj.data.materials[0]
                new_mat = mat.copy()
                new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
                obj.data.materials[0] = new_mat
            obj.select_set(True)
        self.report({'INFO'}, "Lines Detached!")
        props = getattr(context.scene, PROPS_NAME, None)
        if props: update_line_preview(context, props)
        return {'FINISHED'}

class OT_ShowTorusPreview(Operator):
    bl_idname = f"{OP_PREFIX}.show_torus_preview"
    bl_label = "Show Torus Preview"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props:
            props.t_enable_preview = True
            update_torus_preview(context, props)
        return {'FINISHED'}

class OT_DetachTorus(Operator):
    bl_idname = f"{OP_PREFIX}.detach_torus"
    bl_label = "Detach Torus"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col_preview = bpy.data.collections.get(PREVIEW_COL_TORUS)
        if not col_preview or len(col_preview.objects) == 0:
            self.report({'WARNING'}, "切り離すトーラスが見つかりません。")
            return {'CANCELLED'}
        target_col = context.collection
        timestamp = datetime.now().strftime('%H%M%S')
        bpy.ops.object.select_all(action='DESELECT')
        for obj in list(col_preview.objects):
            if obj.name not in target_col.objects: target_col.objects.link(obj)
            col_preview.objects.unlink(obj)
            obj.name = obj.name.replace("[Preview]", "Solid_Torus") + f"_{timestamp}"
            if obj.data.materials:
                mat = obj.data.materials[0]
                new_mat = mat.copy()
                new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
                obj.data.materials[0] = new_mat
            obj.select_set(True)
        self.report({'INFO'}, "Torus Detached!")
        props = getattr(context.scene, PROPS_NAME, None)
        if props: update_torus_preview(context, props)
        return {'FINISHED'}

class OT_CopyFullScript(Operator):
    bl_idname = f"{OP_PREFIX}.copy_script"
    bl_label = "Copy Script"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        target_text = None
        for t in bpy.data.texts:
            if SOURCE_ID_TAG in t.as_string(): target_text = t; break
        if not target_text: return {'CANCELLED'}

        code = target_text.as_string()
        c1, c2, c3, tc = props.color1, props.color2, props.color3, props.t_color
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "enable_preview": {props.enable_preview},\n'
        new_dict += f'    "val_a": {props.val_a:.4f},\n'
        new_dict += f'    "val_b": {props.val_b:.4f},\n'
        new_dict += f'    "val_d": {props.val_d:.4f},\n'
        new_dict += f'    "x_min": {props.x_min:.4f}, "x_max": {props.x_max:.4f},\n'
        new_dict += f'    "y_min": {props.y_min:.4f}, "y_max": {props.y_max:.4f},\n'
        new_dict += f'    "z_min": {props.z_min:.4f}, "z_max": {props.z_max:.4f},\n'
        new_dict += f'    "thickness": {props.thickness:.4f},\n'
        new_dict += f'    "draw_plane": "{props.draw_plane}",\n'
        new_dict += f'    "show_eq1": {props.show_eq1}, "show_eq2": {props.show_eq2}, "show_eq3": {props.show_eq3},\n'
        new_dict += f'    "color1": ({c1[0]:.4f}, {c1[1]:.4f}, {c1[2]:.4f}, {c1[3]:.4f}),\n'
        new_dict += f'    "color2": ({c2[0]:.4f}, {c2[1]:.4f}, {c2[2]:.4f}, {c2[3]:.4f}),\n'
        new_dict += f'    "color3": ({c3[0]:.4f}, {c3[1]:.4f}, {c3[2]:.4f}, {c3[3]:.4f}),\n'
        
        new_dict += f'    "t_enable_preview": {props.t_enable_preview},\n'
        new_dict += f'    "t_mode": "{props.t_mode}",\n'
        new_dict += f'    "t_val_a": {props.t_val_a:.4f}, "t_val_b": {props.t_val_b:.4f},\n'
        new_dict += f'    "t_z_min": {props.t_z_min:.4f}, "t_z_max": {props.t_z_max:.4f}, "t_count": {props.t_count},\n'
        new_dict += f'    "t_z_center": {props.t_z_center:.4f}, "t_z_interval": {props.t_z_interval:.4f}, "t_up_down_count": {props.t_up_down_count},\n'
        new_dict += f'    "t_major_radius": {props.t_major_radius:.4f}, "t_minor_radius": {props.t_minor_radius:.4f},\n'
        new_dict += f'    "t_color": ({tc[0]:.4f}, {tc[1]:.4f}, {tc[2]:.4f}, {tc[3]:.4f}),\n'
        new_dict += "}\n"

        try:
            tag_start = "# <BEGIN" + "_DICT>"
            tag_end = "# <END" + "_DICT>"
            pre_code, rest = code.split(tag_start, 1)
            _, post_code = rest.split(tag_end, 1)
            final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
            context.window_manager.clipboard = final_code
            self.report({'INFO'}, "Code copied!")
        except: return {'CANCELLED'}
        return {'FINISHED'}

class OT_OpenUrl(Operator):
    bl_idname = f"{OP_PREFIX}.open_url"
    bl_label = "Open URL"
    url: StringProperty()
    def execute(self, context): 
        webbrowser.open(self.url)
        return {'FINISHED'}

class OT_RemoveAddon(Operator):
    bl_idname = f"{OP_PREFIX}.remove_addon"
    bl_label = "Close Addon"
    def execute(self, context):
        bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
        self.report({'INFO'}, "アドオンを終了しました。")
        return {'FINISHED'}

# ==============================================================================
#  PANELS
# ==============================================================================

class PT_MainPanel(Panel):
    bl_label = PANEL_TITLE
    bl_idname = f"{PREFIX}_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        # --- Line Preview Button ---
        row_prev = layout.row()
        row_prev.scale_y = 1.5
        if not props.enable_preview:
            row_prev.operator(OT_ShowLinePreview.bl_idname, icon='PLAY', text="Show Line Preview (表示開始)")
        else:
            row_prev.prop(props, "enable_preview", text="Line Preview Active (ON/OFF)", toggle=True, icon='PAUSE')

        box_info = layout.box()
        box_info.label(text="【 Line Info 】", icon='INFO')
        a_str = f"{props.val_a:.2f}" if abs(props.val_a) > 0.0001 else "0.00(Err)"
        box_info.label(text=f"y = ({props.val_b:.2f} / {a_str}) x")
        
        box_values = layout.box()
        col_v = box_values.column(align=True)
        col_v.prop(props, "val_a")
        col_v.prop(props, "val_b")
        col_v.prop(props, "val_d")
        
        box_limits = layout.box()
        box_limits.label(text="Limits (X, Y, Z)", icon='MOD_HULL')
        for axis in ['x', 'y', 'z']:
            r = box_limits.row(align=True)
            r.prop(props, f"{axis}_min", text=f"{axis.upper()} Min")
            r.prop(props, f"{axis}_max", text="Max")

class PT_VisibilityPanel(Panel):
    bl_label = "Line Design & Visibility"
    bl_idname = f"{PREFIX}_PT_visibility"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_parent_id = f"{PREFIX}_PT_main"

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return
        
        layout.prop(props, "thickness")
        layout.prop(props, "draw_plane")
        layout.separator()
        for i in range(1, 4):
            r = layout.row(align=True)
            r.prop(props, f"show_eq{i}", text=f"Eq {i}", toggle=True)
            r.prop(props, f"color{i}", text="")

class PT_CreatePanel(Panel):
    bl_label = "Line Detach"
    bl_idname = f"{PREFIX}_PT_create"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_parent_id = f"{PREFIX}_PT_main"

    def draw(self, context):
        col_exec = self.layout.column()
        col_exec.scale_y = 2.0 
        col_exec.operator(OT_DetachLines.bl_idname, icon='MESH_CYLINDER', text="Detach Lines (位置を固定して切り離し)")

class PT_TorusPanel(Panel):
    bl_label = "Torus Generator"
    bl_idname = f"{PREFIX}_PT_torus"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        row_prev = layout.row()
        row_prev.scale_y = 1.5
        if not props.t_enable_preview:
            row_prev.operator(OT_ShowTorusPreview.bl_idname, icon='PLAY', text="Show Torus Preview (表示開始)")
        else:
            row_prev.prop(props, "t_enable_preview", text="Torus Preview Active (ON/OFF)", toggle=True, icon='PAUSE')

        box_eq = layout.box()
        box_eq.label(text="Center Line: z = (b/a)x", icon='IPO_CONSTANT')
        col = box_eq.column(align=True)
        col.prop(props, "t_val_a")
        col.prop(props, "t_val_b")

        box_r = layout.box()
        box_r.label(text="Placement Mode", icon='UV_SYNC_SELECT')
        box_r.prop(props, "t_mode", text="")
        box_r.separator()

        if props.t_mode == 'INTERVAL':
            c_int = box_r.column(align=True)
            c_int.prop(props, "t_z_center")
            c_int.prop(props, "t_z_interval")
            c_int.prop(props, "t_up_down_count")
        else:
            c_rng = box_r.column(align=True)
            c_rng.prop(props, "t_z_min")
            c_rng.prop(props, "t_z_max")
            c_rng.prop(props, "t_count")

        box_s = layout.box()
        box_s.label(text="Torus Shape", icon='MESH_TORUS')
        box_s.prop(props, "t_major_radius")
        box_s.prop(props, "t_minor_radius")
        box_s.prop(props, "t_color")

        col_exec = layout.column()
        col_exec.scale_y = 1.5
        col_exec.operator(OT_DetachTorus.bl_idname, icon='MESH_TORUS', text="Detach Torus (固定化)")

class PT_SystemPanel(Panel):
    bl_label = "System (Copy / Close)"
    bl_idname = f"{PREFIX}_PT_system"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        r_c = layout.row()
        r_c.scale_y = 1.2
        r_c.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
        
        r_r = layout.row()
        r_r.scale_y = 1.2
        r_r.alert = True 
        r_r.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Close Addon (完全終了)")

class PT_LinksPanel(Panel):
    bl_label = "Links"
    bl_idname = f"{PREFIX}_PT_links"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

    def draw(self, context):
        for l in ADDON_LINKS:
            self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"], icon='URL').url = l["url"]

# ==============================================================================
#  REGISTER
# ==============================================================================

classes = (
    PG_EquationProps, 
    OT_ShowLinePreview, OT_DetachLines, 
    OT_ShowTorusPreview, OT_DetachTorus,
    OT_CopyFullScript, OT_OpenUrl, OT_RemoveAddon, 
    PT_MainPanel, PT_VisibilityPanel, PT_CreatePanel, 
    PT_TorusPanel, PT_SystemPanel, PT_LinksPanel
)

def auto_open_sidebar():
    try:
        for window in bpy.context.window_manager.windows:
            for area in window.screen.areas:
                if area.type == 'VIEW_3D':
                    for space in area.spaces:
                        if space.type == 'VIEW_3D':
                            if not space.show_region_ui:
                                space.show_region_ui = True
    except: pass
    return None

def register():
    for c in classes: 
        try: bpy.utils.register_class(c)
        except ValueError: pass
            
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_EquationProps))
    bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)

def unregister():
    global _timer
    if _timer is not None:
        try: bpy.app.timers.unregister(_timer)
        except Exception: pass
        _timer = None

    cleanup_preview_data()

    if hasattr(bpy.types.Scene, PROPS_NAME): 
        delattr(bpy.types.Scene, PROPS_NAME)
        
    for c in reversed(classes): 
        try: bpy.utils.unregister_class(c)
        except ValueError: pass

if __name__ == "__main__": 
    register()
# Copied: 2026-04-08 12:00:00
import bpy
import time
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime

# ==============================================================================
#  【 基本設定エリア 】
# ==============================================================================

PREFIX       = "EqGen"
ADDON_NAME   = "[ Equation Gen ]"
TAB_NAME     = "[ Equation Gen ]"
PANEL_TITLE  = "Equation Lines"
AUTHOR       = "zionadchat"

# ★ このスクリプト自身のID(絶対に消さないこと)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: EQ_LINES_2026_04_08_V5 ###"

# ==============================================================================
#  システム初期化 & ID管理
# ==============================================================================

bl_info = {
    "name": f"{ADDON_NAME} {PREFIX}",
    "author": AUTHOR,
    "version": (5, 0, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": "Equation Lines & Torus Generator with independent calculations",
    "category": "3D View",
}

OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"

ADDON_LINKS = (
    {"label": "Prefix 20260408", "url": "<https://www.notion.so/Prefix-20260408-33cf5dacaf43807e9e35ff8cdbbc39c6>"},
)

# ==============================================================================
#  デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "enable_preview": False,
    "val_a": 0.6000,
    "val_b": 1.0000,
    "val_d": 10.0000,
    "x_min": -50.0,
    "x_max": 50.0,
    "y_min": -50.0,
    "y_max": 50.0,
    "z_min": -50.0,
    "z_max": 50.0,
    "thickness": 0.5000,
    "draw_plane": "XZ",
    "show_eq1": True,
    "show_eq2": True,
    "show_eq3": True,
    "color1": (1.0000, 0.2000, 0.2000, 1.0000),
    "color2": (0.2000, 1.0000, 0.2000, 1.0000),
    "color3": (0.2000, 0.2000, 1.0000, 1.0000),

    # Torus Properties
    "t_enable_preview": False,
    "t_val_a": 0.6000,
    "t_val_b": 1.0000,
    "t_x_min": -50.0,
    "t_x_max": 50.0,
    "t_z_min": -50.0,
    "t_z_max": 50.0,
    "t_count": 11,
    "t_major_radius": 5.0,
    "t_minor_radius": 1.0,
    "t_color": (0.2000, 0.8000, 0.8000, 1.0000),
}
# <END_DICT>

PREVIEW_COL_LINE = f"{PREFIX}_Line_Preview"
PREVIEW_COL_TORUS = f"{PREFIX}_Torus_Preview"

# ==============================================================================
#  共通マテリアル・データ管理 ロジック
# ==============================================================================

def cleanup_preview_data():
    for name in [PREVIEW_COL_LINE, PREVIEW_COL_TORUS]:
        col = bpy.data.collections.get(name)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0:
                    if isinstance(data, bpy.types.Curve):
                        bpy.data.curves.remove(data)
            
            if len(col.objects) == 0:
                bpy.data.collections.remove(col)

def apply_material_settings(mat, color):
    mat.use_nodes = True
    mat.blend_method = 'BLEND'
    mat.diffuse_color = color
    
    tree = mat.node_tree
    bsdf = tree.nodes.get("Principled BSDF")
    if not bsdf:
        tree.nodes.clear()
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        bsdf.name = "Principled BSDF"
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        tree.links.new(bsdf.outputs[0], out.inputs[0])
        
    if "Base Color" in bsdf.inputs:
        bsdf.inputs["Base Color"].default_value = color
    if "Alpha" in bsdf.inputs:
        bsdf.inputs["Alpha"].default_value = color[3]

def get_preview_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name=name)
    apply_material_settings(mat, color)
    return mat

# ==============================================================================
#  Line プレビューロジック
# ==============================================================================

def get_clipped_segment(m, c, x_min, x_max, v_min, v_max):
    if m == 0:
        if v_min <= c <= v_max:
            return x_min, x_max
        return None, None
    else:
        x_from_v1, x_from_v2 = (v_min - c) / m, (v_max - c) / m
        valid_x_min, valid_x_max = min(x_from_v1, x_from_v2), max(x_from_v1, x_from_v2)
        act_x_min, act_x_max = max(x_min, valid_x_min), min(x_max, valid_x_max)
        if act_x_min > act_x_max: 
            return None, None
        return act_x_min, act_x_max

def calc_points(props, m, c):
    x_min, x_max = min(props.x_min, props.x_max), max(props.x_min, props.x_max)
    y_min, y_max = min(props.y_min, props.y_max), max(props.y_min, props.y_max)
    z_min, z_max = min(props.z_min, props.z_max), max(props.z_min, props.z_max)
    
    if props.draw_plane == 'XZ':
        act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, z_min, z_max)
        if act_x_min is None: return None, None
        return (act_x_min, 0.0, m * act_x_min + c), (act_x_max, 0.0, m * act_x_max + c)
    else:
        act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, y_min, y_max)
        if act_x_min is None: return None, None
        return (act_x_min, m * act_x_min + c, 0.0), (act_x_max, m * act_x_max + c, 0.0)

def update_line_preview(context, props):
    if not props.enable_preview:
        col = bpy.data.collections.get(PREVIEW_COL_LINE)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0: bpy.data.curves.remove(data)
            bpy.data.collections.remove(col)
        return

    col = bpy.data.collections.get(PREVIEW_COL_LINE)
    if not col:
        col = bpy.data.collections.new(PREVIEW_COL_LINE)
        context.scene.collection.children.link(col)

    a = props.val_a if abs(props.val_a) > 0.0001 else 0.0001
    b, d = props.val_b, props.val_d
    m = b / a

    equations = [
        {"id": 1, "show": props.show_eq1, "color": props.color1, "offset": 0.0},
        {"id": 2, "show": props.show_eq2, "color": props.color2, "offset": -d},
        {"id": 3, "show": props.show_eq3, "color": props.color3, "offset": d},
    ]

    for eq in equations:
        obj_name = f"[Preview] EqLine_{eq['id']}"
        obj = bpy.data.objects.get(obj_name)

        if not eq["show"]:
            if obj: obj.hide_viewport = obj.hide_render = True
            continue

        p1, p2 = calc_points(props, m, eq["offset"])
        if p1 is None:
            if obj: obj.hide_viewport = obj.hide_render = True
            continue

        if not obj:
            curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
            curve.dimensions = '3D'
            curve.fill_mode = 'FULL'
            spline = curve.splines.new('POLY')
            spline.points.add(1)
            obj = bpy.data.objects.new(obj_name, curve)
            col.objects.link(obj)
        else:
            curve = obj.data
            spline = curve.splines[0]

        curve.bevel_depth = props.thickness
        curve.bevel_resolution = 6
        spline.points[0].co = (*p1, 1.0)
        spline.points[1].co = (*p2, 1.0)
        obj.hide_viewport = obj.hide_render = False

        mat = get_preview_material(f"Preview_Mat_EqLine{eq['id']}", eq["color"])
        if not obj.data.materials: obj.data.materials.append(mat)
        else: obj.data.materials[0] = mat

# ==============================================================================
#  Torus プレビューロジック (Z=0平面に平行)
# ==============================================================================

def build_curve_circle(curve, radius, segments=32):
    if len(curve.splines) == 0:
        spline = curve.splines.new('POLY')
        spline.points.add(segments - 1)
        spline.use_cyclic_u = True
    else:
        spline = curve.splines[0]
        if len(spline.points) != segments:
            curve.splines.clear()
            spline = curve.splines.new('POLY')
            spline.points.add(segments - 1)
            spline.use_cyclic_u = True
            
    for i in range(segments):
        angle = 2 * math.pi * i / segments
        # XY平面上の円を作成 (Z=0に平行)
        spline.points[i].co = (radius * math.cos(angle), radius * math.sin(angle), 0.0, 1.0)

def update_torus_preview(context, props):
    if not props.t_enable_preview:
        col = bpy.data.collections.get(PREVIEW_COL_TORUS)
        if col:
            for obj in list(col.objects):
                data = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if data and data.users == 0: bpy.data.curves.remove(data)
            bpy.data.collections.remove(col)
        return

    col = bpy.data.collections.get(PREVIEW_COL_TORUS)
    if not col:
        col = bpy.data.collections.new(PREVIEW_COL_TORUS)
        context.scene.collection.children.link(col)

    a = props.t_val_a if abs(props.t_val_a) > 0.0001 else 0.0001
    b = props.t_val_b
    count = props.t_count
    existing_objs = list(col.objects)
    
    mat = get_preview_material("Preview_Mat_Torus", props.t_color)

    for i in range(count):
        obj_name = f"[Preview] Torus_{i+1}"
        
        # 補間比率
        t = i / (count - 1) if count > 1 else 0.5
        x = props.t_x_min + t * (props.t_x_max - props.t_x_min)
        z = props.t_z_min + t * (props.t_z_max - props.t_z_min)
        y = (b / a) * x
        
        if i < len(existing_objs):
            obj = existing_objs[i]
            curve = obj.data
        else:
            curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
            curve.dimensions = '3D'
            curve.fill_mode = 'FULL'
            obj = bpy.data.objects.new(obj_name, curve)
            col.objects.link(obj)
            
        curve.bevel_depth = props.t_minor_radius
        curve.bevel_resolution = 8
        build_curve_circle(curve, props.t_major_radius)
        
        # 中心位置に移動
        obj.location = (x, y, z)
        obj.hide_viewport = False
        obj.hide_render = False
        
        if not obj.data.materials: obj.data.materials.append(mat)
        else: obj.data.materials[0] = mat

    # 余分なオブジェクトを削除
    if len(existing_objs) > count:
        for obj in existing_objs[count:]:
            data = obj.data
            bpy.data.objects.remove(obj, do_unlink=True)
            if data and data.users == 0:
                bpy.data.curves.remove(data)

# ==============================================================================
#  タイマー管理
# ==============================================================================

_timer = None
_last_update_time = 0

def delayed_update():
    global _timer, _last_update_time
    _timer = None
    now = time.time()
    if now - _last_update_time < 0.05:
        if _timer is None:
            _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
        return None
    _last_update_time = now
    
    ctx = bpy.context
    if not ctx or not ctx.scene: return None
    if ctx.object and ctx.object.mode != 'OBJECT': return None

    props = getattr(ctx.scene, PROPS_NAME, None)
    if props:
        update_line_preview(ctx, props)
        update_torus_preview(ctx, props)
    return None

def on_update(self, context):
    global _timer
    if _timer is None: 
        _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)

# ==============================================================================
#  PROPERTIES
# ==============================================================================

class PG_EquationProps(PropertyGroup):
    # Line Properties
    enable_preview: BoolProperty(name="Enable Line Preview", default=CURRENT_DEFAULTS['enable_preview'], update=on_update)
    val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['val_a'], update=on_update)
    val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['val_b'], update=on_update)
    val_d: FloatProperty(name="Value d", default=CURRENT_DEFAULTS['val_d'], update=on_update)
    x_min: FloatProperty(name="X Min", default=CURRENT_DEFAULTS['x_min'], update=on_update)
    x_max: FloatProperty(name="X Max", default=CURRENT_DEFAULTS['x_max'], update=on_update)
    y_min: FloatProperty(name="Y Min", default=CURRENT_DEFAULTS['y_min'], update=on_update)
    y_max: FloatProperty(name="Y Max", default=CURRENT_DEFAULTS['y_max'], update=on_update)
    z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['z_min'], update=on_update)
    z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['z_max'], update=on_update)
    thickness: FloatProperty(name="Line Thickness", default=CURRENT_DEFAULTS['thickness'], min=0.01, max=10.0, update=on_update)
    draw_plane: EnumProperty(name="Draw Plane", items=[('XZ', "Front (XZ)", "XZ Plane"), ('XY', "Top (XY)", "XY Plane")], default=CURRENT_DEFAULTS['draw_plane'], update=on_update)
    show_eq1: BoolProperty(name="Show Eq 1", default=CURRENT_DEFAULTS['show_eq1'], update=on_update)
    show_eq2: BoolProperty(name="Show Eq 2", default=CURRENT_DEFAULTS['show_eq2'], update=on_update)
    show_eq3: BoolProperty(name="Show Eq 3", default=CURRENT_DEFAULTS['show_eq3'], update=on_update)
    color1: FloatVectorProperty(name="Color 1", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color1'], update=on_update)
    color2: FloatVectorProperty(name="Color 2", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color2'], update=on_update)
    color3: FloatVectorProperty(name="Color 3", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color3'], update=on_update)

    # Torus Properties
    t_enable_preview: BoolProperty(name="Enable Torus Preview", default=CURRENT_DEFAULTS['t_enable_preview'], update=on_update)
    t_val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['t_val_a'], update=on_update)
    t_val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['t_val_b'], update=on_update)
    t_x_min: FloatProperty(name="X Min", default=CURRENT_DEFAULTS['t_x_min'], update=on_update)
    t_x_max: FloatProperty(name="X Max", default=CURRENT_DEFAULTS['t_x_max'], update=on_update)
    t_z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['t_z_min'], update=on_update)
    t_z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['t_z_max'], update=on_update)
    t_count: IntProperty(name="Torus Count", default=CURRENT_DEFAULTS['t_count'], min=1, max=100, update=on_update)
    t_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['t_major_radius'], min=0.1, max=100.0, update=on_update)
    t_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['t_minor_radius'], min=0.01, max=50.0, update=on_update)
    t_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['t_color'], update=on_update)

# ==============================================================================
#  OPERATORS
# ==============================================================================

class OT_ShowLinePreview(Operator):
    bl_idname = f"{OP_PREFIX}.show_line_preview"
    bl_label = "Show Line Preview"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props:
            props.enable_preview = True
            update_line_preview(context, props)
        return {'FINISHED'}

class OT_DetachLines(Operator):
    bl_idname = f"{OP_PREFIX}.detach_lines"
    bl_label = "Detach Lines"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col_preview = bpy.data.collections.get(PREVIEW_COL_LINE)
        if not col_preview or len(col_preview.objects) == 0:
            self.report({'WARNING'}, "切り離すプレビュー線が見つかりません。")
            return {'CANCELLED'}
        target_col = context.collection
        timestamp = datetime.now().strftime('%H%M%S')
        bpy.ops.object.select_all(action='DESELECT')
        for obj in list(col_preview.objects):
            if obj.hide_viewport: continue
            if obj.name not in target_col.objects: target_col.objects.link(obj)
            col_preview.objects.unlink(obj)
            obj.name = obj.name.replace("[Preview]", "Solid_EqLine") + f"_{timestamp}"
            if obj.data.materials:
                mat = obj.data.materials[0]
                new_mat = mat.copy()
                new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
                obj.data.materials[0] = new_mat
            obj.select_set(True)
        self.report({'INFO'}, "Lines Detached!")
        props = getattr(context.scene, PROPS_NAME, None)
        if props: update_line_preview(context, props)
        return {'FINISHED'}

class OT_ShowTorusPreview(Operator):
    bl_idname = f"{OP_PREFIX}.show_torus_preview"
    bl_label = "Show Torus Preview"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props:
            props.t_enable_preview = True
            update_torus_preview(context, props)
        return {'FINISHED'}

class OT_DetachTorus(Operator):
    bl_idname = f"{OP_PREFIX}.detach_torus"
    bl_label = "Detach Torus"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col_preview = bpy.data.collections.get(PREVIEW_COL_TORUS)
        if not col_preview or len(col_preview.objects) == 0:
            self.report({'WARNING'}, "切り離すトーラスが見つかりません。")
            return {'CANCELLED'}
        target_col = context.collection
        timestamp = datetime.now().strftime('%H%M%S')
        bpy.ops.object.select_all(action='DESELECT')
        for obj in list(col_preview.objects):
            if obj.name not in target_col.objects: target_col.objects.link(obj)
            col_preview.objects.unlink(obj)
            obj.name = obj.name.replace("[Preview]", "Solid_Torus") + f"_{timestamp}"
            if obj.data.materials:
                mat = obj.data.materials[0]
                new_mat = mat.copy()
                new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
                obj.data.materials[0] = new_mat
            obj.select_set(True)
        self.report({'INFO'}, "Torus Detached!")
        props = getattr(context.scene, PROPS_NAME, None)
        if props: update_torus_preview(context, props)
        return {'FINISHED'}

class OT_CopyFullScript(Operator):
    bl_idname = f"{OP_PREFIX}.copy_script"
    bl_label = "Copy Script"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        target_text = None
        for t in bpy.data.texts:
            if SOURCE_ID_TAG in t.as_string(): target_text = t; break
        if not target_text: return {'CANCELLED'}

        code = target_text.as_string()
        c1, c2, c3, tc = props.color1, props.color2, props.color3, props.t_color
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "enable_preview": {props.enable_preview},\n'
        new_dict += f'    "val_a": {props.val_a:.4f},\n'
        new_dict += f'    "val_b": {props.val_b:.4f},\n'
        new_dict += f'    "val_d": {props.val_d:.4f},\n'
        new_dict += f'    "x_min": {props.x_min:.4f}, "x_max": {props.x_max:.4f},\n'
        new_dict += f'    "y_min": {props.y_min:.4f}, "y_max": {props.y_max:.4f},\n'
        new_dict += f'    "z_min": {props.z_min:.4f}, "z_max": {props.z_max:.4f},\n'
        new_dict += f'    "thickness": {props.thickness:.4f},\n'
        new_dict += f'    "draw_plane": "{props.draw_plane}",\n'
        new_dict += f'    "show_eq1": {props.show_eq1}, "show_eq2": {props.show_eq2}, "show_eq3": {props.show_eq3},\n'
        new_dict += f'    "color1": ({c1[0]:.4f}, {c1[1]:.4f}, {c1[2]:.4f}, {c1[3]:.4f}),\n'
        new_dict += f'    "color2": ({c2[0]:.4f}, {c2[1]:.4f}, {c2[2]:.4f}, {c2[3]:.4f}),\n'
        new_dict += f'    "color3": ({c3[0]:.4f}, {c3[1]:.4f}, {c3[2]:.4f}, {c3[3]:.4f}),\n'
        
        new_dict += f'    "t_enable_preview": {props.t_enable_preview},\n'
        new_dict += f'    "t_val_a": {props.t_val_a:.4f}, "t_val_b": {props.t_val_b:.4f},\n'
        new_dict += f'    "t_x_min": {props.t_x_min:.4f}, "t_x_max": {props.t_x_max:.4f},\n'
        new_dict += f'    "t_z_min": {props.t_z_min:.4f}, "t_z_max": {props.t_z_max:.4f},\n'
        new_dict += f'    "t_count": {props.t_count},\n'
        new_dict += f'    "t_major_radius": {props.t_major_radius:.4f}, "t_minor_radius": {props.t_minor_radius:.4f},\n'
        new_dict += f'    "t_color": ({tc[0]:.4f}, {tc[1]:.4f}, {tc[2]:.4f}, {tc[3]:.4f}),\n'
        new_dict += "}\n"

        try:
            tag_start = "# <BEGIN" + "_DICT>"
            tag_end = "# <END" + "_DICT>"
            pre_code, rest = code.split(tag_start, 1)
            _, post_code = rest.split(tag_end, 1)
            final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
            context.window_manager.clipboard = final_code
            self.report({'INFO'}, "Code copied!")
        except: return {'CANCELLED'}
        return {'FINISHED'}

class OT_OpenUrl(Operator):
    bl_idname = f"{OP_PREFIX}.open_url"
    bl_label = "Open URL"
    url: StringProperty()
    def execute(self, context): 
        webbrowser.open(self.url)
        return {'FINISHED'}

class OT_RemoveAddon(Operator):
    bl_idname = f"{OP_PREFIX}.remove_addon"
    bl_label = "Close Addon"
    def execute(self, context):
        bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
        self.report({'INFO'}, "アドオンを終了しました。")
        return {'FINISHED'}

# ==============================================================================
#  PANELS
# ==============================================================================

class PT_MainPanel(Panel):
    bl_label = PANEL_TITLE
    bl_idname = f"{PREFIX}_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        # --- Line Preview Button ---
        row_prev = layout.row()
        row_prev.scale_y = 1.5
        if not props.enable_preview:
            row_prev.operator(OT_ShowLinePreview.bl_idname, icon='PLAY', text="Show Line Preview (表示開始)")
        else:
            row_prev.prop(props, "enable_preview", text="Line Preview Active (ON/OFF)", toggle=True, icon='PAUSE')

        box_info = layout.box()
        box_info.label(text="【 Line Info 】", icon='INFO')
        a_str = f"{props.val_a:.2f}" if abs(props.val_a) > 0.0001 else "0.00(Err)"
        box_info.label(text=f"y = ({props.val_b:.2f} / {a_str}) x")
        
        box_values = layout.box()
        col_v = box_values.column(align=True)
        col_v.prop(props, "val_a")
        col_v.prop(props, "val_b")
        col_v.prop(props, "val_d")
        
        box_limits = layout.box()
        box_limits.label(text="Limits (X, Y, Z)", icon='MOD_HULL')
        for axis in ['x', 'y', 'z']:
            r = box_limits.row(align=True)
            r.prop(props, f"{axis}_min", text=f"{axis.upper()} Min")
            r.prop(props, f"{axis}_max", text="Max")

class PT_VisibilityPanel(Panel):
    bl_label = "Line Design & Visibility"
    bl_idname = f"{PREFIX}_PT_visibility"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_parent_id = f"{PREFIX}_PT_main"

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return
        
        layout.prop(props, "thickness")
        layout.prop(props, "draw_plane")
        layout.separator()
        for i in range(1, 4):
            r = layout.row(align=True)
            r.prop(props, f"show_eq{i}", text=f"Eq {i}", toggle=True)
            r.prop(props, f"color{i}", text="")

class PT_CreatePanel(Panel):
    bl_label = "Line Detach"
    bl_idname = f"{PREFIX}_PT_create"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_parent_id = f"{PREFIX}_PT_main"

    def draw(self, context):
        col_exec = self.layout.column()
        col_exec.scale_y = 2.0 
        col_exec.operator(OT_DetachLines.bl_idname, icon='MESH_CYLINDER', text="Detach Lines (位置を固定して切り離し)")

class PT_TorusPanel(Panel):
    bl_label = "Torus Generator (独立計算)"
    bl_idname = f"{PREFIX}_PT_torus"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        row_prev = layout.row()
        row_prev.scale_y = 1.5
        if not props.t_enable_preview:
            row_prev.operator(OT_ShowTorusPreview.bl_idname, icon='PLAY', text="Show Torus Preview (表示開始)")
        else:
            row_prev.prop(props, "t_enable_preview", text="Torus Preview Active (ON/OFF)", toggle=True, icon='PAUSE')

        box_eq = layout.box()
        box_eq.label(text="Center Line: y = (b/a)x", icon='NORMALS_FACE')
        col = box_eq.column(align=True)
        col.prop(props, "t_val_a")
        col.prop(props, "t_val_b")

        box_r = layout.box()
        box_r.label(text="Placement Range", icon='ARROW_LEFTRIGHT')
        r_z = box_r.row(align=True)
        r_z.prop(props, "t_z_min", text="Z Min")
        r_z.prop(props, "t_z_max", text="Z Max")
        r_x = box_r.row(align=True)
        r_x.prop(props, "t_x_min", text="X Min")
        r_x.prop(props, "t_x_max", text="X Max")
        box_r.prop(props, "t_count")

        box_s = layout.box()
        box_s.label(text="Torus Shape", icon='MESH_TORUS')
        box_s.prop(props, "t_major_radius")
        box_s.prop(props, "t_minor_radius")
        box_s.prop(props, "t_color")

        col_exec = layout.column()
        col_exec.scale_y = 1.5
        col_exec.operator(OT_DetachTorus.bl_idname, icon='MESH_TORUS', text="Detach Torus (固定化)")

class PT_SystemPanel(Panel):
    bl_label = "System (Copy / Close)"
    bl_idname = f"{PREFIX}_PT_system"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        r_c = layout.row()
        r_c.scale_y = 1.2
        r_c.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
        
        r_r = layout.row()
        r_r.scale_y = 1.2
        r_r.alert = True 
        r_r.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Close Addon (完全終了)")

class PT_LinksPanel(Panel):
    bl_label = "Links"
    bl_idname = f"{PREFIX}_PT_links"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

    def draw(self, context):
        for l in ADDON_LINKS:
            self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"], icon='URL').url = l["url"]

# ==============================================================================
#  REGISTER
# ==============================================================================

classes = (
    PG_EquationProps, 
    OT_ShowLinePreview, OT_DetachLines, 
    OT_ShowTorusPreview, OT_DetachTorus,
    OT_CopyFullScript, OT_OpenUrl, OT_RemoveAddon, 
    PT_MainPanel, PT_VisibilityPanel, PT_CreatePanel, 
    PT_TorusPanel, PT_SystemPanel, PT_LinksPanel
)

def auto_open_sidebar():
    try:
        for window in bpy.context.window_manager.windows:
            for area in window.screen.areas:
                if area.type == 'VIEW_3D':
                    for space in area.spaces:
                        if space.type == 'VIEW_3D':
                            if not space.show_region_ui:
                                space.show_region_ui = True
    except: pass
    return None

def register():
    for c in classes: 
        try: bpy.utils.register_class(c)
        except ValueError: pass
            
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_EquationProps))
    bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)

def unregister():
    global _timer
    if _timer is not None:
        try: bpy.app.timers.unregister(_timer)
        except Exception: pass
        _timer = None

    cleanup_preview_data()

    if hasattr(bpy.types.Scene, PROPS_NAME): 
        delattr(bpy.types.Scene, PROPS_NAME)
        
    for c in reversed(classes): 
        try: bpy.utils.unregister_class(c)
        except ValueError: pass

if __name__ == "__main__": 
    register()
# Copied: 2026-04-08 12:00:00
import bpy
import time
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime

# ==============================================================================
#  【 基本設定エリア 】
# ==============================================================================

PREFIX       = "EqLines"
ADDON_NAME   = "[ Equation Lines ]"
TAB_NAME     = "[ Equation Gen ]"
PANEL_TITLE  = "Equation Lines"
AUTHOR       = "zionadchat"

# ★ このスクリプト自身のID(絶対に消さないこと)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: EQ_LINES_2026_04_08_V4 ###"

# ==============================================================================
#  システム初期化 & ID管理
# ==============================================================================

bl_info = {
    "name": f"{ADDON_NAME} {PREFIX}",
    "author": AUTHOR,
    "version": (4, 3, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": "Equation Lines Generator - Preview button, detach, split panels",
    "category": "3D View",
}

OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"

ADDON_LINKS = (
    {"label": "Prefix 20260408", "url": "<https://www.notion.so/Prefix-20260408-33cf5dacaf43807e9e35ff8cdbbc39c6>"},
)

# ==============================================================================
#  デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "enable_preview": False,
    "val_a": 0.6000,
    "val_b": 1.0000,
    "val_d": 10.0000,
    "x_min": -50.0,
    "x_max": 50.0,
    "y_min": -50.0,
    "y_max": 50.0,
    "z_min": -50.0,
    "z_max": 50.0,
    "thickness": 0.5000,
    "draw_plane": "XZ",
    "show_eq1": True,
    "show_eq2": True,
    "show_eq3": True,
    "color1": (1.0000, 0.2000, 0.2000, 1.0000),
    "color2": (0.2000, 1.0000, 0.2000, 1.0000),
    "color3": (0.2000, 0.2000, 1.0000, 1.0000),
}
# <END_DICT>

PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"

# ==============================================================================
#  マテリアル・データ管理 ロジック
# ==============================================================================

def cleanup_preview_data():
    col = bpy.data.collections.get(PREVIEW_COL_NAME)
    if col:
        for obj in list(col.objects):
            data = obj.data
            bpy.data.objects.remove(obj, do_unlink=True)
            if data and data.users == 0:
                if isinstance(data, bpy.types.Curve):
                    bpy.data.curves.remove(data)
        
        if len(col.objects) == 0:
            bpy.data.collections.remove(col)

def apply_material_settings(mat, color):
    mat.use_nodes = True
    mat.blend_method = 'BLEND'
    mat.diffuse_color = color
    
    tree = mat.node_tree
    bsdf = tree.nodes.get("Principled BSDF")
    if not bsdf:
        tree.nodes.clear()
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        bsdf.name = "Principled BSDF"
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        tree.links.new(bsdf.outputs[0], out.inputs[0])
        
    if "Base Color" in bsdf.inputs:
        bsdf.inputs["Base Color"].default_value = color
    if "Alpha" in bsdf.inputs:
        bsdf.inputs["Alpha"].default_value = color[3]

def get_preview_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name=name)
    apply_material_settings(mat, color)
    return mat

def get_clipped_segment(m, c, x_min, x_max, v_min, v_max):
    if m == 0:
        if v_min <= c <= v_max:
            return x_min, x_max
        else:
            return None, None
    else:
        x_from_v1 = (v_min - c) / m
        x_from_v2 = (v_max - c) / m
        
        valid_x_min = min(x_from_v1, x_from_v2)
        valid_x_max = max(x_from_v1, x_from_v2)
        
        act_x_min = max(x_min, valid_x_min)
        act_x_max = min(x_max, valid_x_max)
        
        if act_x_min > act_x_max: 
            return None, None
        return act_x_min, act_x_max

def calc_points(props, m, c):
    x_min, x_max = min(props.x_min, props.x_max), max(props.x_min, props.x_max)
    y_min, y_max = min(props.y_min, props.y_max), max(props.y_min, props.y_max)
    z_min, z_max = min(props.z_min, props.z_max), max(props.z_min, props.z_max)
    
    if props.draw_plane == 'XZ':
        act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, z_min, z_max)
        if act_x_min is None: return None, None
        v1 = m * act_x_min + c
        v2 = m * act_x_max + c
        return (act_x_min, 0.0, v1), (act_x_max, 0.0, v2)
    else:
        act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, y_min, y_max)
        if act_x_min is None: return None, None
        v1 = m * act_x_min + c
        v2 = m * act_x_max + c
        return (act_x_min, v1, 0.0), (act_x_max, v2, 0.0)

def update_preview_geometry(context):
    props = getattr(context.scene, PROPS_NAME, None)
    if not props: return

    # ★ プレビューが無効な場合はデータを削除して終了
    if not props.enable_preview:
        cleanup_preview_data()
        return

    col = bpy.data.collections.get(PREVIEW_COL_NAME)
    if not col:
        col = bpy.data.collections.new(PREVIEW_COL_NAME)
    if col.name not in context.scene.collection.children:
        context.scene.collection.children.link(col)

    a = props.val_a if abs(props.val_a) > 0.0001 else (0.0001 if props.val_a >= 0 else -0.0001)
    b = props.val_b
    d = props.val_d
    m = b / a

    equations = [
        {"id": 1, "show": props.show_eq1, "color": props.color1, "offset": 0.0},
        {"id": 2, "show": props.show_eq2, "color": props.color2, "offset": -d},
        {"id": 3, "show": props.show_eq3, "color": props.color3, "offset": d},
    ]

    for eq in equations:
        obj_name = f"[Preview] EqLine_{eq['id']}"
        obj = bpy.data.objects.get(obj_name)

        if not eq["show"]:
            if obj:
                obj.hide_viewport = True
                obj.hide_render = True
            continue

        p1, p2 = calc_points(props, m, eq["offset"])
        
        if p1 is None:
            if obj:
                obj.hide_viewport = True
                obj.hide_render = True
            continue

        if not obj:
            curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
            curve.dimensions = '3D'
            curve.fill_mode = 'FULL'
            
            spline = curve.splines.new('POLY')
            spline.points.add(1)
            
            obj = bpy.data.objects.new(obj_name, curve)
            col.objects.link(obj)
        else:
            curve = obj.data
            spline = curve.splines[0]

        curve.bevel_depth = props.thickness
        curve.bevel_resolution = 6
        
        spline.points[0].co = (*p1, 1.0)
        spline.points[1].co = (*p2, 1.0)
        
        obj.hide_viewport = False
        obj.hide_render = False

        mat = get_preview_material(f"Preview_Mat_EqLine{eq['id']}", eq["color"])
        if not obj.data.materials:
            obj.data.materials.append(mat)
        else:
            obj.data.materials[0] = mat

_timer = None
_last_update_time = 0

def delayed_update():
    global _timer, _last_update_time
    _timer = None
    now = time.time()
    if now - _last_update_time < 0.05:
        if _timer is None:
            _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
        return None
    _last_update_time = now
    
    ctx = bpy.context
    if not ctx or not ctx.scene: return None
    if ctx.object and ctx.object.mode != 'OBJECT': return None

    update_preview_geometry(ctx)
    return None

def on_update(self, context):
    global _timer
    if _timer is None: 
        _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)

# ==============================================================================
#  PROPERTIES
# ==============================================================================

class PG_EquationProps(PropertyGroup):
    enable_preview: BoolProperty(name="Enable Preview", default=CURRENT_DEFAULTS['enable_preview'], update=on_update)

    val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['val_a'], update=on_update)
    val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['val_b'], update=on_update)
    val_d: FloatProperty(name="Value d", default=CURRENT_DEFAULTS['val_d'], update=on_update)
    
    x_min: FloatProperty(name="X Min", default=CURRENT_DEFAULTS['x_min'], update=on_update)
    x_max: FloatProperty(name="X Max", default=CURRENT_DEFAULTS['x_max'], update=on_update)
    y_min: FloatProperty(name="Y Min", default=CURRENT_DEFAULTS['y_min'], update=on_update)
    y_max: FloatProperty(name="Y Max", default=CURRENT_DEFAULTS['y_max'], update=on_update)
    z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['z_min'], update=on_update)
    z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['z_max'], update=on_update)
    
    thickness: FloatProperty(name="Cylinder Thickness", default=CURRENT_DEFAULTS['thickness'], min=0.01, max=10.0, update=on_update)
    
    draw_plane: EnumProperty(
        name="Draw Plane",
        items=[('XZ', "Front (XZ)", "Draw on XZ Plane"), ('XY', "Top (XY)", "Draw on XY Plane")],
        default=CURRENT_DEFAULTS['draw_plane'], update=on_update
    )

    show_eq1: BoolProperty(name="Show Eq 1", default=CURRENT_DEFAULTS['show_eq1'], update=on_update)
    show_eq2: BoolProperty(name="Show Eq 2", default=CURRENT_DEFAULTS['show_eq2'], update=on_update)
    show_eq3: BoolProperty(name="Show Eq 3", default=CURRENT_DEFAULTS['show_eq3'], update=on_update)
    
    color1: FloatVectorProperty(name="Color 1", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color1'], update=on_update)
    color2: FloatVectorProperty(name="Color 2", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color2'], update=on_update)
    color3: FloatVectorProperty(name="Color 3", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color3'], update=on_update)

# ==============================================================================
#  OPERATORS
# ==============================================================================

class OT_ShowPreview(Operator):
    bl_idname = f"{OP_PREFIX}.show_preview"
    bl_label = "Show Preview Lines (最初の一括表示)"
    bl_description = "プレビューを有効化し、画面にラインを一括表示します"
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props:
            props.enable_preview = True
            update_preview_geometry(context)
        return {'FINISHED'}

class OT_DetachLines(Operator):
    bl_idname = f"{OP_PREFIX}.detach_lines"
    bl_label = "Detach Lines"
    bl_description = "現在のプレビュー線を通常オブジェクトに変換し、パラメータ追従から切り離します"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        col_preview = bpy.data.collections.get(PREVIEW_COL_NAME)
        if not col_preview or len(col_preview.objects) == 0:
            self.report({'WARNING'}, "切り離すプレビュー線が見つかりません。")
            return {'CANCELLED'}
        
        target_col = context.collection
        timestamp = datetime.now().strftime('%H%M%S')
        
        bpy.ops.object.select_all(action='DESELECT')
        created_count = 0
        
        for obj in list(col_preview.objects):
            if obj.hide_viewport:
                continue

            if obj.name not in target_col.objects:
                target_col.objects.link(obj)
            
            col_preview.objects.unlink(obj)
            obj.name = obj.name.replace("[Preview]", "Solid_EqLine") + f"_{timestamp}"
            
            if obj.data.materials:
                mat = obj.data.materials[0]
                new_mat = mat.copy()
                new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
                obj.data.materials[0] = new_mat
            
            obj.select_set(True)
            context.view_layer.objects.active = obj
            created_count += 1
            
        if created_count > 0:
            self.report({'INFO'}, f"{created_count}個のラインを切り離しました!(位置が固定されます)")
        else:
            self.report({'WARNING'}, "切り離せるラインがありませんでした。")
        
        update_preview_geometry(context)
        return {'FINISHED'}

class OT_CopyFullScript(Operator):
    bl_idname = f"{OP_PREFIX}.copy_script"
    bl_label = "Copy Script"
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        target_text = None
        for t in bpy.data.texts:
            if SOURCE_ID_TAG in t.as_string(): target_text = t; break
        
        if not target_text:
            self.report({'WARNING'}, "Source script not found in Text Editor.")
            return {'CANCELLED'}

        code = target_text.as_string()
        c1, c2, c3 = props.color1, props.color2, props.color3
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "enable_preview": {props.enable_preview},\n'
        new_dict += f'    "val_a": {props.val_a:.4f},\n'
        new_dict += f'    "val_b": {props.val_b:.4f},\n'
        new_dict += f'    "val_d": {props.val_d:.4f},\n'
        new_dict += f'    "x_min": {props.x_min:.4f},\n'
        new_dict += f'    "x_max": {props.x_max:.4f},\n'
        new_dict += f'    "y_min": {props.y_min:.4f},\n'
        new_dict += f'    "y_max": {props.y_max:.4f},\n'
        new_dict += f'    "z_min": {props.z_min:.4f},\n'
        new_dict += f'    "z_max": {props.z_max:.4f},\n'
        new_dict += f'    "thickness": {props.thickness:.4f},\n'
        new_dict += f'    "draw_plane": "{props.draw_plane}",\n'
        new_dict += f'    "show_eq1": {props.show_eq1},\n'
        new_dict += f'    "show_eq2": {props.show_eq2},\n'
        new_dict += f'    "show_eq3": {props.show_eq3},\n'
        new_dict += f'    "color1": ({c1[0]:.4f}, {c1[1]:.4f}, {c1[2]:.4f}, {c1[3]:.4f}),\n'
        new_dict += f'    "color2": ({c2[0]:.4f}, {c2[1]:.4f}, {c2[2]:.4f}, {c2[3]:.4f}),\n'
        new_dict += f'    "color3": ({c3[0]:.4f}, {c3[1]:.4f}, {c3[2]:.4f}, {c3[3]:.4f}),\n'
        new_dict += "}\n"

        try:
            tag_start = "# <BEGIN" + "_DICT>"
            tag_end = "# <END" + "_DICT>"
            if tag_start not in code or tag_end not in code: return {'CANCELLED'}
            
            pre_code, rest = code.split(tag_start, 1)
            _, post_code = rest.split(tag_end, 1)
            final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
            
            lines = final_code.split("\n")
            if len(lines) > 0 and lines[0].startswith("# Copied:"):
                lines[0] = f"# Copied: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
            
            context.window_manager.clipboard = "\n".join(lines)
            self.report({'INFO'}, "Code copied!")
        except Exception: 
            return {'CANCELLED'}
        return {'FINISHED'}

class OT_OpenUrl(Operator):
    bl_idname = f"{OP_PREFIX}.open_url"
    bl_label = "Open URL"
    url: StringProperty()
    def execute(self, context): 
        webbrowser.open(self.url)
        return {'FINISHED'}

class OT_RemoveAddon(Operator):
    bl_idname = f"{OP_PREFIX}.remove_addon"
    bl_label = "Close Addon"
    def execute(self, context):
        bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
        self.report({'INFO'}, "アドオンを終了し、プレビューを削除しました。")
        return {'FINISHED'}

# ==============================================================================
#  PANELS
# ==============================================================================

class PT_MainPanel(Panel):
    bl_label = PANEL_TITLE
    bl_idname = f"{PREFIX}_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        # --- Show/Hide Preview Button ---
        row_prev = layout.row()
        row_prev.scale_y = 1.5
        if not props.enable_preview:
            row_prev.operator(OT_ShowPreview.bl_idname, icon='PLAY', text="Show Preview Lines (表示開始)")
        else:
            row_prev.prop(props, "enable_preview", text="Preview Active (ON - クリックで消去)", toggle=True, icon='PAUSE')

        layout.separator()

        # --- Equations Info ---
        box_info = layout.box()
        box_info.label(text="【 Equations Info 】", icon='INFO')
        a_str = f"{props.val_a:.2f}" if abs(props.val_a) > 0.0001 else "0.00(Err)"
        b_str = f"{props.val_b:.2f}"
        d_str = f"{props.val_d:.2f}"
        box_info.label(text=f"y = ({b_str} / {a_str}) x")
        box_info.label(text=f"y = ({b_str} / {a_str}) x - {d_str}")
        box_info.label(text=f"y = ({b_str} / {a_str}) x + {d_str}")

        layout.separator()

        # --- Parameters ---
        box_values = layout.box()
        box_values.label(text="Parameters", icon='DRIVER')
        col_v = box_values.column(align=True)
        col_v.prop(props, "val_a")
        col_v.prop(props, "val_b")
        col_v.prop(props, "val_d")
        
        layout.separator()

        # --- Limits ---
        box_limits = layout.box()
        box_limits.label(text="Limits (X, Y, Z)", icon='MOD_HULL')
        
        row_x = box_limits.row(align=True)
        row_x.prop(props, "x_min", text="X Min")
        row_x.prop(props, "x_max", text="Max")
        
        row_y = box_limits.row(align=True)
        row_y.prop(props, "y_min", text="Y Min")
        row_y.prop(props, "y_max", text="Max")
        
        row_z = box_limits.row(align=True)
        row_z.prop(props, "z_min", text="Z Min")
        row_z.prop(props, "z_max", text="Max")

class PT_VisibilityPanel(Panel):
    bl_label = "Design & Visibility"
    bl_idname = f"{PREFIX}_PT_visibility"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_parent_id = f"{PREFIX}_PT_main"

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return
        
        layout.prop(props, "thickness")
        layout.prop(props, "draw_plane")
        layout.separator()
        
        # --- 個別表示非表示・カラー ---
        r1 = layout.row(align=True)
        r1.prop(props, "show_eq1", text="Eq 1", toggle=True)
        r1.prop(props, "color1", text="")

        r2 = layout.row(align=True)
        r2.prop(props, "show_eq2", text="Eq 2", toggle=True)
        r2.prop(props, "color2", text="")

        r3 = layout.row(align=True)
        r3.prop(props, "show_eq3", text="Eq 3", toggle=True)
        r3.prop(props, "color3", text="")

class PT_CreatePanel(Panel):
    bl_label = "Create Objects"
    bl_idname = f"{PREFIX}_PT_create"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_parent_id = f"{PREFIX}_PT_main"

    def draw(self, context):
        layout = self.layout
        
        # --- オブジェクト切り離しボタン ---
        col_exec = layout.column()
        col_exec.scale_y = 2.0 
        col_exec.operator(OT_DetachLines.bl_idname, icon='MESH_CYLINDER', text="Detach Lines (位置を固定して切り離し)")

class PT_SystemPanel(Panel):
    bl_label = "System (Copy / Close)"
    bl_idname = f"{PREFIX}_PT_system"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_parent_id = f"{PREFIX}_PT_main"

    def draw(self, context):
        layout = self.layout
        
        # --- コピー機能 ---
        row_copy = layout.row()
        row_copy.scale_y = 1.2
        row_copy.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
        
        # --- アドオン終了 ---
        row_rem = layout.row()
        row_rem.scale_y = 1.2
        row_rem.alert = True 
        row_rem.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Close Addon (アドオン完全終了)")

class PT_LinksPanel(Panel):
    bl_label = "Links"
    bl_idname = f"{PREFIX}_PT_links"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_parent_id = f"{PREFIX}_PT_main"

    def draw(self, context):
        layout = self.layout
        
        # --- リンク パネル ---
        for l in ADDON_LINKS:
            layout.operator(OT_OpenUrl.bl_idname, text=l["label"], icon='URL').url = l["url"]

# ==============================================================================
#  REGISTER
# ==============================================================================

classes = (
    PG_EquationProps, 
    OT_ShowPreview,
    OT_DetachLines, 
    OT_CopyFullScript, 
    OT_OpenUrl, 
    OT_RemoveAddon, 
    PT_MainPanel, 
    PT_VisibilityPanel, 
    PT_CreatePanel, 
    PT_SystemPanel,
    PT_LinksPanel
)

def auto_open_sidebar():
    try:
        for window in bpy.context.window_manager.windows:
            for area in window.screen.areas:
                if area.type == 'VIEW_3D':
                    for space in area.spaces:
                        if space.type == 'VIEW_3D':
                            if not space.show_region_ui:
                                space.show_region_ui = True
    except: pass
    return None

def register():
    for c in classes: 
        try:
            bpy.utils.register_class(c)
        except ValueError:
            pass
            
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_EquationProps))
    bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)

def unregister():
    global _timer
    if _timer is not None:
        try: 
            bpy.app.timers.unregister(_timer)
        except Exception: 
            pass
        _timer = None

    cleanup_preview_data()

    if hasattr(bpy.types.Scene, PROPS_NAME): 
        delattr(bpy.types.Scene, PROPS_NAME)
        
    for c in reversed(classes): 
        try:
            bpy.utils.unregister_class(c)
        except ValueError:
            pass

if __name__ == "__main__": 
    register()