blender Million 2026

prefix 一次方程式 トーラス20260408











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