blender Million 2026







# Copied: 2026-03-24 21:03:00
import bpy
import bmesh
import webbrowser
import math
import mathutils
import time
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime

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

PREFIX       = "ShapeTorus20260324"
ADDON_NAME   = "zionad 520[ Shape-Torus ]"
TAB_NAME     = "[ Shape Torus copy ]   "
PANEL_TITLE  = "Multi-Shape Generator"
AUTHOR       = "zionadchat"

# ★ このスクリプト自身のID
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SHAPE_TORUS_2026_03_24_V9_MULTI_SHAPE ###"

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

bl_info = {
    "name": f"{ADDON_NAME} {PREFIX}",
    "author": AUTHOR,
    "version": (9, 10, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": f"Multi-Shape Torus & Equation Cylinders - {PREFIX}",
    "category": "3D View",
}

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

# ★ リンク設定
ADDON_LINKS = (
    {"label": "時空図 交点 2060407", "url": "<https://www.notion.so/2060407-33af5dacaf43808d86bbf0d54d4d0dd5>"},
    {"label": "Prefix トーラス正方形 20260324", "url": "<https://www.notion.so/Prefix-20260324-32df5dacaf4380528980db6a989d6306>"},
)

# ==============================================================================
#  デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_preview": True,
    "show_guide": True,
    "torus_color": (0.0391, 0.8000, 0.1647, 1.0000),
    "torus_loc": (0.0000, 0.0000, 0.0000),
    "torus_rot": (0.0000, 0.0000, 0.0000),
    "base_shape": "SQUARE",
    "torus_plane": "XY",
    "size_x": 10.0000,
    "size_y": 5.0000,
    "corner_radius": 0.0000,
    "minor_radius": 0.5000,
    "major_segments": 32,
    "corner_segments": 8,
    "minor_segments": 16,
    "cyl_thickness": 0.5000,
    "cyl_color": (0.1000, 0.6000, 0.9000, 1.0000),
    "zx_color": (0.9000, 0.2000, 0.2000, 1.0000),
    "minus_zx_color": (0.9000, 0.8000, 0.2000, 1.0000),
    "custom_zx_color": (0.2000, 0.8000, 0.9000, 1.0000),
    "cyl_slope_a": 0.6000,
    "cyl_offset": 10.0000,
    "cyl_limit": 50.0000,
    "cyl_custom_b": 1.6000,
    "show_cyl_group1": True,
    "show_cyl_group2": True,
    "show_cyl_group3": True,
    "show_cyl_group4": True,
}
# <END_DICT>

# ==============================================================================
#  データ クリーンアップ管理
# ==============================================================================

PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_OBJ_NAME = f"[Preview] Shape_{PREFIX}"
PREVIEW_GUIDE_NAME = f"[Preview] Guide_{PREFIX}"
PREVIEW_MAT_NAME = f"PreviewMat_{PREFIX}"

def cleanup_preview_data():
    for name in[PREVIEW_OBJ_NAME, PREVIEW_GUIDE_NAME]:
        obj = bpy.data.objects.get(name)
        if obj:
            mesh = obj.data
            bpy.data.objects.remove(obj, do_unlink=True)
            if mesh and mesh.users == 0: bpy.data.meshes.remove(mesh)
                
    meshes_to_remove =[m for m in bpy.data.meshes if m.name.startswith(f"PreviewMesh_{PREFIX}")]
    for m in meshes_to_remove:
        if m.users == 0: bpy.data.meshes.remove(m)
    mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
    if mat and mat.users == 0: bpy.data.materials.remove(mat)
    col = bpy.data.collections.get(PREVIEW_COL_NAME)
    if col and len(col.objects) == 0: bpy.data.collections.remove(col)

def cleanup_old_materials(prefix="Mat_UniqueShape", limit=50):
    mats =[m for m in bpy.data.materials if m.name.startswith(prefix)]
    if len(mats) > limit:
        for m in mats[:-limit]:
            if m.users == 0: bpy.data.materials.remove(m)

# ==============================================================================
#  ジオメトリ エンジン
# ==============================================================================

def create_square_guide_bmesh(bm, size):
    S = size / 2.0
    v1 = bm.verts.new((S, S, 0)); v2 = bm.verts.new((-S, S, 0))
    v3 = bm.verts.new((-S, -S, 0)); v4 = bm.verts.new((S, -S, 0))
    bm.verts.ensure_lookup_table()
    bm.edges.new((v1, v2)); bm.edges.new((v2, v3)); bm.edges.new((v3, v4)); bm.edges.new((v4, v1))
    return bm

def create_cube_guide_bmesh(bm, size):
    geom = bmesh.ops.create_cube(bm, size=size)
    faces =[f for f in bm.faces]
    bmesh.ops.delete(bm, geom=faces, context='FACES_ONLY')
    return bm

def create_ellipse_guide_bmesh(bm, size_x, size_y, segments=64):
    a = size_x / 2.0; b = size_y / 2.0
    verts =[]
    for i in range(segments):
        t = i * 2.0 * math.pi / segments
        verts.append(bm.verts.new((a * math.cos(t), b * math.sin(t), 0)))
    bm.verts.ensure_lookup_table()
    for i in range(segments): bm.edges.new((verts[i], verts[(i + 1) % segments]))
    return bm

def create_ellipse_torus_bmesh(bm, size_x, size_y, minor_radius, major_segments, minor_segments):
    a = size_x / 2.0; b = size_y / 2.0
    rings =[]
    for i in range(major_segments):
        t = i * 2.0 * math.pi / major_segments
        p = mathutils.Vector((a * math.cos(t), b * math.sin(t), 0))
        n = mathutils.Vector((b * math.cos(t), a * math.sin(t), 0)).normalized()
        up = mathutils.Vector((0, 0, 1))
        ring =[]
        for j in range(minor_segments):
            theta = j * 2.0 * math.pi / minor_segments
            offset = n * (minor_radius * math.cos(theta)) + up * (minor_radius * math.sin(theta))
            ring.append(bm.verts.new(p + offset))
        rings.append(ring)
    bm.verts.ensure_lookup_table()
    edge_loops = []
    for ring in rings:
        edges =[]
        for j in range(minor_segments): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
        edge_loops.append(edges)
    bm.edges.ensure_lookup_table()
    for i in range(major_segments):
        try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % major_segments])
        except Exception: pass
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm

def create_square_torus_bmesh(bm, size, corner_radius, minor_radius, corner_segments, minor_segments):
    half_size = size / 2.0
    actual_corner_radius = min(max(corner_radius, 0.0), half_size)
    rings =[]; EPS = 1e-6
    if actual_corner_radius < EPS:
        L = half_size
        corners =[
            (mathutils.Vector((L, L, 0)), mathutils.Vector((1, 1, 0)).normalized()),
            (mathutils.Vector((-L, L, 0)), mathutils.Vector((-1, 1, 0)).normalized()),
            (mathutils.Vector((-L, -L, 0)), mathutils.Vector((-1, -1, 0)).normalized()),
            (mathutils.Vector((L, -L, 0)), mathutils.Vector((1, -1, 0)).normalized())
        ]
        scale_xy = 1.0 / math.cos(math.pi / 4)
        for p, n in corners:
            b = mathutils.Vector((0, 0, 1))
            ring =[]
            for j in range(minor_segments):
                theta = j * 2.0 * math.pi / minor_segments
                offset = n * (minor_radius * math.cos(theta) * scale_xy) + b * (minor_radius * math.sin(theta))
                ring.append(bm.verts.new(p + offset))
            rings.append(ring)
    else:
        L = half_size - actual_corner_radius
        pts = []
        for q in range(4):
            cx = L if q in[0, 3] else -L
            cy = L if q in [0, 1] else -L
            for i in range(corner_segments + 1):
                angle = q * (math.pi / 2) + i * (math.pi / 2) / corner_segments
                pts.append((mathutils.Vector((cx + actual_corner_radius * math.cos(angle), cy + actual_corner_radius * math.sin(angle), 0)), mathutils.Vector((math.cos(angle), math.sin(angle), 0))))
        unique_pts =[]
        for p, n in pts:
            if not unique_pts or (unique_pts[-1][0] - p).length > EPS: unique_pts.append((p, n))
        if len(unique_pts) > 1 and (unique_pts[-1][0] - unique_pts[0][0]).length < EPS: unique_pts.pop()
        for p, n in unique_pts:
            b = mathutils.Vector((0, 0, 1))
            ring =[]
            for j in range(minor_segments):
                theta = j * 2.0 * math.pi / minor_segments
                ring.append(bm.verts.new(p + n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))))
            rings.append(ring)
    bm.verts.ensure_lookup_table()
    total_rings = len(rings)
    if total_rings < 3: return bm
    edge_loops = []
    for ring in rings:
        edges =[]
        for j in range(minor_segments): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
        edge_loops.append(edges)
    bm.edges.ensure_lookup_table()
    for i in range(total_rings):
        try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % total_rings])
        except Exception: pass
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm

def create_cube_framework_bmesh(bm, size, minor_radius, minor_segments):
    L = size / 2.0
    verts_co =[
        mathutils.Vector(( L,  L,  L)), mathutils.Vector((-L,  L,  L)),
        mathutils.Vector((-L, -L,  L)), mathutils.Vector(( L, -L,  L)),
        mathutils.Vector(( L,  L, -L)), mathutils.Vector((-L,  L, -L)),
        mathutils.Vector((-L, -L, -L)), mathutils.Vector(( L, -L, -L)),
    ]
    edges_idx =[(0,1), (1,2), (2,3), (3,0), (4,5), (5,6), (6,7), (7,4), (0,4), (1,5), (2,6), (3,7)]
    for co in verts_co:
        geom = bmesh.ops.create_uvsphere(bm, u_segments=minor_segments, v_segments=max(minor_segments//2, 3), radius=minor_radius)
        bmesh.ops.translate(bm, verts=geom['verts'], vec=co)
    for idx1, idx2 in edges_idx:
        v1 = verts_co[idx1]; v2 = verts_co[idx2]
        dist = (v1 - v2).length
        geom = bmesh.ops.create_cone(bm, cap_ends=False, cap_tris=False, segments=minor_segments, radius1=minor_radius, radius2=minor_radius, depth=dist)
        axis = (v1 - v2).normalized()
        bmesh.ops.transform(bm, matrix=mathutils.Vector((0,0,1)).rotation_difference(axis).to_matrix().to_4x4(), verts=geom['verts'])
        bmesh.ops.translate(bm, verts=geom['verts'], vec=(v1 + v2) / 2.0)
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm

def apply_auto_smooth(mesh):
    if bpy.app.version < (4, 1, 0):
        try:
            if hasattr(mesh, "use_auto_smooth"):
                mesh.use_auto_smooth = True
                mesh.auto_smooth_angle = math.radians(30)
        except AttributeError: pass

# ==============================================================================
#  計算ロジック(直線の方程式をボックス制限内でクリッピング)
# ==============================================================================
def get_line_segment_in_bounds(M, C, limit):
    pts = []
    eps = 1e-4
    z1 = M * (-limit) + C
    if -limit - eps <= z1 <= limit + eps: pts.append((-limit, z1))
    z2 = M * limit + C
    if -limit - eps <= z2 <= limit + eps: pts.append((limit, z2))
    if abs(M) > 1e-6:
        x3 = (-limit - C) / M
        if -limit - eps <= x3 <= limit + eps: pts.append((x3, -limit))
        x4 = (limit - C) / M
        if -limit - eps <= x4 <= limit + eps: pts.append((x4, limit))
    unique_pts = []
    for p in pts:
        if not any(abs(p[0] - up[0]) < eps and abs(p[1] - up[1]) < eps for up in unique_pts):
            unique_pts.append(p)
    if len(unique_pts) >= 2: return unique_pts[0], unique_pts[1]
    return None

def create_cylinder_line(M, C, limit, thickness, mat, target_collection):
    pts = get_line_segment_in_bounds(M, C, limit)
    if not pts: return None
    p1_2d, p2_2d = pts
    p1 = mathutils.Vector((p1_2d[0], 0.0, p1_2d[1]))
    p2 = mathutils.Vector((p2_2d[0], 0.0, p2_2d[1]))
    dist = (p2 - p1).length
    if dist < 1e-4: return None
    center = (p1 + p2) / 2.0
    bm = bmesh.new()
    geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=32, radius1=thickness, radius2=thickness, depth=dist)
    axis = (p2 - p1).normalized()
    up = mathutils.Vector((0, 0, 1))
    bmesh.ops.transform(bm, matrix=up.rotation_difference(axis).to_matrix().to_4x4(), verts=geom['verts'])
    bmesh.ops.translate(bm, verts=geom['verts'], vec=center)
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    mesh = bpy.data.meshes.new(f"EqCyl_Mesh")
    bm.to_mesh(mesh)
    bm.free(); apply_auto_smooth(mesh)
    obj = bpy.data.objects.new(f"EqLine_{datetime.now().strftime('%H%M%S')}", mesh)
    target_collection.objects.link(obj)
    obj.data.materials.append(mat)
    return obj

# ==============================================================================
#  マテリアル・プレビュー制御
# ==============================================================================

def create_unique_material(color, name_prefix="Mat_UniqueShape"):
    mat_name = f"{name_prefix}_{datetime.now().strftime('%M%S%f')[:5]}"
    mat = bpy.data.materials.new(name=mat_name)
    mat.use_nodes = True; mat.blend_method = 'BLEND'
    if mat.use_nodes:
        tree = mat.node_tree
        tree.nodes.clear()
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        out.location = (300, 0)
        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]
    cleanup_old_materials(name_prefix)
    return mat

def get_or_create_preview_material():
    mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
    if not mat:
        mat = bpy.data.materials.new(name=PREVIEW_MAT_NAME)
        mat.use_nodes = True; mat.blend_method = 'BLEND'
    return mat

def update_preview_material(mat, color):
    if mat.use_nodes:
        bsdf = None
        for node in mat.node_tree.nodes:
            if node.type == 'BSDF_PRINCIPLED': bsdf = node; break
        if not bsdf:
            mat.node_tree.nodes.clear()
            bsdf = mat.node_tree.nodes.new("ShaderNodeBsdfPrincipled")
            out = mat.node_tree.nodes.new("ShaderNodeOutputMaterial")
            mat.node_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_transform_matrix(props):
    rot_matrix = mathutils.Matrix.Identity(4)
    if props.torus_plane == 'YZ': rot_matrix = mathutils.Matrix.Rotation(math.radians(90.0), 4, 'Y')
    elif props.torus_plane == 'ZX': rot_matrix = mathutils.Matrix.Rotation(math.radians(-90.0), 4, 'X')
    user_rot = mathutils.Euler((math.radians(props.torus_rot[0]), math.radians(props.torus_rot[1]), math.radians(props.torus_rot[2])), 'XYZ').to_matrix().to_4x4()
    loc_matrix = mathutils.Matrix.Translation(mathutils.Vector(props.torus_loc))
    return loc_matrix @ user_rot @ rot_matrix

def generate_shape_bmesh(bm, props):
    sx = min(max(props.size_x, 0.01), 10000.0); sy = min(max(props.size_y, 0.01), 10000.0)
    mr = min(max(props.minor_radius, 0.001), 5000.0)
    if props.base_shape == 'CUBE': create_cube_framework_bmesh(bm, sx, mr, props.minor_segments)
    elif props.base_shape == 'SQUARE': create_square_torus_bmesh(bm, sx, props.corner_radius, mr, props.corner_segments, props.minor_segments)
    elif props.base_shape == 'CIRCLE': create_ellipse_torus_bmesh(bm, sx, sx, mr, props.major_segments, props.minor_segments)
    elif props.base_shape == 'ELLIPSE': create_ellipse_torus_bmesh(bm, sx, sy, mr, props.major_segments, props.minor_segments)

def update_preview_geometry(context):
    props = getattr(context.scene, PROPS_NAME, None)
    if not props: 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)
    
    obj = bpy.data.objects.get(PREVIEW_OBJ_NAME); guide_obj = bpy.data.objects.get(PREVIEW_GUIDE_NAME)
    if not props.show_preview:
        if obj: bpy.data.objects.remove(obj, do_unlink=True)
        if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)
        return

    final_matrix = get_transform_matrix(props)
    scene_mesh_name = f"PreviewMesh_{PREFIX}_{context.scene.name}"
    bm = bmesh.new()
    try:
        generate_shape_bmesh(bm, props)
        bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
        mesh = bpy.data.meshes.get(scene_mesh_name)
        if not mesh: mesh = bpy.data.meshes.new(scene_mesh_name)
        else: mesh.clear_geometry()
        bm.to_mesh(mesh)
        apply_auto_smooth(mesh); mesh.update(calc_edges=True)
    finally: bm.free()

    if not obj:
        obj = bpy.data.objects.new(PREVIEW_OBJ_NAME, mesh)
        col.objects.link(obj)
    elif obj.data != mesh: obj.data = mesh

    mat = get_or_create_preview_material()
    update_preview_material(mat, props.torus_color)
    if not obj.data.materials: obj.data.materials.append(mat)
    else: obj.data.materials[0] = mat

    if props.show_guide:
        bm_g = bmesh.new()
        try:
            if props.base_shape == 'CUBE': create_cube_guide_bmesh(bm_g, props.size_x)
            elif props.base_shape == 'SQUARE': create_square_guide_bmesh(bm_g, props.size_x)
            elif props.base_shape == 'CIRCLE': create_ellipse_guide_bmesh(bm_g, props.size_x, props.size_x, segments=props.major_segments)
            elif props.base_shape == 'ELLIPSE': create_ellipse_guide_bmesh(bm_g, props.size_x, props.size_y, segments=props.major_segments)
            bmesh.ops.transform(bm_g, matrix=final_matrix, verts=bm_g.verts)
            guide_mesh_name = scene_mesh_name + "_Guide"
            mesh_g = bpy.data.meshes.get(guide_mesh_name)
            if not mesh_g: mesh_g = bpy.data.meshes.new(guide_mesh_name)
            else: mesh_g.clear_geometry()
            bm_g.to_mesh(mesh_g)
            mesh_g.update(calc_edges=True)
        finally: bm_g.free()
        if not guide_obj:
            guide_obj = bpy.data.objects.new(PREVIEW_GUIDE_NAME, mesh_g)
            col.objects.link(guide_obj)
        elif guide_obj.data != mesh_g: guide_obj.data = mesh_g
        guide_obj.display_type = 'WIRE'; guide_obj.show_in_front = True
    else:
        if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)

_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)

# ★ 表示/非表示トグル用のアップデート関数
def update_cyl_visibility(self, context):
    prefixes = {
        'show_cyl_group1': "EqLine_1_over_aX_",
        'show_cyl_group2': "EqLine_Z_eq_X_",
        'show_cyl_group3': "EqLine_Z_eq_minus_X_",
        'show_cyl_group4': "EqLine_Z_eq_1_over_bX_"
    }
    for prop_name, prefix in prefixes.items():
        is_visible = getattr(self, prop_name)
        for obj in bpy.data.objects:
            if obj.name.startswith(prefix):
                obj.hide_viewport = not is_visible
                obj.hide_render = not is_visible

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

class PG_TorusProps(PropertyGroup):
    show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
    show_guide: BoolProperty(name="Show Guide", default=CURRENT_DEFAULTS['show_guide'], update=on_update)
    torus_color: FloatVectorProperty(name="Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['torus_color'], update=on_update)
    base_shape: EnumProperty(name="Shape", items=[('CUBE', "Cube", ""), ('SQUARE', "Square", ""), ('CIRCLE', "Circle", ""), ('ELLIPSE', "Ellipse", "")], default=CURRENT_DEFAULTS['base_shape'], update=on_update)
    torus_plane: EnumProperty(name="Plane", items=[('XY', "XY Plane", ""), ('YZ', "YZ Plane", ""), ('ZX', "ZX Plane", "")], default=CURRENT_DEFAULTS['torus_plane'], update=on_update)
    torus_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['torus_loc'], update=on_update)
    torus_rot: FloatVectorProperty(name="Rotation", size=3, default=CURRENT_DEFAULTS['torus_rot'], update=on_update)
    size_x: FloatProperty(name="Size", default=CURRENT_DEFAULTS['size_x'], min=0.1, max=10000.0, update=on_update)
    size_y: FloatProperty(name="Size Y", default=CURRENT_DEFAULTS['size_y'], min=0.1, max=10000.0, update=on_update)
    corner_radius: FloatProperty(name="Corner Radius", default=CURRENT_DEFAULTS['corner_radius'], min=0.0, max=5000.0, update=on_update)
    minor_radius: FloatProperty(name="Tube Thickness", default=CURRENT_DEFAULTS['minor_radius'], min=0.01, max=5000.0, update=on_update)
    major_segments: IntProperty(name="Resolution", default=CURRENT_DEFAULTS['major_segments'], min=3, soft_max=128, update=on_update)
    corner_segments: IntProperty(name="Corner Segs", default=CURRENT_DEFAULTS['corner_segments'], min=1, soft_max=128, update=on_update)
    minor_segments: IntProperty(name="Tube Segs", default=CURRENT_DEFAULTS['minor_segments'], min=3, soft_max=128, update=on_update)

    cyl_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['cyl_thickness'], min=0.01, max=50.0)
    cyl_color: FloatVectorProperty(name="Color (1/a)(X±C)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['cyl_color'])
    zx_color: FloatVectorProperty(name="Color (Z=X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['zx_color'])
    minus_zx_color: FloatVectorProperty(name="Color (Z=-X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['minus_zx_color'])
    custom_zx_color: FloatVectorProperty(name="Color Custom", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['custom_zx_color'])
    
    cyl_slope_a: FloatProperty(name="Slope Param (a)", default=CURRENT_DEFAULTS['cyl_slope_a'], min=0.001)
    cyl_offset: FloatProperty(name="Offset (C)", default=CURRENT_DEFAULTS['cyl_offset'], min=0.0)
    cyl_limit: FloatProperty(name="Limit Bounds", default=CURRENT_DEFAULTS['cyl_limit'], min=1.0)
    cyl_custom_b: FloatProperty(name="Custom Slope (b)", default=CURRENT_DEFAULTS['cyl_custom_b'])
    
    show_cyl_group1: BoolProperty(name="Vis G1", default=CURRENT_DEFAULTS['show_cyl_group1'], update=update_cyl_visibility)
    show_cyl_group2: BoolProperty(name="Vis G2", default=CURRENT_DEFAULTS['show_cyl_group2'], update=update_cyl_visibility)
    show_cyl_group3: BoolProperty(name="Vis G3", default=CURRENT_DEFAULTS['show_cyl_group3'], update=update_cyl_visibility)
    show_cyl_group4: BoolProperty(name="Vis G4", default=CURRENT_DEFAULTS['show_cyl_group4'], update=update_cyl_visibility)

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

class OT_CreateTorus(Operator):
    bl_idname = f"{OP_PREFIX}.create_torus"; bl_label = "Create Shape Torus"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        bm = bmesh.new()
        generate_shape_bmesh(bm, props)
        final_matrix = get_transform_matrix(props)
        bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
        mesh = bpy.data.meshes.new(f"Shape_Mesh")
        bm.to_mesh(mesh)
        bm.free(); apply_auto_smooth(mesh)
        
        name_dict = {'CUBE': "CubeFrame", 'SQUARE': "SqTorus", 'CIRCLE': "CircTorus", 'ELLIPSE': "ElpsTorus"}
        prefix_name = name_dict.get(props.base_shape, "Shape")
        
        col_name = f"ShapeGroup_{datetime.now().strftime('%H%M%S')}"
        new_col = bpy.data.collections.new(col_name)
        context.scene.collection.children.link(new_col)
        
        obj = bpy.data.objects.new(f"{prefix_name}_{datetime.now().strftime('%H%M%S')}", mesh)
        new_col.objects.link(obj)
        
        unique_mat = create_unique_material(props.torus_color, "Mat_Unique")
        obj.data.materials.append(unique_mat)
        
        bpy.ops.object.select_all(action='DESELECT')
        obj.select_set(True)
        context.view_layer.objects.active = obj
        self.report({'INFO'}, f"Created {prefix_name} in collection '{col_name}'!")
        return {'FINISHED'}

class OT_CreateEquationCylinders(Operator):
    bl_idname = f"{OP_PREFIX}.create_equation_cylinders"
    bl_label = "Create 6 Cylinders"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        a = props.cyl_slope_a; C = props.cyl_offset; limit = props.cyl_limit; thickness = props.cyl_thickness
        b = props.cyl_custom_b
        
        if abs(a) < 0.0001:
            self.report({'ERROR'}, "Parameter 'a' is too close to zero!")
            return {'CANCELLED'}
            
        col_name = f"EqCylinders_{datetime.now().strftime('%H%M%S')}"
        new_col = bpy.data.collections.new(col_name)
        context.scene.collection.children.link(new_col)
            
        mat_eq = create_unique_material(props.cyl_color, "Mat_EqLine")
        mat_zx = create_unique_material(props.zx_color, "Mat_ZXLine")
        mat_mzx = create_unique_material(props.minus_zx_color, "Mat_MinusZXLine")
        mat_custom = create_unique_material(props.custom_zx_color, "Mat_CustomZXLine")
        
        created_objs = []
        
        # 1. Z = (1/a)(X ± C) (3 lines)
        for C_sign in [-1, 0, 1]:
            C_val = C_sign * C
            obj = create_cylinder_line(1.0/a, C_val/a, limit, thickness, mat_eq, new_col)
            if obj:
                obj.name = f"EqLine_1_over_aX_C{C_val}_{datetime.now().strftime('%H%M%S')}"
                obj.hide_viewport = not props.show_cyl_group1
                obj.hide_render = not props.show_cyl_group1
                created_objs.append(obj)
                
        # 2. Z = X (1 line)
        obj_zx = create_cylinder_line(1.0, 0.0, limit, thickness, mat_zx, new_col)
        if obj_zx:
            obj_zx.name = f"EqLine_Z_eq_X_{datetime.now().strftime('%H%M%S')}"
            obj_zx.hide_viewport = not props.show_cyl_group2
            obj_zx.hide_render = not props.show_cyl_group2
            created_objs.append(obj_zx)
            
        # 3. Z = -X (1 line)
        obj_mzx = create_cylinder_line(-1.0, 0.0, limit, thickness, mat_mzx, new_col)
        if obj_mzx:
            obj_mzx.name = f"EqLine_Z_eq_minus_X_{datetime.now().strftime('%H%M%S')}"
            obj_mzx.hide_viewport = not props.show_cyl_group3
            obj_mzx.hide_render = not props.show_cyl_group3
            created_objs.append(obj_mzx)
            
        # 4. Z = (1/b)X (1 line)
        if abs(b) < 0.0001:
            self.report({'WARNING'}, "Custom parameter 'b' is zero. Skipped creating Custom Line.")
        else:
            obj_custom = create_cylinder_line(1.0 / b, 0.0, limit, thickness, mat_custom, new_col)
            if obj_custom:
                obj_custom.name = f"EqLine_Z_eq_1_over_bX_{datetime.now().strftime('%H%M%S')}"
                obj_custom.hide_viewport = not props.show_cyl_group4
                obj_custom.hide_render = not props.show_cyl_group4
                created_objs.append(obj_custom)
            
        bpy.ops.object.select_all(action='DESELECT')
        for obj in created_objs: obj.select_set(True)
        if created_objs:
            context.view_layer.objects.active = created_objs[-1]
            self.report({'INFO'}, f"Created {len(created_objs)} Cylinders in collection '{col_name}'!")
        else:
            self.report({'WARNING'}, "No cylinders created. Lines might be entirely outside the bounds.")
            
        return {'FINISHED'}

class OT_CopyIntersectionInfo(Operator):
    bl_idname = f"{OP_PREFIX}.copy_intersection"
    bl_label = "Copy Intersections"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        a = props.cyl_slope_a; C = props.cyl_offset; b = props.cyl_custom_b
        
        text = "Intersection Points:\n"
        text += f"Equations & Parameters:\n"
        text += f" Base Lines : Z = (1/{a:.4f})(X ± {C:.4f})\n\n"
        
        text += "[ Z = X  and  Base Lines ]\n"
        if abs(a - 1.0) < 0.0001: text += " Lines are parallel.\n"
        else:
            for sign in [-1, 0, 1]:
                C_val = sign * C
                x_val = C_val / (a - 1.0)
                text += f" C={C_val:+.1f} : X={x_val:.4f}, Z={x_val:.4f}\n"
            
        text += "\n[ Z = -X  and  Base Lines ]\n"
        if abs(a + 1.0) < 0.0001: text += " Lines are parallel.\n"
        else:
            for sign in [-1, 0, 1]:
                C_val = sign * C
                x_val = -C_val / (a + 1.0)
                text += f" C={C_val:+.1f} : X={x_val:.4f}, Z={-x_val:.4f}\n"

        text += f"\n[ Z = (1/{b:.4f})X  and  Base Lines ]\n"
        d = a - b
        if abs(b) < 0.0001: text += " Parameter 'b' is zero. Invalid.\n"
        elif abs(d) < 0.0001: text += " Lines are parallel.\n"
        else:
            for sign in [-1, 0, 1]:
                C_val = sign * C
                x_val = (b * C_val) / d
                text += f" C={C_val:+.1f} : X={x_val:.4f}, Z={x_val/b:.4f}\n"
            
        context.window_manager.clipboard = text
        self.report({'INFO'}, "Intersection Data Copied to Clipboard!")
        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()
        c, l, r = props.torus_color, props.torus_loc, props.torus_rot
        cc, zc, mzc, czc = props.cyl_color, props.zx_color, props.minus_zx_color, props.custom_zx_color
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_preview": {props.show_preview},\n'
        new_dict += f'    "show_guide": {props.show_guide},\n'
        new_dict += f'    "torus_color": ({c[0]:.4f}, {c[1]:.4f}, {c[2]:.4f}, {c[3]:.4f}),\n'
        new_dict += f'    "torus_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
        new_dict += f'    "torus_rot": ({r[0]:.4f}, {r[1]:.4f}, {r[2]:.4f}),\n'
        new_dict += f'    "base_shape": "{props.base_shape}",\n'
        new_dict += f'    "torus_plane": "{props.torus_plane}",\n'
        new_dict += f'    "size_x": {props.size_x:.4f},\n'
        new_dict += f'    "size_y": {props.size_y:.4f},\n'
        new_dict += f'    "corner_radius": {props.corner_radius:.4f},\n'
        new_dict += f'    "minor_radius": {props.minor_radius:.4f},\n'
        new_dict += f'    "major_segments": {props.major_segments},\n'
        new_dict += f'    "corner_segments": {props.corner_segments},\n'
        new_dict += f'    "minor_segments": {props.minor_segments},\n'
        new_dict += f'    "cyl_thickness": {props.cyl_thickness:.4f},\n'
        new_dict += f'    "cyl_color": ({cc[0]:.4f}, {cc[1]:.4f}, {cc[2]:.4f}, {cc[3]:.4f}),\n'
        new_dict += f'    "zx_color": ({zc[0]:.4f}, {zc[1]:.4f}, {zc[2]:.4f}, {zc[3]:.4f}),\n'
        new_dict += f'    "minus_zx_color": ({mzc[0]:.4f}, {mzc[1]:.4f}, {mzc[2]:.4f}, {mzc[3]:.4f}),\n'
        new_dict += f'    "custom_zx_color": ({czc[0]:.4f}, {czc[1]:.4f}, {czc[2]:.4f}, {czc[3]:.4f}),\n'
        new_dict += f'    "cyl_slope_a": {props.cyl_slope_a:.4f},\n'
        new_dict += f'    "cyl_offset": {props.cyl_offset:.4f},\n'
        new_dict += f'    "cyl_limit": {props.cyl_limit:.4f},\n'
        new_dict += f'    "cyl_custom_b": {props.cyl_custom_b:.4f},\n'
        new_dict += f'    "show_cyl_group1": {props.show_cyl_group1},\n'
        new_dict += f'    "show_cyl_group2": {props.show_cyl_group2},\n'
        new_dict += f'    "show_cyl_group3": {props.show_cyl_group3},\n'
        new_dict += f'    "show_cyl_group4": {props.show_cyl_group4},\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
            if SOURCE_ID_TAG not in final_code: return {'CANCELLED'}
            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 safely!")
        except Exception as e: return {'CANCELLED'}
        return {'FINISHED'}

class OT_Reset(Operator):
    bl_idname = f"{OP_PREFIX}.reset"; bl_label = "Reset Transform"
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME)
        p.torus_loc = (0,0,0); p.torus_rot = (0,0,0); p.torus_plane = 'XY'; p.base_shape = 'SQUARE'
        p.size_x = 10.0; p.size_y = 5.0; p.corner_radius = 0.0; p.minor_radius = 0.5
        p.cyl_thickness = 0.5; p.cyl_color = (0.1, 0.6, 0.9, 1.0)
        p.zx_color = (0.9, 0.2, 0.2, 1.0); p.minus_zx_color = (0.9, 0.8, 0.2, 1.0)
        p.custom_zx_color = (0.2, 0.8, 0.9, 1.0)
        p.cyl_slope_a = 0.6; p.cyl_offset = 10.0; p.cyl_limit = 50.0; p.cyl_custom_b = 1.6
        p.show_cyl_group1 = True; p.show_cyl_group2 = True
        p.show_cyl_group3 = True; p.show_cyl_group4 = True
        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 = "Remove Addon"
    def execute(self, context):
        bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
        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: layout.label(text="Reload Script"); return
        row = layout.row(); row.scale_y = 1.2
        row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
        layout.separator()
        layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
        box = layout.box()
        if not props.show_preview: box.label(text="Preview is Hidden", icon='INFO')
        box.prop(props, "torus_color")
        col = box.column(align=True)
        col.prop(props, "base_shape"); col.prop(props, "torus_plane")
        col.separator()
        col.prop(props, "torus_loc"); col.prop(props, "torus_rot")
        box.separator()
        box.prop(props, "show_guide", icon='MESH_GRID', text="Show Guide Wire")
        
        col_s = box.column(align=True)
        if props.base_shape == 'ELLIPSE': col_s.prop(props, "size_x", text="Size X"); col_s.prop(props, "size_y", text="Size Y")
        else: col_s.prop(props, "size_x", text="Size")
        
        row_cr = col_s.row(); row_cr.enabled = (props.base_shape == 'SQUARE')
        row_cr.prop(props, "corner_radius")
        if props.corner_radius <= 0.001 and props.base_shape == 'SQUARE': row_cr.label(text="[90° Mode]", icon='SNAP_VERTEX')
            
        col_s.prop(props, "minor_radius")
        row_seg = box.row()
        if props.base_shape in['CIRCLE', 'ELLIPSE']: row_seg.prop(props, "major_segments", text="Resolution")
        elif props.base_shape == 'SQUARE': row_seg.prop(props, "corner_segments", text="Corner Segs")
        else: row_seg.label(text="[Cube has fixed corners]")
        box.row().prop(props, "minor_segments")
        box.operator(OT_Reset.bl_idname, icon='LOOP_BACK')
        layout.separator()
        col_exec = layout.column(); col_exec.scale_y = 1.5
        icons = {'CUBE': 'MESH_CUBE', 'SQUARE': 'MESH_PLANE', 'CIRCLE': 'MESH_CIRCLE', 'ELLIPSE': 'MESH_CIRCLE'}
        texts = {'CUBE': "Create Cube Frame", 'SQUARE': "Create Square Torus", 'CIRCLE': "Create Circle Torus", 'ELLIPSE': "Create Ellipse Torus"}
        col_exec.operator(OT_CreateTorus.bl_idname, icon=icons.get(props.base_shape, 'MESH_TORUS'), text=texts.get(props.base_shape, "Create Torus"))

class PT_EquationCylindersPanel(Panel):
    bl_label = "Equation Cylinders (6 Lines)"
    bl_idname = f"{PREFIX}_PT_eq_cylinders"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return
        
        a = props.cyl_slope_a
        C = props.cyl_offset
        b = props.cyl_custom_b
        
        box = layout.box()
        box.label(text="Math Equations (Y=0 Plane):", icon='FILE_TEXT')
        box.label(text=f"  G1: Z = ( 1 / {a:.2f} ) ( X ± {C:.2f} )")
        box.label(text=f"  G2: Z = X")
        box.label(text=f"  G3: Z = -X")
        box.label(text=f"  G4: Z = ( 1 / {b:.2f} ) X")
        
        box.separator()
        col = box.column(align=True)
        col.prop(props, "cyl_slope_a", text="Param (a)")
        col.prop(props, "cyl_offset", text="Offset (C)")
        col.prop(props, "cyl_limit", text="Limit Bounds (-L to L)")
        col.prop(props, "cyl_custom_b", text="Custom Param (b)")
        
        box.separator()
        box.prop(props, "cyl_thickness", text="Thickness")
        
        # ★ カラー設定 & 表示トグル
        col_c = box.column(align=True)
        r1 = col_c.row(align=True)
        r1.prop(props, "show_cyl_group1", text="", icon='HIDE_OFF' if props.show_cyl_group1 else 'HIDE_ON')
        r1.prop(props, "cyl_color", text=f"Z=(1/{a:.2f})(X±{C:.1f})")
        
        r2 = col_c.row(align=True)
        r2.prop(props, "show_cyl_group2", text="", icon='HIDE_OFF' if props.show_cyl_group2 else 'HIDE_ON')
        r2.prop(props, "zx_color", text="Z = X")
        
        r3 = col_c.row(align=True)
        r3.prop(props, "show_cyl_group3", text="", icon='HIDE_OFF' if props.show_cyl_group3 else 'HIDE_ON')
        r3.prop(props, "minus_zx_color", text="Z = -X")

        r4 = col_c.row(align=True)
        r4.prop(props, "show_cyl_group4", text="", icon='HIDE_OFF' if props.show_cyl_group4 else 'HIDE_ON')
        r4.prop(props, "custom_zx_color", text=f"Z = (1/{b:.2f})X")
        
        # ★ 交点表示エリア (縦1行ずつ表示)
        box.separator()
        box_int = box.box()
        box_int.label(text=f"Intersections with Z=(1/{a:.2f})(X±{C:.1f}) :", icon='DRIVER')
        
        # [ Z = X ] 側
        box_int.label(text="--- [ with Z = X ] ---")
        if abs(1.0 - a) < 0.0001: 
            box_int.label(text="  Parallel")
        else:
            for sign in [-1, 0, 1]:
                C_val = sign * C
                x_val = C_val / (a - 1.0)
                box_int.label(text=f"  C={C_val:+.1f} : X={x_val:.2f}, Z={x_val:.2f}")
            
        box_int.separator()
        
        # [ Z = -X ] 側
        box_int.label(text="--- [ with Z = -X ] ---")
        if abs(-1.0 - a) < 0.0001: 
            box_int.label(text="  Parallel")
        else:
            for sign in [-1, 0, 1]:
                C_val = sign * C
                x_val = -C_val / (a + 1.0)
                box_int.label(text=f"  C={C_val:+.1f} : X={x_val:.2f}, Z={-x_val:.2f}")
            
        box_int.separator()
        
        # [ Z = (1/b)X ] 側
        box_int.label(text=f"--- [ with Z = (1/{b:.2f})X ] ---")
        d = a - b
        if abs(b) < 0.0001: 
            box_int.label(text="  Invalid (b=0)")
        elif abs(d) < 0.0001: 
            box_int.label(text="  Parallel")
        else:
            for sign in [-1, 0, 1]:
                C_val = sign * C
                x_val = (b * C_val) / d
                box_int.label(text=f"  C={C_val:+.1f} : X={x_val:.2f}, Z={x_val/b:.2f}")
            
        box_int.operator(OT_CopyIntersectionInfo.bl_idname, icon='COPYDOWN', text="Copy Intersections")
        col_exec = box.column(); col_exec.scale_y = 1.2
        col_exec.operator(OT_CreateEquationCylinders.bl_idname, icon='MESH_CYLINDER', text="Create 6 Cylinders")

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_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        for l in ADDON_LINKS: self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"]).url = l["url"]

class PT_RemovePanel(Panel):
    bl_label = "System"; bl_idname = f"{PREFIX}_PT_remove"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): self.layout.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Remove Addon")

# ==============================================================================
#  REGISTER
# ==============================================================================
classes = (
    PG_TorusProps, 
    OT_CreateTorus, 
    OT_CreateEquationCylinders, 
    OT_CopyIntersectionInfo, 
    OT_CopyFullScript, 
    OT_Reset, 
    OT_OpenUrl, 
    OT_RemoveAddon, 
    PT_MainPanel, 
    PT_EquationCylindersPanel, 
    PT_LinksPanel, 
    PT_RemovePanel
)

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_TorusProps))
    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-03-24 21:03:00
import bpy
import bmesh
import webbrowser
import math
import mathutils
import time
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime

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

PREFIX       = "ShapeTorus20260324"
ADDON_NAME   = "zionad 520[ Shape-Torus ]"
TAB_NAME     = "[ Shape Torus copy ]   "
PANEL_TITLE  = "Multi-Shape Generator"
AUTHOR       = "zionadchat"

# ★ このスクリプト自身のID
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SHAPE_TORUS_2026_03_24_V9_MULTI_SHAPE ###"

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

bl_info = {
    "name": f"{ADDON_NAME} {PREFIX}",
    "author": AUTHOR,
    "version": (9, 9, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": f"Multi-Shape Torus & Equation Cylinders - {PREFIX}",
    "category": "3D View",
}

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

# ★ リンク設定
ADDON_LINKS = (
    {"label": "時空図 交点 2060407", "url": "<https://www.notion.so/2060407-33af5dacaf43808d86bbf0d54d4d0dd5>"},
    {"label": "Prefix トーラス正方形 20260324", "url": "<https://www.notion.so/Prefix-20260324-32df5dacaf4380528980db6a989d6306>"},
)

# ==============================================================================
#  デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_preview": True,
    "show_guide": True,
    "torus_color": (0.0391, 0.8000, 0.1647, 1.0000),
    "torus_loc": (0.0000, 0.0000, 0.0000),
    "torus_rot": (0.0000, 0.0000, 0.0000),
    "base_shape": "SQUARE",
    "torus_plane": "XY",
    "size_x": 10.0000,
    "size_y": 5.0000,
    "corner_radius": 0.0000,
    "minor_radius": 0.5000,
    "major_segments": 32,
    "corner_segments": 8,
    "minor_segments": 16,
    "cyl_thickness": 0.5000,
    "cyl_color": (0.1000, 0.6000, 0.9000, 1.0000),
    "zx_color": (0.9000, 0.2000, 0.2000, 1.0000),
    "minus_zx_color": (0.9000, 0.8000, 0.2000, 1.0000),
    "custom_zx_color": (0.2000, 0.8000, 0.9000, 1.0000),
    "cyl_slope_a": 0.6000,
    "cyl_offset": 10.0000,
    "cyl_limit": 50.0000,
    "cyl_custom_b": -1.6000,
    "show_cyl_group1": True,
    "show_cyl_group2": True,
    "show_cyl_group3": True,
    "show_cyl_group4": True,
}
# <END_DICT>

# ==============================================================================
#  データ クリーンアップ管理
# ==============================================================================

PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_OBJ_NAME = f"[Preview] Shape_{PREFIX}"
PREVIEW_GUIDE_NAME = f"[Preview] Guide_{PREFIX}"
PREVIEW_MAT_NAME = f"PreviewMat_{PREFIX}"

def cleanup_preview_data():
    for name in[PREVIEW_OBJ_NAME, PREVIEW_GUIDE_NAME]:
        obj = bpy.data.objects.get(name)
        if obj:
            mesh = obj.data
            bpy.data.objects.remove(obj, do_unlink=True)
            if mesh and mesh.users == 0: bpy.data.meshes.remove(mesh)
                
    meshes_to_remove =[m for m in bpy.data.meshes if m.name.startswith(f"PreviewMesh_{PREFIX}")]
    for m in meshes_to_remove:
        if m.users == 0: bpy.data.meshes.remove(m)
    mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
    if mat and mat.users == 0: bpy.data.materials.remove(mat)
    col = bpy.data.collections.get(PREVIEW_COL_NAME)
    if col and len(col.objects) == 0: bpy.data.collections.remove(col)

def cleanup_old_materials(prefix="Mat_UniqueShape", limit=50):
    mats =[m for m in bpy.data.materials if m.name.startswith(prefix)]
    if len(mats) > limit:
        for m in mats[:-limit]:
            if m.users == 0: bpy.data.materials.remove(m)

# ==============================================================================
#  ジオメトリ エンジン
# ==============================================================================

def create_square_guide_bmesh(bm, size):
    S = size / 2.0
    v1 = bm.verts.new((S, S, 0)); v2 = bm.verts.new((-S, S, 0))
    v3 = bm.verts.new((-S, -S, 0)); v4 = bm.verts.new((S, -S, 0))
    bm.verts.ensure_lookup_table()
    bm.edges.new((v1, v2)); bm.edges.new((v2, v3)); bm.edges.new((v3, v4)); bm.edges.new((v4, v1))
    return bm

def create_cube_guide_bmesh(bm, size):
    geom = bmesh.ops.create_cube(bm, size=size)
    faces =[f for f in bm.faces]
    bmesh.ops.delete(bm, geom=faces, context='FACES_ONLY')
    return bm

def create_ellipse_guide_bmesh(bm, size_x, size_y, segments=64):
    a = size_x / 2.0; b = size_y / 2.0
    verts =[]
    for i in range(segments):
        t = i * 2.0 * math.pi / segments
        verts.append(bm.verts.new((a * math.cos(t), b * math.sin(t), 0)))
    bm.verts.ensure_lookup_table()
    for i in range(segments): bm.edges.new((verts[i], verts[(i + 1) % segments]))
    return bm

def create_ellipse_torus_bmesh(bm, size_x, size_y, minor_radius, major_segments, minor_segments):
    a = size_x / 2.0; b = size_y / 2.0
    rings =[]
    for i in range(major_segments):
        t = i * 2.0 * math.pi / major_segments
        p = mathutils.Vector((a * math.cos(t), b * math.sin(t), 0))
        n = mathutils.Vector((b * math.cos(t), a * math.sin(t), 0)).normalized()
        up = mathutils.Vector((0, 0, 1))
        ring =[]
        for j in range(minor_segments):
            theta = j * 2.0 * math.pi / minor_segments
            offset = n * (minor_radius * math.cos(theta)) + up * (minor_radius * math.sin(theta))
            ring.append(bm.verts.new(p + offset))
        rings.append(ring)
    bm.verts.ensure_lookup_table()
    edge_loops = []
    for ring in rings:
        edges =[]
        for j in range(minor_segments): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
        edge_loops.append(edges)
    bm.edges.ensure_lookup_table()
    for i in range(major_segments):
        try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % major_segments])
        except Exception: pass
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm

def create_square_torus_bmesh(bm, size, corner_radius, minor_radius, corner_segments, minor_segments):
    half_size = size / 2.0
    actual_corner_radius = min(max(corner_radius, 0.0), half_size)
    rings =[]; EPS = 1e-6
    if actual_corner_radius < EPS:
        L = half_size
        corners =[
            (mathutils.Vector((L, L, 0)), mathutils.Vector((1, 1, 0)).normalized()),
            (mathutils.Vector((-L, L, 0)), mathutils.Vector((-1, 1, 0)).normalized()),
            (mathutils.Vector((-L, -L, 0)), mathutils.Vector((-1, -1, 0)).normalized()),
            (mathutils.Vector((L, -L, 0)), mathutils.Vector((1, -1, 0)).normalized())
        ]
        scale_xy = 1.0 / math.cos(math.pi / 4)
        for p, n in corners:
            b = mathutils.Vector((0, 0, 1))
            ring =[]
            for j in range(minor_segments):
                theta = j * 2.0 * math.pi / minor_segments
                offset = n * (minor_radius * math.cos(theta) * scale_xy) + b * (minor_radius * math.sin(theta))
                ring.append(bm.verts.new(p + offset))
            rings.append(ring)
    else:
        L = half_size - actual_corner_radius
        pts = []
        for q in range(4):
            cx = L if q in[0, 3] else -L
            cy = L if q in [0, 1] else -L
            for i in range(corner_segments + 1):
                angle = q * (math.pi / 2) + i * (math.pi / 2) / corner_segments
                pts.append((mathutils.Vector((cx + actual_corner_radius * math.cos(angle), cy + actual_corner_radius * math.sin(angle), 0)), mathutils.Vector((math.cos(angle), math.sin(angle), 0))))
        unique_pts =[]
        for p, n in pts:
            if not unique_pts or (unique_pts[-1][0] - p).length > EPS: unique_pts.append((p, n))
        if len(unique_pts) > 1 and (unique_pts[-1][0] - unique_pts[0][0]).length < EPS: unique_pts.pop()
        for p, n in unique_pts:
            b = mathutils.Vector((0, 0, 1))
            ring =[]
            for j in range(minor_segments):
                theta = j * 2.0 * math.pi / minor_segments
                ring.append(bm.verts.new(p + n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))))
            rings.append(ring)
    bm.verts.ensure_lookup_table()
    total_rings = len(rings)
    if total_rings < 3: return bm
    edge_loops = []
    for ring in rings:
        edges =[]
        for j in range(minor_segments): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
        edge_loops.append(edges)
    bm.edges.ensure_lookup_table()
    for i in range(total_rings):
        try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % total_rings])
        except Exception: pass
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm

def create_cube_framework_bmesh(bm, size, minor_radius, minor_segments):
    L = size / 2.0
    verts_co =[
        mathutils.Vector(( L,  L,  L)), mathutils.Vector((-L,  L,  L)),
        mathutils.Vector((-L, -L,  L)), mathutils.Vector(( L, -L,  L)),
        mathutils.Vector(( L,  L, -L)), mathutils.Vector((-L,  L, -L)),
        mathutils.Vector((-L, -L, -L)), mathutils.Vector(( L, -L, -L)),
    ]
    edges_idx =[(0,1), (1,2), (2,3), (3,0), (4,5), (5,6), (6,7), (7,4), (0,4), (1,5), (2,6), (3,7)]
    for co in verts_co:
        geom = bmesh.ops.create_uvsphere(bm, u_segments=minor_segments, v_segments=max(minor_segments//2, 3), radius=minor_radius)
        bmesh.ops.translate(bm, verts=geom['verts'], vec=co)
    for idx1, idx2 in edges_idx:
        v1 = verts_co[idx1]; v2 = verts_co[idx2]
        dist = (v1 - v2).length
        geom = bmesh.ops.create_cone(bm, cap_ends=False, cap_tris=False, segments=minor_segments, radius1=minor_radius, radius2=minor_radius, depth=dist)
        axis = (v1 - v2).normalized()
        bmesh.ops.transform(bm, matrix=mathutils.Vector((0,0,1)).rotation_difference(axis).to_matrix().to_4x4(), verts=geom['verts'])
        bmesh.ops.translate(bm, verts=geom['verts'], vec=(v1 + v2) / 2.0)
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm

def apply_auto_smooth(mesh):
    if bpy.app.version < (4, 1, 0):
        try:
            if hasattr(mesh, "use_auto_smooth"):
                mesh.use_auto_smooth = True
                mesh.auto_smooth_angle = math.radians(30)
        except AttributeError: pass

# ==============================================================================
#  計算ロジック(直線の方程式をボックス制限内でクリッピング)
# ==============================================================================
def get_line_segment_in_bounds(M, C, limit):
    pts = []
    eps = 1e-4
    z1 = M * (-limit) + C
    if -limit - eps <= z1 <= limit + eps: pts.append((-limit, z1))
    z2 = M * limit + C
    if -limit - eps <= z2 <= limit + eps: pts.append((limit, z2))
    if abs(M) > 1e-6:
        x3 = (-limit - C) / M
        if -limit - eps <= x3 <= limit + eps: pts.append((x3, -limit))
        x4 = (limit - C) / M
        if -limit - eps <= x4 <= limit + eps: pts.append((x4, limit))
    unique_pts = []
    for p in pts:
        if not any(abs(p[0] - up[0]) < eps and abs(p[1] - up[1]) < eps for up in unique_pts):
            unique_pts.append(p)
    if len(unique_pts) >= 2: return unique_pts[0], unique_pts[1]
    return None

def create_cylinder_line(M, C, limit, thickness, mat, target_collection):
    pts = get_line_segment_in_bounds(M, C, limit)
    if not pts: return None
    p1_2d, p2_2d = pts
    p1 = mathutils.Vector((p1_2d[0], 0.0, p1_2d[1]))
    p2 = mathutils.Vector((p2_2d[0], 0.0, p2_2d[1]))
    dist = (p2 - p1).length
    if dist < 1e-4: return None
    center = (p1 + p2) / 2.0
    bm = bmesh.new()
    geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=32, radius1=thickness, radius2=thickness, depth=dist)
    axis = (p2 - p1).normalized()
    up = mathutils.Vector((0, 0, 1))
    bmesh.ops.transform(bm, matrix=up.rotation_difference(axis).to_matrix().to_4x4(), verts=geom['verts'])
    bmesh.ops.translate(bm, verts=geom['verts'], vec=center)
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    mesh = bpy.data.meshes.new(f"EqCyl_Mesh")
    bm.to_mesh(mesh)
    bm.free(); apply_auto_smooth(mesh)
    obj = bpy.data.objects.new(f"EqLine_{datetime.now().strftime('%H%M%S')}", mesh)
    target_collection.objects.link(obj)
    obj.data.materials.append(mat)
    return obj

# ==============================================================================
#  マテリアル・プレビュー制御
# ==============================================================================

def create_unique_material(color, name_prefix="Mat_UniqueShape"):
    mat_name = f"{name_prefix}_{datetime.now().strftime('%M%S%f')[:5]}"
    mat = bpy.data.materials.new(name=mat_name)
    mat.use_nodes = True; mat.blend_method = 'BLEND'
    if mat.use_nodes:
        tree = mat.node_tree
        tree.nodes.clear()
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        out.location = (300, 0)
        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]
    cleanup_old_materials(name_prefix)
    return mat

def get_or_create_preview_material():
    mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
    if not mat:
        mat = bpy.data.materials.new(name=PREVIEW_MAT_NAME)
        mat.use_nodes = True; mat.blend_method = 'BLEND'
    return mat

def update_preview_material(mat, color):
    if mat.use_nodes:
        bsdf = None
        for node in mat.node_tree.nodes:
            if node.type == 'BSDF_PRINCIPLED': bsdf = node; break
        if not bsdf:
            mat.node_tree.nodes.clear()
            bsdf = mat.node_tree.nodes.new("ShaderNodeBsdfPrincipled")
            out = mat.node_tree.nodes.new("ShaderNodeOutputMaterial")
            mat.node_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_transform_matrix(props):
    rot_matrix = mathutils.Matrix.Identity(4)
    if props.torus_plane == 'YZ': rot_matrix = mathutils.Matrix.Rotation(math.radians(90.0), 4, 'Y')
    elif props.torus_plane == 'ZX': rot_matrix = mathutils.Matrix.Rotation(math.radians(-90.0), 4, 'X')
    user_rot = mathutils.Euler((math.radians(props.torus_rot[0]), math.radians(props.torus_rot[1]), math.radians(props.torus_rot[2])), 'XYZ').to_matrix().to_4x4()
    loc_matrix = mathutils.Matrix.Translation(mathutils.Vector(props.torus_loc))
    return loc_matrix @ user_rot @ rot_matrix

def generate_shape_bmesh(bm, props):
    sx = min(max(props.size_x, 0.01), 10000.0); sy = min(max(props.size_y, 0.01), 10000.0)
    mr = min(max(props.minor_radius, 0.001), 5000.0)
    if props.base_shape == 'CUBE': create_cube_framework_bmesh(bm, sx, mr, props.minor_segments)
    elif props.base_shape == 'SQUARE': create_square_torus_bmesh(bm, sx, props.corner_radius, mr, props.corner_segments, props.minor_segments)
    elif props.base_shape == 'CIRCLE': create_ellipse_torus_bmesh(bm, sx, sx, mr, props.major_segments, props.minor_segments)
    elif props.base_shape == 'ELLIPSE': create_ellipse_torus_bmesh(bm, sx, sy, mr, props.major_segments, props.minor_segments)

def update_preview_geometry(context):
    props = getattr(context.scene, PROPS_NAME, None)
    if not props: 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)
    
    obj = bpy.data.objects.get(PREVIEW_OBJ_NAME); guide_obj = bpy.data.objects.get(PREVIEW_GUIDE_NAME)
    if not props.show_preview:
        if obj: bpy.data.objects.remove(obj, do_unlink=True)
        if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)
        return

    final_matrix = get_transform_matrix(props)
    scene_mesh_name = f"PreviewMesh_{PREFIX}_{context.scene.name}"
    bm = bmesh.new()
    try:
        generate_shape_bmesh(bm, props)
        bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
        mesh = bpy.data.meshes.get(scene_mesh_name)
        if not mesh: mesh = bpy.data.meshes.new(scene_mesh_name)
        else: mesh.clear_geometry()
        bm.to_mesh(mesh)
        apply_auto_smooth(mesh); mesh.update(calc_edges=True)
    finally: bm.free()

    if not obj:
        obj = bpy.data.objects.new(PREVIEW_OBJ_NAME, mesh)
        col.objects.link(obj)
    elif obj.data != mesh: obj.data = mesh

    mat = get_or_create_preview_material()
    update_preview_material(mat, props.torus_color)
    if not obj.data.materials: obj.data.materials.append(mat)
    else: obj.data.materials[0] = mat

    if props.show_guide:
        bm_g = bmesh.new()
        try:
            if props.base_shape == 'CUBE': create_cube_guide_bmesh(bm_g, props.size_x)
            elif props.base_shape == 'SQUARE': create_square_guide_bmesh(bm_g, props.size_x)
            elif props.base_shape == 'CIRCLE': create_ellipse_guide_bmesh(bm_g, props.size_x, props.size_x, segments=props.major_segments)
            elif props.base_shape == 'ELLIPSE': create_ellipse_guide_bmesh(bm_g, props.size_x, props.size_y, segments=props.major_segments)
            bmesh.ops.transform(bm_g, matrix=final_matrix, verts=bm_g.verts)
            guide_mesh_name = scene_mesh_name + "_Guide"
            mesh_g = bpy.data.meshes.get(guide_mesh_name)
            if not mesh_g: mesh_g = bpy.data.meshes.new(guide_mesh_name)
            else: mesh_g.clear_geometry()
            bm_g.to_mesh(mesh_g)
            mesh_g.update(calc_edges=True)
        finally: bm_g.free()
        if not guide_obj:
            guide_obj = bpy.data.objects.new(PREVIEW_GUIDE_NAME, mesh_g)
            col.objects.link(guide_obj)
        elif guide_obj.data != mesh_g: guide_obj.data = mesh_g
        guide_obj.display_type = 'WIRE'; guide_obj.show_in_front = True
    else:
        if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)

_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)

# ★ 表示/非表示トグル用のアップデート関数
def update_cyl_visibility(self, context):
    prefixes = {
        'show_cyl_group1': "EqLine_aX_",
        'show_cyl_group2': "EqLine_Z_eq_X_",
        'show_cyl_group3': "EqLine_Z_eq_minus_X_",
        'show_cyl_group4': "EqLine_Z_eq_custom_bX_"
    }
    for prop_name, prefix in prefixes.items():
        is_visible = getattr(self, prop_name)
        for obj in bpy.data.objects:
            if obj.name.startswith(prefix):
                obj.hide_viewport = not is_visible
                obj.hide_render = not is_visible

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

class PG_TorusProps(PropertyGroup):
    show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
    show_guide: BoolProperty(name="Show Guide", default=CURRENT_DEFAULTS['show_guide'], update=on_update)
    torus_color: FloatVectorProperty(name="Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['torus_color'], update=on_update)
    base_shape: EnumProperty(name="Shape", items=[('CUBE', "Cube", ""), ('SQUARE', "Square", ""), ('CIRCLE', "Circle", ""), ('ELLIPSE', "Ellipse", "")], default=CURRENT_DEFAULTS['base_shape'], update=on_update)
    torus_plane: EnumProperty(name="Plane", items=[('XY', "XY Plane", ""), ('YZ', "YZ Plane", ""), ('ZX', "ZX Plane", "")], default=CURRENT_DEFAULTS['torus_plane'], update=on_update)
    torus_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['torus_loc'], update=on_update)
    torus_rot: FloatVectorProperty(name="Rotation", size=3, default=CURRENT_DEFAULTS['torus_rot'], update=on_update)
    size_x: FloatProperty(name="Size", default=CURRENT_DEFAULTS['size_x'], min=0.1, max=10000.0, update=on_update)
    size_y: FloatProperty(name="Size Y", default=CURRENT_DEFAULTS['size_y'], min=0.1, max=10000.0, update=on_update)
    corner_radius: FloatProperty(name="Corner Radius", default=CURRENT_DEFAULTS['corner_radius'], min=0.0, max=5000.0, update=on_update)
    minor_radius: FloatProperty(name="Tube Thickness", default=CURRENT_DEFAULTS['minor_radius'], min=0.01, max=5000.0, update=on_update)
    major_segments: IntProperty(name="Resolution", default=CURRENT_DEFAULTS['major_segments'], min=3, soft_max=128, update=on_update)
    corner_segments: IntProperty(name="Corner Segs", default=CURRENT_DEFAULTS['corner_segments'], min=1, soft_max=128, update=on_update)
    minor_segments: IntProperty(name="Tube Segs", default=CURRENT_DEFAULTS['minor_segments'], min=3, soft_max=128, update=on_update)

    cyl_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['cyl_thickness'], min=0.01, max=50.0)
    cyl_color: FloatVectorProperty(name="Color (a*X ± C)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['cyl_color'])
    zx_color: FloatVectorProperty(name="Color (Z=X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['zx_color'])
    minus_zx_color: FloatVectorProperty(name="Color (Z=-X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['minus_zx_color'])
    custom_zx_color: FloatVectorProperty(name="Color Custom", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['custom_zx_color'])
    
    cyl_slope_a: FloatProperty(name="Slope (a)", default=CURRENT_DEFAULTS['cyl_slope_a'])
    cyl_offset: FloatProperty(name="Offset (C)", default=CURRENT_DEFAULTS['cyl_offset'], min=0.0)
    cyl_limit: FloatProperty(name="Limit Bounds", default=CURRENT_DEFAULTS['cyl_limit'], min=1.0)
    cyl_custom_b: FloatProperty(name="Custom Slope (b)", default=CURRENT_DEFAULTS['cyl_custom_b'])
    
    show_cyl_group1: BoolProperty(name="Vis G1", default=CURRENT_DEFAULTS['show_cyl_group1'], update=update_cyl_visibility)
    show_cyl_group2: BoolProperty(name="Vis G2", default=CURRENT_DEFAULTS['show_cyl_group2'], update=update_cyl_visibility)
    show_cyl_group3: BoolProperty(name="Vis G3", default=CURRENT_DEFAULTS['show_cyl_group3'], update=update_cyl_visibility)
    show_cyl_group4: BoolProperty(name="Vis G4", default=CURRENT_DEFAULTS['show_cyl_group4'], update=update_cyl_visibility)

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

class OT_CreateTorus(Operator):
    bl_idname = f"{OP_PREFIX}.create_torus"; bl_label = "Create Shape Torus"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        bm = bmesh.new()
        generate_shape_bmesh(bm, props)
        final_matrix = get_transform_matrix(props)
        bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
        mesh = bpy.data.meshes.new(f"Shape_Mesh")
        bm.to_mesh(mesh)
        bm.free(); apply_auto_smooth(mesh)
        
        name_dict = {'CUBE': "CubeFrame", 'SQUARE': "SqTorus", 'CIRCLE': "CircTorus", 'ELLIPSE': "ElpsTorus"}
        prefix_name = name_dict.get(props.base_shape, "Shape")
        
        col_name = f"ShapeGroup_{datetime.now().strftime('%H%M%S')}"
        new_col = bpy.data.collections.new(col_name)
        context.scene.collection.children.link(new_col)
        
        obj = bpy.data.objects.new(f"{prefix_name}_{datetime.now().strftime('%H%M%S')}", mesh)
        new_col.objects.link(obj)
        
        unique_mat = create_unique_material(props.torus_color, "Mat_Unique")
        obj.data.materials.append(unique_mat)
        
        bpy.ops.object.select_all(action='DESELECT')
        obj.select_set(True)
        context.view_layer.objects.active = obj
        self.report({'INFO'}, f"Created {prefix_name} in collection '{col_name}'!")
        return {'FINISHED'}

class OT_CreateEquationCylinders(Operator):
    bl_idname = f"{OP_PREFIX}.create_equation_cylinders"
    bl_label = "Create 6 Cylinders"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        a = props.cyl_slope_a; C_val = props.cyl_offset; limit = props.cyl_limit; thickness = props.cyl_thickness
        b = props.cyl_custom_b
        
        col_name = f"EqCylinders_{datetime.now().strftime('%H%M%S')}"
        new_col = bpy.data.collections.new(col_name)
        context.scene.collection.children.link(new_col)
            
        mat_eq = create_unique_material(props.cyl_color, "Mat_EqLine")
        mat_zx = create_unique_material(props.zx_color, "Mat_ZXLine")
        mat_mzx = create_unique_material(props.minus_zx_color, "Mat_MinusZXLine")
        mat_custom = create_unique_material(props.custom_zx_color, "Mat_CustomZXLine")
        
        created_objs = []
        
        # 1. Z = a*X ± C (3 lines)
        for C in [-C_val, 0.0, C_val]:
            obj = create_cylinder_line(a, C, limit, thickness, mat_eq, new_col)
            if obj:
                obj.name = f"EqLine_aX_C{C}_{datetime.now().strftime('%H%M%S')}"
                obj.hide_viewport = not props.show_cyl_group1
                obj.hide_render = not props.show_cyl_group1
                created_objs.append(obj)
                
        # 2. Z = X (1 line)
        obj_zx = create_cylinder_line(1.0, 0.0, limit, thickness, mat_zx, new_col)
        if obj_zx:
            obj_zx.name = f"EqLine_Z_eq_X_{datetime.now().strftime('%H%M%S')}"
            obj_zx.hide_viewport = not props.show_cyl_group2
            obj_zx.hide_render = not props.show_cyl_group2
            created_objs.append(obj_zx)
            
        # 3. Z = -X (1 line)
        obj_mzx = create_cylinder_line(-1.0, 0.0, limit, thickness, mat_mzx, new_col)
        if obj_mzx:
            obj_mzx.name = f"EqLine_Z_eq_minus_X_{datetime.now().strftime('%H%M%S')}"
            obj_mzx.hide_viewport = not props.show_cyl_group3
            obj_mzx.hide_render = not props.show_cyl_group3
            created_objs.append(obj_mzx)
            
        # 4. Z = b*X (1 line)
        obj_custom = create_cylinder_line(b, 0.0, limit, thickness, mat_custom, new_col)
        if obj_custom:
            obj_custom.name = f"EqLine_Z_eq_custom_bX_{datetime.now().strftime('%H%M%S')}"
            obj_custom.hide_viewport = not props.show_cyl_group4
            obj_custom.hide_render = not props.show_cyl_group4
            created_objs.append(obj_custom)
            
        bpy.ops.object.select_all(action='DESELECT')
        for obj in created_objs: obj.select_set(True)
        if created_objs:
            context.view_layer.objects.active = created_objs[-1]
            self.report({'INFO'}, f"Created {len(created_objs)} Cylinders in collection '{col_name}'!")
        else:
            self.report({'WARNING'}, "No cylinders created. Lines might be entirely outside the bounds.")
            
        return {'FINISHED'}

class OT_CopyIntersectionInfo(Operator):
    bl_idname = f"{OP_PREFIX}.copy_intersection"
    bl_label = "Copy Intersections"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        a = props.cyl_slope_a; C_val = props.cyl_offset; b = props.cyl_custom_b
        
        text = "Intersection Points:\n"
        text += f"Equations & Parameters:\n"
        text += f" Base Lines : Z = {a:.4f} * X ± {C_val:.4f}\n\n"
        
        text += "[ Z = X  and  Base Lines ]\n"
        if abs(1.0 - a) < 0.0001: text += " Lines are parallel.\n"
        else:
            x_m = (-C_val) / (1.0 - a); x_p = C_val / (1.0 - a)
            text += f" Line (C = -{C_val:.1f}): X = {x_m:.4f}, Z = {x_m:.4f}\n"
            text += f" Line (C =  0.0): X = 0.0000, Z = 0.0000\n"
            text += f" Line (C = +{C_val:.1f}): X = {x_p:.4f}, Z = {x_p:.4f}\n"
            
        text += "\n[ Z = -X  and  Base Lines ]\n"
        if abs(-1.0 - a) < 0.0001: text += " Lines are parallel.\n"
        else:
            xm_m = (-C_val) / (-1.0 - a); xm_p = C_val / (-1.0 - a)
            text += f" Line (C = -{C_val:.1f}): X = {xm_m:.4f}, Z = {-xm_m:.4f}\n"
            text += f" Line (C =  0.0): X = 0.0000, Z = 0.0000\n"
            text += f" Line (C = +{C_val:.1f}): X = {xm_p:.4f}, Z = {-xm_p:.4f}\n"

        text += f"\n[ Z = {b:.4f} * X  and  Base Lines ]\n"
        d = b - a
        if abs(d) < 0.0001: text += " Lines are parallel.\n"
        else:
            xb_m = (-C_val) / d; xb_p = C_val / d
            text += f" Line (C = -{C_val:.1f}): X = {xb_m:.4f}, Z = {b*xb_m:.4f}\n"
            text += f" Line (C =  0.0): X = 0.0000, Z = 0.0000\n"
            text += f" Line (C = +{C_val:.1f}): X = {xb_p:.4f}, Z = {b*xb_p:.4f}\n"
            
        context.window_manager.clipboard = text
        self.report({'INFO'}, "Intersection Data Copied to Clipboard!")
        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()
        c, l, r = props.torus_color, props.torus_loc, props.torus_rot
        cc, zc, mzc, czc = props.cyl_color, props.zx_color, props.minus_zx_color, props.custom_zx_color
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_preview": {props.show_preview},\n'
        new_dict += f'    "show_guide": {props.show_guide},\n'
        new_dict += f'    "torus_color": ({c[0]:.4f}, {c[1]:.4f}, {c[2]:.4f}, {c[3]:.4f}),\n'
        new_dict += f'    "torus_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
        new_dict += f'    "torus_rot": ({r[0]:.4f}, {r[1]:.4f}, {r[2]:.4f}),\n'
        new_dict += f'    "base_shape": "{props.base_shape}",\n'
        new_dict += f'    "torus_plane": "{props.torus_plane}",\n'
        new_dict += f'    "size_x": {props.size_x:.4f},\n'
        new_dict += f'    "size_y": {props.size_y:.4f},\n'
        new_dict += f'    "corner_radius": {props.corner_radius:.4f},\n'
        new_dict += f'    "minor_radius": {props.minor_radius:.4f},\n'
        new_dict += f'    "major_segments": {props.major_segments},\n'
        new_dict += f'    "corner_segments": {props.corner_segments},\n'
        new_dict += f'    "minor_segments": {props.minor_segments},\n'
        new_dict += f'    "cyl_thickness": {props.cyl_thickness:.4f},\n'
        new_dict += f'    "cyl_color": ({cc[0]:.4f}, {cc[1]:.4f}, {cc[2]:.4f}, {cc[3]:.4f}),\n'
        new_dict += f'    "zx_color": ({zc[0]:.4f}, {zc[1]:.4f}, {zc[2]:.4f}, {zc[3]:.4f}),\n'
        new_dict += f'    "minus_zx_color": ({mzc[0]:.4f}, {mzc[1]:.4f}, {mzc[2]:.4f}, {mzc[3]:.4f}),\n'
        new_dict += f'    "custom_zx_color": ({czc[0]:.4f}, {czc[1]:.4f}, {czc[2]:.4f}, {czc[3]:.4f}),\n'
        new_dict += f'    "cyl_slope_a": {props.cyl_slope_a:.4f},\n'
        new_dict += f'    "cyl_offset": {props.cyl_offset:.4f},\n'
        new_dict += f'    "cyl_limit": {props.cyl_limit:.4f},\n'
        new_dict += f'    "cyl_custom_b": {props.cyl_custom_b:.4f},\n'
        new_dict += f'    "show_cyl_group1": {props.show_cyl_group1},\n'
        new_dict += f'    "show_cyl_group2": {props.show_cyl_group2},\n'
        new_dict += f'    "show_cyl_group3": {props.show_cyl_group3},\n'
        new_dict += f'    "show_cyl_group4": {props.show_cyl_group4},\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
            if SOURCE_ID_TAG not in final_code: return {'CANCELLED'}
            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 safely!")
        except Exception as e: return {'CANCELLED'}
        return {'FINISHED'}

class OT_Reset(Operator):
    bl_idname = f"{OP_PREFIX}.reset"; bl_label = "Reset Transform"
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME)
        p.torus_loc = (0,0,0); p.torus_rot = (0,0,0); p.torus_plane = 'XY'; p.base_shape = 'SQUARE'
        p.size_x = 10.0; p.size_y = 5.0; p.corner_radius = 0.0; p.minor_radius = 0.5
        p.cyl_thickness = 0.5; p.cyl_color = (0.1, 0.6, 0.9, 1.0)
        p.zx_color = (0.9, 0.2, 0.2, 1.0); p.minus_zx_color = (0.9, 0.8, 0.2, 1.0)
        p.custom_zx_color = (0.2, 0.8, 0.9, 1.0)
        p.cyl_slope_a = 0.6; p.cyl_offset = 10.0; p.cyl_limit = 50.0; p.cyl_custom_b = -1.6
        p.show_cyl_group1 = True; p.show_cyl_group2 = True
        p.show_cyl_group3 = True; p.show_cyl_group4 = True
        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 = "Remove Addon"
    def execute(self, context):
        bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
        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: layout.label(text="Reload Script"); return
        row = layout.row(); row.scale_y = 1.2
        row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
        layout.separator()
        layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
        box = layout.box()
        if not props.show_preview: box.label(text="Preview is Hidden", icon='INFO')
        box.prop(props, "torus_color")
        col = box.column(align=True)
        col.prop(props, "base_shape"); col.prop(props, "torus_plane")
        col.separator()
        col.prop(props, "torus_loc"); col.prop(props, "torus_rot")
        box.separator()
        box.prop(props, "show_guide", icon='MESH_GRID', text="Show Guide Wire")
        
        col_s = box.column(align=True)
        if props.base_shape == 'ELLIPSE': col_s.prop(props, "size_x", text="Size X"); col_s.prop(props, "size_y", text="Size Y")
        else: col_s.prop(props, "size_x", text="Size")
        
        row_cr = col_s.row(); row_cr.enabled = (props.base_shape == 'SQUARE')
        row_cr.prop(props, "corner_radius")
        if props.corner_radius <= 0.001 and props.base_shape == 'SQUARE': row_cr.label(text="[90° Mode]", icon='SNAP_VERTEX')
            
        col_s.prop(props, "minor_radius")
        row_seg = box.row()
        if props.base_shape in['CIRCLE', 'ELLIPSE']: row_seg.prop(props, "major_segments", text="Resolution")
        elif props.base_shape == 'SQUARE': row_seg.prop(props, "corner_segments", text="Corner Segs")
        else: row_seg.label(text="[Cube has fixed corners]")
        box.row().prop(props, "minor_segments")
        box.operator(OT_Reset.bl_idname, icon='LOOP_BACK')
        layout.separator()
        col_exec = layout.column(); col_exec.scale_y = 1.5
        icons = {'CUBE': 'MESH_CUBE', 'SQUARE': 'MESH_PLANE', 'CIRCLE': 'MESH_CIRCLE', 'ELLIPSE': 'MESH_CIRCLE'}
        texts = {'CUBE': "Create Cube Frame", 'SQUARE': "Create Square Torus", 'CIRCLE': "Create Circle Torus", 'ELLIPSE': "Create Ellipse Torus"}
        col_exec.operator(OT_CreateTorus.bl_idname, icon=icons.get(props.base_shape, 'MESH_TORUS'), text=texts.get(props.base_shape, "Create Torus"))

class PT_EquationCylindersPanel(Panel):
    bl_label = "Equation Cylinders (6 Lines)"
    bl_idname = f"{PREFIX}_PT_eq_cylinders"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return
        
        a = props.cyl_slope_a
        C_val = props.cyl_offset
        b = props.cyl_custom_b
        
        box = layout.box()
        box.label(text="Math Equations (Y=0 Plane):", icon='FILE_TEXT')
        box.label(text=f"  G1: Z = {a:.2f} * X ± {C_val:.2f}")
        box.label(text=f"  G2: Z = X")
        box.label(text=f"  G3: Z = -X")
        box.label(text=f"  G4: Z = {b:.2f} * X")
        
        box.separator()
        col = box.column(align=True)
        col.prop(props, "cyl_slope_a", text="Slope (a)")
        col.prop(props, "cyl_offset", text="Offset (C)")
        col.prop(props, "cyl_limit", text="Limit Bounds (-L to L)")
        col.prop(props, "cyl_custom_b", text="Custom Slope (b)")
        
        box.separator()
        box.prop(props, "cyl_thickness", text="Thickness")
        
        # ★ カラー設定 & 表示トグル
        col_c = box.column(align=True)
        r1 = col_c.row(align=True)
        r1.prop(props, "show_cyl_group1", text="", icon='HIDE_OFF' if props.show_cyl_group1 else 'HIDE_ON')
        r1.prop(props, "cyl_color", text=f"Z = {a:.2f}X ± {C_val:.1f}")
        
        r2 = col_c.row(align=True)
        r2.prop(props, "show_cyl_group2", text="", icon='HIDE_OFF' if props.show_cyl_group2 else 'HIDE_ON')
        r2.prop(props, "zx_color", text="Z = X")
        
        r3 = col_c.row(align=True)
        r3.prop(props, "show_cyl_group3", text="", icon='HIDE_OFF' if props.show_cyl_group3 else 'HIDE_ON')
        r3.prop(props, "minus_zx_color", text="Z = -X")

        r4 = col_c.row(align=True)
        r4.prop(props, "show_cyl_group4", text="", icon='HIDE_OFF' if props.show_cyl_group4 else 'HIDE_ON')
        r4.prop(props, "custom_zx_color", text=f"Z = {b:.2f}X")
        
        # ★ 交点表示エリア (縦1行ずつ表示)
        box.separator()
        box_int = box.box()
        box_int.label(text=f"Intersections with Z={a:.2f}X ± {C_val:.1f} :", icon='DRIVER')
        
        # [ Z = X ] 側
        box_int.label(text="--- [ with Z = X ] ---")
        if abs(1.0 - a) < 0.0001: 
            box_int.label(text="  Parallel")
        else:
            x_minus = (-C_val) / (1.0 - a); x_plus  = C_val / (1.0 - a)
            box_int.label(text=f"  C=-{C_val:.1f} : X={x_minus:.2f}, Z={x_minus:.2f}")
            box_int.label(text=f"  C= 0.0 : X=0.00, Z=0.00")
            box_int.label(text=f"  C=+{C_val:.1f} : X={x_plus:.2f}, Z={x_plus:.2f}")
            
        box_int.separator()
        
        # [ Z = -X ] 側
        box_int.label(text="--- [ with Z = -X ] ---")
        if abs(-1.0 - a) < 0.0001: 
            box_int.label(text="  Parallel")
        else:
            xm_minus = (-C_val) / (-1.0 - a); xm_plus  = C_val / (-1.0 - a)
            box_int.label(text=f"  C=-{C_val:.1f} : X={xm_minus:.2f}, Z={-xm_minus:.2f}")
            box_int.label(text=f"  C= 0.0 : X=0.00, Z=0.00")
            box_int.label(text=f"  C=+{C_val:.1f} : X={xm_plus:.2f}, Z={-xm_plus:.2f}")
            
        box_int.separator()
        
        # [ Z = b*X ] 側
        box_int.label(text=f"--- [ with Z = {b:.2f}X ] ---")
        d = b - a
        if abs(d) < 0.0001: 
            box_int.label(text="  Parallel")
        else:
            xb_minus = (-C_val) / d; xb_plus = C_val / d
            box_int.label(text=f"  C=-{C_val:.1f} : X={xb_minus:.2f}, Z={b*xb_minus:.2f}")
            box_int.label(text=f"  C= 0.0 : X=0.00, Z=0.00")
            box_int.label(text=f"  C=+{C_val:.1f} : X={xb_plus:.2f}, Z={b*xb_plus:.2f}")
            
        box_int.operator(OT_CopyIntersectionInfo.bl_idname, icon='COPYDOWN', text="Copy Intersections")
        col_exec = box.column(); col_exec.scale_y = 1.2
        col_exec.operator(OT_CreateEquationCylinders.bl_idname, icon='MESH_CYLINDER', text="Create 6 Cylinders")

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_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        for l in ADDON_LINKS: self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"]).url = l["url"]

class PT_RemovePanel(Panel):
    bl_label = "System"; bl_idname = f"{PREFIX}_PT_remove"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): self.layout.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Remove Addon")

# ==============================================================================
#  REGISTER
# ==============================================================================
classes = (
    PG_TorusProps, 
    OT_CreateTorus, 
    OT_CreateEquationCylinders, 
    OT_CopyIntersectionInfo, 
    OT_CopyFullScript, 
    OT_Reset, 
    OT_OpenUrl, 
    OT_RemoveAddon, 
    PT_MainPanel, 
    PT_EquationCylindersPanel, 
    PT_LinksPanel, 
    PT_RemovePanel
)

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_TorusProps))
    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-03-24 21:03:00
import bpy
import bmesh
import webbrowser
import math
import mathutils
import time
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime

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

PREFIX       = "ShapeTorus20260324"
ADDON_NAME   = "zionad 520[ Shape-Torus ]"
TAB_NAME     = "[ Shape Torus copy ]   "
PANEL_TITLE  = "Multi-Shape Generator"
AUTHOR       = "zionadchat"

# ★ このスクリプト自身のID
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SHAPE_TORUS_2026_03_24_V9_MULTI_SHAPE ###"

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

bl_info = {
    "name": f"{ADDON_NAME} {PREFIX}",
    "author": AUTHOR,
    "version": (9, 8, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": f"Multi-Shape Torus & Equation Cylinders - {PREFIX}",
    "category": "3D View",
}

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

# ★ リンク設定
ADDON_LINKS = (
    {"label": "時空図 交点 2060407", "url": "<https://www.notion.so/2060407-33af5dacaf43808d86bbf0d54d4d0dd5>"},
    {"label": "Prefix トーラス正方形 20260324", "url": "<https://www.notion.so/Prefix-20260324-32df5dacaf4380528980db6a989d6306>"},
)

# ==============================================================================
#  デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_preview": True,
    "show_guide": True,
    "torus_color": (0.0391, 0.8000, 0.1647, 1.0000),
    "torus_loc": (0.0000, 0.0000, 0.0000),
    "torus_rot": (0.0000, 0.0000, 0.0000),
    "base_shape": "SQUARE",
    "torus_plane": "XY",
    "size_x": 10.0000,
    "size_y": 5.0000,
    "corner_radius": 0.0000,
    "minor_radius": 0.5000,
    "major_segments": 32,
    "corner_segments": 8,
    "minor_segments": 16,
    "cyl_thickness": 0.5000,
    "cyl_color": (0.1000, 0.6000, 0.9000, 1.0000),
    "zx_color": (0.9000, 0.2000, 0.2000, 1.0000),
    "minus_zx_color": (0.9000, 0.8000, 0.2000, 1.0000),
    "custom_zx_color": (0.2000, 0.8000, 0.9000, 1.0000),
    "cyl_slope_denom": 0.6000,
    "cyl_offset": 6.0000,
    "cyl_limit": 50.0000,
    "cyl_custom_denom": -1.6000,
    "show_cyl_group1": True,
    "show_cyl_group2": True,
    "show_cyl_group3": True,
    "show_cyl_group4": True,
}
# <END_DICT>

# ==============================================================================
#  データ クリーンアップ管理
# ==============================================================================

PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_OBJ_NAME = f"[Preview] Shape_{PREFIX}"
PREVIEW_GUIDE_NAME = f"[Preview] Guide_{PREFIX}"
PREVIEW_MAT_NAME = f"PreviewMat_{PREFIX}"

def cleanup_preview_data():
    for name in[PREVIEW_OBJ_NAME, PREVIEW_GUIDE_NAME]:
        obj = bpy.data.objects.get(name)
        if obj:
            mesh = obj.data
            bpy.data.objects.remove(obj, do_unlink=True)
            if mesh and mesh.users == 0: bpy.data.meshes.remove(mesh)
                
    meshes_to_remove =[m for m in bpy.data.meshes if m.name.startswith(f"PreviewMesh_{PREFIX}")]
    for m in meshes_to_remove:
        if m.users == 0: bpy.data.meshes.remove(m)
    mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
    if mat and mat.users == 0: bpy.data.materials.remove(mat)
    col = bpy.data.collections.get(PREVIEW_COL_NAME)
    if col and len(col.objects) == 0: bpy.data.collections.remove(col)

def cleanup_old_materials(prefix="Mat_UniqueShape", limit=50):
    mats =[m for m in bpy.data.materials if m.name.startswith(prefix)]
    if len(mats) > limit:
        for m in mats[:-limit]:
            if m.users == 0: bpy.data.materials.remove(m)

# ==============================================================================
#  ジオメトリ エンジン
# ==============================================================================

def create_square_guide_bmesh(bm, size):
    S = size / 2.0
    v1 = bm.verts.new((S, S, 0)); v2 = bm.verts.new((-S, S, 0))
    v3 = bm.verts.new((-S, -S, 0)); v4 = bm.verts.new((S, -S, 0))
    bm.verts.ensure_lookup_table()
    bm.edges.new((v1, v2)); bm.edges.new((v2, v3)); bm.edges.new((v3, v4)); bm.edges.new((v4, v1))
    return bm

def create_cube_guide_bmesh(bm, size):
    geom = bmesh.ops.create_cube(bm, size=size)
    faces =[f for f in bm.faces]
    bmesh.ops.delete(bm, geom=faces, context='FACES_ONLY')
    return bm

def create_ellipse_guide_bmesh(bm, size_x, size_y, segments=64):
    a = size_x / 2.0; b = size_y / 2.0
    verts =[]
    for i in range(segments):
        t = i * 2.0 * math.pi / segments
        verts.append(bm.verts.new((a * math.cos(t), b * math.sin(t), 0)))
    bm.verts.ensure_lookup_table()
    for i in range(segments): bm.edges.new((verts[i], verts[(i + 1) % segments]))
    return bm

def create_ellipse_torus_bmesh(bm, size_x, size_y, minor_radius, major_segments, minor_segments):
    a = size_x / 2.0; b = size_y / 2.0
    rings =[]
    for i in range(major_segments):
        t = i * 2.0 * math.pi / major_segments
        p = mathutils.Vector((a * math.cos(t), b * math.sin(t), 0))
        n = mathutils.Vector((b * math.cos(t), a * math.sin(t), 0)).normalized()
        up = mathutils.Vector((0, 0, 1))
        ring =[]
        for j in range(minor_segments):
            theta = j * 2.0 * math.pi / minor_segments
            offset = n * (minor_radius * math.cos(theta)) + up * (minor_radius * math.sin(theta))
            ring.append(bm.verts.new(p + offset))
        rings.append(ring)
    bm.verts.ensure_lookup_table()
    edge_loops = []
    for ring in rings:
        edges =[]
        for j in range(minor_segments): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
        edge_loops.append(edges)
    bm.edges.ensure_lookup_table()
    for i in range(major_segments):
        try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % major_segments])
        except Exception: pass
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm

def create_square_torus_bmesh(bm, size, corner_radius, minor_radius, corner_segments, minor_segments):
    half_size = size / 2.0
    actual_corner_radius = min(max(corner_radius, 0.0), half_size)
    rings =[]; EPS = 1e-6
    if actual_corner_radius < EPS:
        L = half_size
        corners =[
            (mathutils.Vector((L, L, 0)), mathutils.Vector((1, 1, 0)).normalized()),
            (mathutils.Vector((-L, L, 0)), mathutils.Vector((-1, 1, 0)).normalized()),
            (mathutils.Vector((-L, -L, 0)), mathutils.Vector((-1, -1, 0)).normalized()),
            (mathutils.Vector((L, -L, 0)), mathutils.Vector((1, -1, 0)).normalized())
        ]
        scale_xy = 1.0 / math.cos(math.pi / 4)
        for p, n in corners:
            b = mathutils.Vector((0, 0, 1))
            ring =[]
            for j in range(minor_segments):
                theta = j * 2.0 * math.pi / minor_segments
                offset = n * (minor_radius * math.cos(theta) * scale_xy) + b * (minor_radius * math.sin(theta))
                ring.append(bm.verts.new(p + offset))
            rings.append(ring)
    else:
        L = half_size - actual_corner_radius
        pts = []
        for q in range(4):
            cx = L if q in[0, 3] else -L
            cy = L if q in [0, 1] else -L
            for i in range(corner_segments + 1):
                angle = q * (math.pi / 2) + i * (math.pi / 2) / corner_segments
                pts.append((mathutils.Vector((cx + actual_corner_radius * math.cos(angle), cy + actual_corner_radius * math.sin(angle), 0)), mathutils.Vector((math.cos(angle), math.sin(angle), 0))))
        unique_pts =[]
        for p, n in pts:
            if not unique_pts or (unique_pts[-1][0] - p).length > EPS: unique_pts.append((p, n))
        if len(unique_pts) > 1 and (unique_pts[-1][0] - unique_pts[0][0]).length < EPS: unique_pts.pop()
        for p, n in unique_pts:
            b = mathutils.Vector((0, 0, 1))
            ring =[]
            for j in range(minor_segments):
                theta = j * 2.0 * math.pi / minor_segments
                ring.append(bm.verts.new(p + n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))))
            rings.append(ring)
    bm.verts.ensure_lookup_table()
    total_rings = len(rings)
    if total_rings < 3: return bm
    edge_loops = []
    for ring in rings:
        edges =[]
        for j in range(minor_segments): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
        edge_loops.append(edges)
    bm.edges.ensure_lookup_table()
    for i in range(total_rings):
        try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % total_rings])
        except Exception: pass
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm

def create_cube_framework_bmesh(bm, size, minor_radius, minor_segments):
    L = size / 2.0
    verts_co =[
        mathutils.Vector(( L,  L,  L)), mathutils.Vector((-L,  L,  L)),
        mathutils.Vector((-L, -L,  L)), mathutils.Vector(( L, -L,  L)),
        mathutils.Vector(( L,  L, -L)), mathutils.Vector((-L,  L, -L)),
        mathutils.Vector((-L, -L, -L)), mathutils.Vector(( L, -L, -L)),
    ]
    edges_idx =[(0,1), (1,2), (2,3), (3,0), (4,5), (5,6), (6,7), (7,4), (0,4), (1,5), (2,6), (3,7)]
    for co in verts_co:
        geom = bmesh.ops.create_uvsphere(bm, u_segments=minor_segments, v_segments=max(minor_segments//2, 3), radius=minor_radius)
        bmesh.ops.translate(bm, verts=geom['verts'], vec=co)
    for idx1, idx2 in edges_idx:
        v1 = verts_co[idx1]; v2 = verts_co[idx2]
        dist = (v1 - v2).length
        geom = bmesh.ops.create_cone(bm, cap_ends=False, cap_tris=False, segments=minor_segments, radius1=minor_radius, radius2=minor_radius, depth=dist)
        axis = (v1 - v2).normalized()
        bmesh.ops.transform(bm, matrix=mathutils.Vector((0,0,1)).rotation_difference(axis).to_matrix().to_4x4(), verts=geom['verts'])
        bmesh.ops.translate(bm, verts=geom['verts'], vec=(v1 + v2) / 2.0)
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm

def apply_auto_smooth(mesh):
    if bpy.app.version < (4, 1, 0):
        try:
            if hasattr(mesh, "use_auto_smooth"):
                mesh.use_auto_smooth = True
                mesh.auto_smooth_angle = math.radians(30)
        except AttributeError: pass

# ==============================================================================
#  計算ロジック(直線の方程式をボックス制限内でクリッピング)
# ==============================================================================
def get_line_segment_in_bounds(M, C, limit):
    pts = []
    eps = 1e-4
    z1 = M * (-limit) + C
    if -limit - eps <= z1 <= limit + eps: pts.append((-limit, z1))
    z2 = M * limit + C
    if -limit - eps <= z2 <= limit + eps: pts.append((limit, z2))
    if abs(M) > 1e-6:
        x3 = (-limit - C) / M
        if -limit - eps <= x3 <= limit + eps: pts.append((x3, -limit))
        x4 = (limit - C) / M
        if -limit - eps <= x4 <= limit + eps: pts.append((x4, limit))
    unique_pts = []
    for p in pts:
        if not any(abs(p[0] - up[0]) < eps and abs(p[1] - up[1]) < eps for up in unique_pts):
            unique_pts.append(p)
    if len(unique_pts) >= 2: return unique_pts[0], unique_pts[1]
    return None

def create_cylinder_line(M, C, limit, thickness, mat, target_collection):
    pts = get_line_segment_in_bounds(M, C, limit)
    if not pts: return None
    p1_2d, p2_2d = pts
    p1 = mathutils.Vector((p1_2d[0], 0.0, p1_2d[1]))
    p2 = mathutils.Vector((p2_2d[0], 0.0, p2_2d[1]))
    dist = (p2 - p1).length
    if dist < 1e-4: return None
    center = (p1 + p2) / 2.0
    bm = bmesh.new()
    geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=32, radius1=thickness, radius2=thickness, depth=dist)
    axis = (p2 - p1).normalized()
    up = mathutils.Vector((0, 0, 1))
    bmesh.ops.transform(bm, matrix=up.rotation_difference(axis).to_matrix().to_4x4(), verts=geom['verts'])
    bmesh.ops.translate(bm, verts=geom['verts'], vec=center)
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    mesh = bpy.data.meshes.new(f"EqCyl_Mesh")
    bm.to_mesh(mesh)
    bm.free(); apply_auto_smooth(mesh)
    obj = bpy.data.objects.new(f"EqLine_{datetime.now().strftime('%H%M%S')}", mesh)
    
    # ★ 指定されたコレクションにリンク
    target_collection.objects.link(obj)
    obj.data.materials.append(mat)
    return obj

# ==============================================================================
#  マテリアル・プレビュー制御
# ==============================================================================

def create_unique_material(color, name_prefix="Mat_UniqueShape"):
    mat_name = f"{name_prefix}_{datetime.now().strftime('%M%S%f')[:5]}"
    mat = bpy.data.materials.new(name=mat_name)
    mat.use_nodes = True; mat.blend_method = 'BLEND'
    if mat.use_nodes:
        tree = mat.node_tree
        tree.nodes.clear()
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        out.location = (300, 0)
        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]
    cleanup_old_materials(name_prefix)
    return mat

def get_or_create_preview_material():
    mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
    if not mat:
        mat = bpy.data.materials.new(name=PREVIEW_MAT_NAME)
        mat.use_nodes = True; mat.blend_method = 'BLEND'
    return mat

def update_preview_material(mat, color):
    if mat.use_nodes:
        bsdf = None
        for node in mat.node_tree.nodes:
            if node.type == 'BSDF_PRINCIPLED': bsdf = node; break
        if not bsdf:
            mat.node_tree.nodes.clear()
            bsdf = mat.node_tree.nodes.new("ShaderNodeBsdfPrincipled")
            out = mat.node_tree.nodes.new("ShaderNodeOutputMaterial")
            mat.node_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_transform_matrix(props):
    rot_matrix = mathutils.Matrix.Identity(4)
    if props.torus_plane == 'YZ': rot_matrix = mathutils.Matrix.Rotation(math.radians(90.0), 4, 'Y')
    elif props.torus_plane == 'ZX': rot_matrix = mathutils.Matrix.Rotation(math.radians(-90.0), 4, 'X')
    user_rot = mathutils.Euler((math.radians(props.torus_rot[0]), math.radians(props.torus_rot[1]), math.radians(props.torus_rot[2])), 'XYZ').to_matrix().to_4x4()
    loc_matrix = mathutils.Matrix.Translation(mathutils.Vector(props.torus_loc))
    return loc_matrix @ user_rot @ rot_matrix

def generate_shape_bmesh(bm, props):
    sx = min(max(props.size_x, 0.01), 10000.0); sy = min(max(props.size_y, 0.01), 10000.0)
    mr = min(max(props.minor_radius, 0.001), 5000.0)
    if props.base_shape == 'CUBE': create_cube_framework_bmesh(bm, sx, mr, props.minor_segments)
    elif props.base_shape == 'SQUARE': create_square_torus_bmesh(bm, sx, props.corner_radius, mr, props.corner_segments, props.minor_segments)
    elif props.base_shape == 'CIRCLE': create_ellipse_torus_bmesh(bm, sx, sx, mr, props.major_segments, props.minor_segments)
    elif props.base_shape == 'ELLIPSE': create_ellipse_torus_bmesh(bm, sx, sy, mr, props.major_segments, props.minor_segments)

def update_preview_geometry(context):
    props = getattr(context.scene, PROPS_NAME, None)
    if not props: 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)
    
    obj = bpy.data.objects.get(PREVIEW_OBJ_NAME); guide_obj = bpy.data.objects.get(PREVIEW_GUIDE_NAME)
    if not props.show_preview:
        if obj: bpy.data.objects.remove(obj, do_unlink=True)
        if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)
        return

    final_matrix = get_transform_matrix(props)
    scene_mesh_name = f"PreviewMesh_{PREFIX}_{context.scene.name}"
    bm = bmesh.new()
    try:
        generate_shape_bmesh(bm, props)
        bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
        mesh = bpy.data.meshes.get(scene_mesh_name)
        if not mesh: mesh = bpy.data.meshes.new(scene_mesh_name)
        else: mesh.clear_geometry()
        bm.to_mesh(mesh)
        apply_auto_smooth(mesh); mesh.update(calc_edges=True)
    finally: bm.free()

    if not obj:
        obj = bpy.data.objects.new(PREVIEW_OBJ_NAME, mesh)
        col.objects.link(obj)
    elif obj.data != mesh: obj.data = mesh

    mat = get_or_create_preview_material()
    update_preview_material(mat, props.torus_color)
    if not obj.data.materials: obj.data.materials.append(mat)
    else: obj.data.materials[0] = mat

    if props.show_guide:
        bm_g = bmesh.new()
        try:
            if props.base_shape == 'CUBE': create_cube_guide_bmesh(bm_g, props.size_x)
            elif props.base_shape == 'SQUARE': create_square_guide_bmesh(bm_g, props.size_x)
            elif props.base_shape == 'CIRCLE': create_ellipse_guide_bmesh(bm_g, props.size_x, props.size_x, segments=props.major_segments)
            elif props.base_shape == 'ELLIPSE': create_ellipse_guide_bmesh(bm_g, props.size_x, props.size_y, segments=props.major_segments)
            bmesh.ops.transform(bm_g, matrix=final_matrix, verts=bm_g.verts)
            guide_mesh_name = scene_mesh_name + "_Guide"
            mesh_g = bpy.data.meshes.get(guide_mesh_name)
            if not mesh_g: mesh_g = bpy.data.meshes.new(guide_mesh_name)
            else: mesh_g.clear_geometry()
            bm_g.to_mesh(mesh_g)
            mesh_g.update(calc_edges=True)
        finally: bm_g.free()
        if not guide_obj:
            guide_obj = bpy.data.objects.new(PREVIEW_GUIDE_NAME, mesh_g)
            col.objects.link(guide_obj)
        elif guide_obj.data != mesh_g: guide_obj.data = mesh_g
        guide_obj.display_type = 'WIRE'; guide_obj.show_in_front = True
    else:
        if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)

_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)

# ★ 表示/非表示トグル用のアップデート関数
def update_cyl_visibility(self, context):
    prefixes = {
        'show_cyl_group1': "EqLine_1_over_aX_",
        'show_cyl_group2': "EqLine_Z_eq_X_",
        'show_cyl_group3': "EqLine_Z_eq_minus_X_",
        'show_cyl_group4': "EqLine_Z_eq_custom_bX_"
    }
    for prop_name, prefix in prefixes.items():
        is_visible = getattr(self, prop_name)
        for obj in bpy.data.objects:
            if obj.name.startswith(prefix):
                obj.hide_viewport = not is_visible
                obj.hide_render = not is_visible

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

class PG_TorusProps(PropertyGroup):
    show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
    show_guide: BoolProperty(name="Show Guide", default=CURRENT_DEFAULTS['show_guide'], update=on_update)
    torus_color: FloatVectorProperty(name="Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['torus_color'], update=on_update)
    base_shape: EnumProperty(name="Shape", items=[('CUBE', "Cube", ""), ('SQUARE', "Square", ""), ('CIRCLE', "Circle", ""), ('ELLIPSE', "Ellipse", "")], default=CURRENT_DEFAULTS['base_shape'], update=on_update)
    torus_plane: EnumProperty(name="Plane", items=[('XY', "XY Plane", ""), ('YZ', "YZ Plane", ""), ('ZX', "ZX Plane", "")], default=CURRENT_DEFAULTS['torus_plane'], update=on_update)
    torus_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['torus_loc'], update=on_update)
    torus_rot: FloatVectorProperty(name="Rotation", size=3, default=CURRENT_DEFAULTS['torus_rot'], update=on_update)
    size_x: FloatProperty(name="Size", default=CURRENT_DEFAULTS['size_x'], min=0.1, max=10000.0, update=on_update)
    size_y: FloatProperty(name="Size Y", default=CURRENT_DEFAULTS['size_y'], min=0.1, max=10000.0, update=on_update)
    corner_radius: FloatProperty(name="Corner Radius", default=CURRENT_DEFAULTS['corner_radius'], min=0.0, max=5000.0, update=on_update)
    minor_radius: FloatProperty(name="Tube Thickness", default=CURRENT_DEFAULTS['minor_radius'], min=0.01, max=5000.0, update=on_update)
    major_segments: IntProperty(name="Resolution", default=CURRENT_DEFAULTS['major_segments'], min=3, soft_max=128, update=on_update)
    corner_segments: IntProperty(name="Corner Segs", default=CURRENT_DEFAULTS['corner_segments'], min=1, soft_max=128, update=on_update)
    minor_segments: IntProperty(name="Tube Segs", default=CURRENT_DEFAULTS['minor_segments'], min=3, soft_max=128, update=on_update)

    cyl_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['cyl_thickness'], min=0.01, max=50.0)
    cyl_color: FloatVectorProperty(name="Color (1/a*X ± C)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['cyl_color'])
    zx_color: FloatVectorProperty(name="Color (Z=X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['zx_color'])
    minus_zx_color: FloatVectorProperty(name="Color (Z=-X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['minus_zx_color'])
    custom_zx_color: FloatVectorProperty(name="Color Custom", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['custom_zx_color'])
    
    cyl_slope_denom: FloatProperty(name="Denominator (a)", default=CURRENT_DEFAULTS['cyl_slope_denom'], min=0.001)
    cyl_offset: FloatProperty(name="Offset (C)", default=CURRENT_DEFAULTS['cyl_offset'], min=0.0)
    cyl_limit: FloatProperty(name="Limit Bounds", default=CURRENT_DEFAULTS['cyl_limit'], min=1.0)
    cyl_custom_denom: FloatProperty(name="Custom Denom (b)", default=CURRENT_DEFAULTS['cyl_custom_denom'])
    
    show_cyl_group1: BoolProperty(name="Vis G1", default=CURRENT_DEFAULTS['show_cyl_group1'], update=update_cyl_visibility)
    show_cyl_group2: BoolProperty(name="Vis G2", default=CURRENT_DEFAULTS['show_cyl_group2'], update=update_cyl_visibility)
    show_cyl_group3: BoolProperty(name="Vis G3", default=CURRENT_DEFAULTS['show_cyl_group3'], update=update_cyl_visibility)
    show_cyl_group4: BoolProperty(name="Vis G4", default=CURRENT_DEFAULTS['show_cyl_group4'], update=update_cyl_visibility)

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

class OT_CreateTorus(Operator):
    bl_idname = f"{OP_PREFIX}.create_torus"; bl_label = "Create Shape Torus"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        bm = bmesh.new()
        generate_shape_bmesh(bm, props)
        final_matrix = get_transform_matrix(props)
        bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
        mesh = bpy.data.meshes.new(f"Shape_Mesh")
        bm.to_mesh(mesh)
        bm.free(); apply_auto_smooth(mesh)
        
        name_dict = {'CUBE': "CubeFrame", 'SQUARE': "SqTorus", 'CIRCLE': "CircTorus", 'ELLIPSE': "ElpsTorus"}
        prefix_name = name_dict.get(props.base_shape, "Shape")
        
        # ★ 専用コレクションの作成
        col_name = f"ShapeGroup_{datetime.now().strftime('%H%M%S')}"
        new_col = bpy.data.collections.new(col_name)
        context.scene.collection.children.link(new_col)
        
        obj = bpy.data.objects.new(f"{prefix_name}_{datetime.now().strftime('%H%M%S')}", mesh)
        new_col.objects.link(obj)
        
        unique_mat = create_unique_material(props.torus_color, "Mat_Unique")
        obj.data.materials.append(unique_mat)
        
        bpy.ops.object.select_all(action='DESELECT')
        obj.select_set(True)
        context.view_layer.objects.active = obj
        self.report({'INFO'}, f"Created {prefix_name} in collection '{col_name}'!")
        return {'FINISHED'}

class OT_CreateEquationCylinders(Operator):
    bl_idname = f"{OP_PREFIX}.create_equation_cylinders"
    bl_label = "Create 6 Cylinders"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        a = props.cyl_slope_denom; C_val = props.cyl_offset; limit = props.cyl_limit; thickness = props.cyl_thickness
        b = props.cyl_custom_denom
        
        if abs(a) < 0.0001:
            self.report({'ERROR'}, "Denominator 'a' is too close to zero!")
            return {'CANCELLED'}
            
        # ★ 専用コレクションの作成
        col_name = f"EqCylinders_{datetime.now().strftime('%H%M%S')}"
        new_col = bpy.data.collections.new(col_name)
        context.scene.collection.children.link(new_col)
            
        M = 1.0 / a
        mat_eq = create_unique_material(props.cyl_color, "Mat_EqLine")
        mat_zx = create_unique_material(props.zx_color, "Mat_ZXLine")
        mat_mzx = create_unique_material(props.minus_zx_color, "Mat_MinusZXLine")
        mat_custom = create_unique_material(props.custom_zx_color, "Mat_CustomZXLine")
        
        created_objs = []
        
        # 1. Z = (1/a)X ± C (3 lines)
        for C in [-C_val, 0.0, C_val]:
            obj = create_cylinder_line(M, C, limit, thickness, mat_eq, new_col)
            if obj:
                obj.name = f"EqLine_1_over_aX_C{C}_{datetime.now().strftime('%H%M%S')}"
                obj.hide_viewport = not props.show_cyl_group1
                obj.hide_render = not props.show_cyl_group1
                created_objs.append(obj)
                
        # 2. Z = X (1 line)
        obj_zx = create_cylinder_line(1.0, 0.0, limit, thickness, mat_zx, new_col)
        if obj_zx:
            obj_zx.name = f"EqLine_Z_eq_X_{datetime.now().strftime('%H%M%S')}"
            obj_zx.hide_viewport = not props.show_cyl_group2
            obj_zx.hide_render = not props.show_cyl_group2
            created_objs.append(obj_zx)
            
        # 3. Z = -X (1 line)
        obj_mzx = create_cylinder_line(-1.0, 0.0, limit, thickness, mat_mzx, new_col)
        if obj_mzx:
            obj_mzx.name = f"EqLine_Z_eq_minus_X_{datetime.now().strftime('%H%M%S')}"
            obj_mzx.hide_viewport = not props.show_cyl_group3
            obj_mzx.hide_render = not props.show_cyl_group3
            created_objs.append(obj_mzx)
            
        # 4. Z = (1/b)X (1 line)
        if abs(b) < 0.0001:
            self.report({'WARNING'}, "Custom Denominator 'b' is zero. Skipped creating Custom Line.")
        else:
            obj_custom = create_cylinder_line(1.0 / b, 0.0, limit, thickness, mat_custom, new_col)
            if obj_custom:
                obj_custom.name = f"EqLine_Z_eq_custom_bX_{datetime.now().strftime('%H%M%S')}"
                obj_custom.hide_viewport = not props.show_cyl_group4
                obj_custom.hide_render = not props.show_cyl_group4
                created_objs.append(obj_custom)
            
        bpy.ops.object.select_all(action='DESELECT')
        for obj in created_objs: obj.select_set(True)
        if created_objs:
            context.view_layer.objects.active = created_objs[-1]
            self.report({'INFO'}, f"Created {len(created_objs)} Cylinders in collection '{col_name}'!")
        else:
            self.report({'WARNING'}, "No cylinders created. Lines might be entirely outside the bounds.")
            
        return {'FINISHED'}

class OT_CopyIntersectionInfo(Operator):
    bl_idname = f"{OP_PREFIX}.copy_intersection"
    bl_label = "Copy Intersections"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        a = props.cyl_slope_denom; C_val = props.cyl_offset; b = props.cyl_custom_denom
        
        text = "Intersection Points:\n"
        text += f"Equations & Parameters:\n"
        text += f" Base Lines : Z = (1/{a:.4f})*X ± {C_val:.4f}\n\n"
        
        text += "[ Z = X  and  Base Lines ]\n"
        if abs(1.0 - a * 1.0) < 0.0001: text += " Lines are parallel.\n"
        else:
            x_m = (a * (-C_val)) / (1.0 - a); x_p = (a * C_val) / (1.0 - a)
            text += f" Line (C = -{C_val:.1f}): X = {x_m:.4f}, Z = {x_m:.4f}\n"
            text += f" Line (C =  0.0): X = 0.0000, Z = 0.0000\n"
            text += f" Line (C = +{C_val:.1f}): X = {x_p:.4f}, Z = {x_p:.4f}\n"
            
        text += "\n[ Z = -X  and  Base Lines ]\n"
        if abs(1.0 - a * (-1.0)) < 0.0001: text += " Lines are parallel.\n"
        else:
            xm_m = (a * (-C_val)) / (1.0 + a); xm_p = (a * C_val) / (1.0 + a)
            text += f" Line (C = -{C_val:.1f}): X = {xm_m:.4f}, Z = {-xm_m:.4f}\n"
            text += f" Line (C =  0.0): X = 0.0000, Z = 0.0000\n"
            text += f" Line (C = +{C_val:.1f}): X = {xm_p:.4f}, Z = {-xm_p:.4f}\n"

        text += f"\n[ Z = (1/{b:.4f})X  and  Base Lines ]\n"
        d = a - b
        if abs(b) < 0.0001: text += " Denominator 'b' is zero. Invalid.\n"
        elif abs(d) < 0.0001: text += " Lines are parallel.\n"
        else:
            xb_m = (a * b * (-C_val)) / d; xb_p = (a * b * C_val) / d
            text += f" Line (C = -{C_val:.1f}): X = {xb_m:.4f}, Z = {xb_m/b:.4f}\n"
            text += f" Line (C =  0.0): X = 0.0000, Z = 0.0000\n"
            text += f" Line (C = +{C_val:.1f}): X = {xb_p:.4f}, Z = {xb_p/b:.4f}\n"
            
        context.window_manager.clipboard = text
        self.report({'INFO'}, "Intersection Data Copied to Clipboard!")
        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()
        c, l, r = props.torus_color, props.torus_loc, props.torus_rot
        cc, zc, mzc, czc = props.cyl_color, props.zx_color, props.minus_zx_color, props.custom_zx_color
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_preview": {props.show_preview},\n'
        new_dict += f'    "show_guide": {props.show_guide},\n'
        new_dict += f'    "torus_color": ({c[0]:.4f}, {c[1]:.4f}, {c[2]:.4f}, {c[3]:.4f}),\n'
        new_dict += f'    "torus_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
        new_dict += f'    "torus_rot": ({r[0]:.4f}, {r[1]:.4f}, {r[2]:.4f}),\n'
        new_dict += f'    "base_shape": "{props.base_shape}",\n'
        new_dict += f'    "torus_plane": "{props.torus_plane}",\n'
        new_dict += f'    "size_x": {props.size_x:.4f},\n'
        new_dict += f'    "size_y": {props.size_y:.4f},\n'
        new_dict += f'    "corner_radius": {props.corner_radius:.4f},\n'
        new_dict += f'    "minor_radius": {props.minor_radius:.4f},\n'
        new_dict += f'    "major_segments": {props.major_segments},\n'
        new_dict += f'    "corner_segments": {props.corner_segments},\n'
        new_dict += f'    "minor_segments": {props.minor_segments},\n'
        new_dict += f'    "cyl_thickness": {props.cyl_thickness:.4f},\n'
        new_dict += f'    "cyl_color": ({cc[0]:.4f}, {cc[1]:.4f}, {cc[2]:.4f}, {cc[3]:.4f}),\n'
        new_dict += f'    "zx_color": ({zc[0]:.4f}, {zc[1]:.4f}, {zc[2]:.4f}, {zc[3]:.4f}),\n'
        new_dict += f'    "minus_zx_color": ({mzc[0]:.4f}, {mzc[1]:.4f}, {mzc[2]:.4f}, {mzc[3]:.4f}),\n'
        new_dict += f'    "custom_zx_color": ({czc[0]:.4f}, {czc[1]:.4f}, {czc[2]:.4f}, {czc[3]:.4f}),\n'
        new_dict += f'    "cyl_slope_denom": {props.cyl_slope_denom:.4f},\n'
        new_dict += f'    "cyl_offset": {props.cyl_offset:.4f},\n'
        new_dict += f'    "cyl_limit": {props.cyl_limit:.4f},\n'
        new_dict += f'    "cyl_custom_denom": {props.cyl_custom_denom:.4f},\n'
        new_dict += f'    "show_cyl_group1": {props.show_cyl_group1},\n'
        new_dict += f'    "show_cyl_group2": {props.show_cyl_group2},\n'
        new_dict += f'    "show_cyl_group3": {props.show_cyl_group3},\n'
        new_dict += f'    "show_cyl_group4": {props.show_cyl_group4},\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
            if SOURCE_ID_TAG not in final_code: return {'CANCELLED'}
            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 safely!")
        except Exception as e: return {'CANCELLED'}
        return {'FINISHED'}

class OT_Reset(Operator):
    bl_idname = f"{OP_PREFIX}.reset"; bl_label = "Reset Transform"
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME)
        p.torus_loc = (0,0,0); p.torus_rot = (0,0,0); p.torus_plane = 'XY'; p.base_shape = 'SQUARE'
        p.size_x = 10.0; p.size_y = 5.0; p.corner_radius = 0.0; p.minor_radius = 0.5
        p.cyl_thickness = 0.5; p.cyl_color = (0.1, 0.6, 0.9, 1.0)
        p.zx_color = (0.9, 0.2, 0.2, 1.0); p.minus_zx_color = (0.9, 0.8, 0.2, 1.0)
        p.custom_zx_color = (0.2, 0.8, 0.9, 1.0)
        p.cyl_slope_denom = 0.6; p.cyl_offset = 10.0; p.cyl_limit = 50.0; p.cyl_custom_denom = -1.6
        p.show_cyl_group1 = True; p.show_cyl_group2 = True
        p.show_cyl_group3 = True; p.show_cyl_group4 = True
        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 = "Remove Addon"
    def execute(self, context):
        bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
        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: layout.label(text="Reload Script"); return
        row = layout.row(); row.scale_y = 1.2
        row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
        layout.separator()
        layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
        box = layout.box()
        if not props.show_preview: box.label(text="Preview is Hidden", icon='INFO')
        box.prop(props, "torus_color")
        col = box.column(align=True)
        col.prop(props, "base_shape"); col.prop(props, "torus_plane")
        col.separator()
        col.prop(props, "torus_loc"); col.prop(props, "torus_rot")
        box.separator()
        box.prop(props, "show_guide", icon='MESH_GRID', text="Show Guide Wire")
        
        col_s = box.column(align=True)
        if props.base_shape == 'ELLIPSE': col_s.prop(props, "size_x", text="Size X"); col_s.prop(props, "size_y", text="Size Y")
        else: col_s.prop(props, "size_x", text="Size")
        
        row_cr = col_s.row(); row_cr.enabled = (props.base_shape == 'SQUARE')
        row_cr.prop(props, "corner_radius")
        if props.corner_radius <= 0.001 and props.base_shape == 'SQUARE': row_cr.label(text="[90° Mode]", icon='SNAP_VERTEX')
            
        col_s.prop(props, "minor_radius")
        row_seg = box.row()
        if props.base_shape in['CIRCLE', 'ELLIPSE']: row_seg.prop(props, "major_segments", text="Resolution")
        elif props.base_shape == 'SQUARE': row_seg.prop(props, "corner_segments", text="Corner Segs")
        else: row_seg.label(text="[Cube has fixed corners]")
        box.row().prop(props, "minor_segments")
        box.operator(OT_Reset.bl_idname, icon='LOOP_BACK')
        layout.separator()
        col_exec = layout.column(); col_exec.scale_y = 1.5
        icons = {'CUBE': 'MESH_CUBE', 'SQUARE': 'MESH_PLANE', 'CIRCLE': 'MESH_CIRCLE', 'ELLIPSE': 'MESH_CIRCLE'}
        texts = {'CUBE': "Create Cube Frame", 'SQUARE': "Create Square Torus", 'CIRCLE': "Create Circle Torus", 'ELLIPSE': "Create Ellipse Torus"}
        col_exec.operator(OT_CreateTorus.bl_idname, icon=icons.get(props.base_shape, 'MESH_TORUS'), text=texts.get(props.base_shape, "Create Torus"))

class PT_EquationCylindersPanel(Panel):
    bl_label = "Equation Cylinders (6 Lines)"
    bl_idname = f"{PREFIX}_PT_eq_cylinders"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return
        
        a = props.cyl_slope_denom
        C_val = props.cyl_offset
        b = props.cyl_custom_denom
        
        box = layout.box()
        box.label(text="Math Equations (Y=0 Plane):", icon='FILE_TEXT')
        box.label(text=f"  G1: Z = ( 1 / {a:.2f} ) * X ± {C_val:.2f}")
        box.label(text=f"  G2: Z = X")
        box.label(text=f"  G3: Z = -X")
        box.label(text=f"  G4: Z = ( 1 / {b:.2f} ) * X")
        
        box.separator()
        col = box.column(align=True)
        col.prop(props, "cyl_slope_denom", text="Denominator (a)")
        col.prop(props, "cyl_offset", text="Offset (C)")
        col.prop(props, "cyl_limit", text="Limit Bounds (-L to L)")
        col.prop(props, "cyl_custom_denom", text="Custom Denom (b)")
        
        box.separator()
        box.prop(props, "cyl_thickness", text="Thickness")
        
        # ★ カラー設定 & 表示トグル
        col_c = box.column(align=True)
        r1 = col_c.row(align=True)
        r1.prop(props, "show_cyl_group1", text="", icon='HIDE_OFF' if props.show_cyl_group1 else 'HIDE_ON')
        r1.prop(props, "cyl_color", text=f"Z=(1/{a:.2f})X ± {C_val:.1f}")
        
        r2 = col_c.row(align=True)
        r2.prop(props, "show_cyl_group2", text="", icon='HIDE_OFF' if props.show_cyl_group2 else 'HIDE_ON')
        r2.prop(props, "zx_color", text="Z = X")
        
        r3 = col_c.row(align=True)
        r3.prop(props, "show_cyl_group3", text="", icon='HIDE_OFF' if props.show_cyl_group3 else 'HIDE_ON')
        r3.prop(props, "minus_zx_color", text="Z = -X")

        r4 = col_c.row(align=True)
        r4.prop(props, "show_cyl_group4", text="", icon='HIDE_OFF' if props.show_cyl_group4 else 'HIDE_ON')
        r4.prop(props, "custom_zx_color", text=f"Z = (1/{b:.2f})X")
        
        # ★ 交点表示エリア (縦1行ずつ表示)
        box.separator()
        box_int = box.box()
        box_int.label(text=f"Intersections with Z=(1/{a:.2f})X ± {C_val:.1f} :", icon='DRIVER')
        
        # [ Z = X ] 側
        box_int.label(text="--- [ with Z = X ] ---")
        if abs(1.0 - a * 1.0) < 0.0001: 
            box_int.label(text="  Parallel")
        else:
            x_minus = (a * (-C_val)) / (1.0 - a); x_plus  = (a * C_val) / (1.0 - a)
            box_int.label(text=f"  C=-{C_val:.1f} : X={x_minus:.2f}, Z={x_minus:.2f}")
            box_int.label(text=f"  C= 0.0 : X=0.00, Z=0.00")
            box_int.label(text=f"  C=+{C_val:.1f} : X={x_plus:.2f}, Z={x_plus:.2f}")
            
        box_int.separator()
        
        # [ Z = -X ] 側
        box_int.label(text="--- [ with Z = -X ] ---")
        if abs(1.0 - a * (-1.0)) < 0.0001: 
            box_int.label(text="  Parallel")
        else:
            xm_minus = (a * (-C_val)) / (1.0 + a); xm_plus  = (a * C_val) / (1.0 + a)
            box_int.label(text=f"  C=-{C_val:.1f} : X={xm_minus:.2f}, Z={-xm_minus:.2f}")
            box_int.label(text=f"  C= 0.0 : X=0.00, Z=0.00")
            box_int.label(text=f"  C=+{C_val:.1f} : X={xm_plus:.2f}, Z={-xm_plus:.2f}")
            
        box_int.separator()
        
        # [ Z = (1/b)X ] 側
        box_int.label(text=f"--- [ with Z = (1/{b:.2f})X ] ---")
        d = a - b
        if abs(b) < 0.0001: 
            box_int.label(text="  Invalid (b=0)")
        elif abs(d) < 0.0001: 
            box_int.label(text="  Parallel")
        else:
            xb_minus = (a * b * (-C_val)) / d; xb_plus = (a * b * C_val) / d
            box_int.label(text=f"  C=-{C_val:.1f} : X={xb_minus:.2f}, Z={xb_minus/b:.2f}")
            box_int.label(text=f"  C= 0.0 : X=0.00, Z=0.00")
            box_int.label(text=f"  C=+{C_val:.1f} : X={xb_plus:.2f}, Z={xb_plus/b:.2f}")
            
        box_int.operator(OT_CopyIntersectionInfo.bl_idname, icon='COPYDOWN', text="Copy Intersections")
        col_exec = box.column(); col_exec.scale_y = 1.2
        col_exec.operator(OT_CreateEquationCylinders.bl_idname, icon='MESH_CYLINDER', text="Create 6 Cylinders")

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_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        for l in ADDON_LINKS: self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"]).url = l["url"]

class PT_RemovePanel(Panel):
    bl_label = "System"; bl_idname = f"{PREFIX}_PT_remove"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): self.layout.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Remove Addon")

# ==============================================================================
#  REGISTER
# ==============================================================================
classes = (
    PG_TorusProps, 
    OT_CreateTorus, 
    OT_CreateEquationCylinders, 
    OT_CopyIntersectionInfo, 
    OT_CopyFullScript, 
    OT_Reset, 
    OT_OpenUrl, 
    OT_RemoveAddon, 
    PT_MainPanel, 
    PT_EquationCylindersPanel, 
    PT_LinksPanel, 
    PT_RemovePanel
)

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_TorusProps))
    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-03-24 21:03:00
import bpy
import bmesh
import webbrowser
import math
import mathutils
import time
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime

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

PREFIX       = "ShapeTorus20260324"
ADDON_NAME   = "zionad 520[ Shape-Torus ]"
TAB_NAME     = "[ Shape Torus copy ]   "
PANEL_TITLE  = "Multi-Shape Generator"
AUTHOR       = "zionadchat"

# ★ このスクリプト自身のID
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SHAPE_TORUS_2026_03_24_V9_MULTI_SHAPE ###"

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

bl_info = {
    "name": f"{ADDON_NAME} {PREFIX}",
    "author": AUTHOR,
    "version": (9, 7, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": f"Multi-Shape Torus & Equation Cylinders - {PREFIX}",
    "category": "3D View",
}

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

# ★ リンク設定
ADDON_LINKS = (
    {"label": "時空図 交点 2060407", "url": "<https://www.notion.so/2060407-33af5dacaf43808d86bbf0d54d4d0dd5>"},
    {"label": "Prefix トーラス正方形 20260324", "url": "<https://www.notion.so/Prefix-20260324-32df5dacaf4380528980db6a989d6306>"},
)

# ==============================================================================
#  デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_preview": True,
    "show_guide": True,
    "torus_color": (0.0391, 0.8000, 0.1647, 1.0000),
    "torus_loc": (0.0000, 0.0000, 0.0000),
    "torus_rot": (0.0000, 0.0000, 0.0000),
    "base_shape": "SQUARE",
    "torus_plane": "XY",
    "size_x": 10.0000,
    "size_y": 5.0000,
    "corner_radius": 0.0000,
    "minor_radius": 0.5000,
    "major_segments": 32,
    "corner_segments": 8,
    "minor_segments": 16,
    "cyl_thickness": 0.5000,
    "cyl_color": (0.1000, 0.6000, 0.9000, 1.0000),
    "zx_color": (0.9000, 0.2000, 0.2000, 1.0000),
    "minus_zx_color": (0.9000, 0.8000, 0.2000, 1.0000),
    "custom_zx_color": (0.2000, 0.8000, 0.9000, 1.0000),
    "cyl_slope_denom": 0.6000,
    "cyl_offset": 10.0000,
    "cyl_limit": 50.0000,
    "cyl_custom_denom": -1.6000,
    "show_cyl_group1": True,
    "show_cyl_group2": True,
    "show_cyl_group3": True,
    "show_cyl_group4": True,
}
# <END_DICT>

# ==============================================================================
#  データ クリーンアップ管理
# ==============================================================================

PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_OBJ_NAME = f"[Preview] Shape_{PREFIX}"
PREVIEW_GUIDE_NAME = f"[Preview] Guide_{PREFIX}"
PREVIEW_MAT_NAME = f"PreviewMat_{PREFIX}"

def cleanup_preview_data():
    for name in[PREVIEW_OBJ_NAME, PREVIEW_GUIDE_NAME]:
        obj = bpy.data.objects.get(name)
        if obj:
            mesh = obj.data
            bpy.data.objects.remove(obj, do_unlink=True)
            if mesh and mesh.users == 0: bpy.data.meshes.remove(mesh)
                
    meshes_to_remove =[m for m in bpy.data.meshes if m.name.startswith(f"PreviewMesh_{PREFIX}")]
    for m in meshes_to_remove:
        if m.users == 0: bpy.data.meshes.remove(m)
    mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
    if mat and mat.users == 0: bpy.data.materials.remove(mat)
    col = bpy.data.collections.get(PREVIEW_COL_NAME)
    if col and len(col.objects) == 0: bpy.data.collections.remove(col)

def cleanup_old_materials(prefix="Mat_UniqueShape", limit=50):
    mats =[m for m in bpy.data.materials if m.name.startswith(prefix)]
    if len(mats) > limit:
        for m in mats[:-limit]:
            if m.users == 0: bpy.data.materials.remove(m)

# ==============================================================================
#  ジオメトリ エンジン
# ==============================================================================

def create_square_guide_bmesh(bm, size):
    S = size / 2.0
    v1 = bm.verts.new((S, S, 0)); v2 = bm.verts.new((-S, S, 0))
    v3 = bm.verts.new((-S, -S, 0)); v4 = bm.verts.new((S, -S, 0))
    bm.verts.ensure_lookup_table()
    bm.edges.new((v1, v2)); bm.edges.new((v2, v3)); bm.edges.new((v3, v4)); bm.edges.new((v4, v1))
    return bm

def create_cube_guide_bmesh(bm, size):
    geom = bmesh.ops.create_cube(bm, size=size)
    faces =[f for f in bm.faces]
    bmesh.ops.delete(bm, geom=faces, context='FACES_ONLY')
    return bm

def create_ellipse_guide_bmesh(bm, size_x, size_y, segments=64):
    a = size_x / 2.0; b = size_y / 2.0
    verts =[]
    for i in range(segments):
        t = i * 2.0 * math.pi / segments
        verts.append(bm.verts.new((a * math.cos(t), b * math.sin(t), 0)))
    bm.verts.ensure_lookup_table()
    for i in range(segments): bm.edges.new((verts[i], verts[(i + 1) % segments]))
    return bm

def create_ellipse_torus_bmesh(bm, size_x, size_y, minor_radius, major_segments, minor_segments):
    a = size_x / 2.0; b = size_y / 2.0
    rings =[]
    for i in range(major_segments):
        t = i * 2.0 * math.pi / major_segments
        p = mathutils.Vector((a * math.cos(t), b * math.sin(t), 0))
        n = mathutils.Vector((b * math.cos(t), a * math.sin(t), 0)).normalized()
        up = mathutils.Vector((0, 0, 1))
        ring =[]
        for j in range(minor_segments):
            theta = j * 2.0 * math.pi / minor_segments
            offset = n * (minor_radius * math.cos(theta)) + up * (minor_radius * math.sin(theta))
            ring.append(bm.verts.new(p + offset))
        rings.append(ring)
    bm.verts.ensure_lookup_table()
    edge_loops = []
    for ring in rings:
        edges =[]
        for j in range(minor_segments): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
        edge_loops.append(edges)
    bm.edges.ensure_lookup_table()
    for i in range(major_segments):
        try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % major_segments])
        except Exception: pass
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm

def create_square_torus_bmesh(bm, size, corner_radius, minor_radius, corner_segments, minor_segments):
    half_size = size / 2.0
    actual_corner_radius = min(max(corner_radius, 0.0), half_size)
    rings =[]; EPS = 1e-6
    if actual_corner_radius < EPS:
        L = half_size
        corners =[
            (mathutils.Vector((L, L, 0)), mathutils.Vector((1, 1, 0)).normalized()),
            (mathutils.Vector((-L, L, 0)), mathutils.Vector((-1, 1, 0)).normalized()),
            (mathutils.Vector((-L, -L, 0)), mathutils.Vector((-1, -1, 0)).normalized()),
            (mathutils.Vector((L, -L, 0)), mathutils.Vector((1, -1, 0)).normalized())
        ]
        scale_xy = 1.0 / math.cos(math.pi / 4)
        for p, n in corners:
            b = mathutils.Vector((0, 0, 1))
            ring =[]
            for j in range(minor_segments):
                theta = j * 2.0 * math.pi / minor_segments
                offset = n * (minor_radius * math.cos(theta) * scale_xy) + b * (minor_radius * math.sin(theta))
                ring.append(bm.verts.new(p + offset))
            rings.append(ring)
    else:
        L = half_size - actual_corner_radius
        pts = []
        for q in range(4):
            cx = L if q in[0, 3] else -L
            cy = L if q in [0, 1] else -L
            for i in range(corner_segments + 1):
                angle = q * (math.pi / 2) + i * (math.pi / 2) / corner_segments
                pts.append((mathutils.Vector((cx + actual_corner_radius * math.cos(angle), cy + actual_corner_radius * math.sin(angle), 0)), mathutils.Vector((math.cos(angle), math.sin(angle), 0))))
        unique_pts =[]
        for p, n in pts:
            if not unique_pts or (unique_pts[-1][0] - p).length > EPS: unique_pts.append((p, n))
        if len(unique_pts) > 1 and (unique_pts[-1][0] - unique_pts[0][0]).length < EPS: unique_pts.pop()
        for p, n in unique_pts:
            b = mathutils.Vector((0, 0, 1))
            ring =[]
            for j in range(minor_segments):
                theta = j * 2.0 * math.pi / minor_segments
                ring.append(bm.verts.new(p + n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))))
            rings.append(ring)
    bm.verts.ensure_lookup_table()
    total_rings = len(rings)
    if total_rings < 3: return bm
    edge_loops = []
    for ring in rings:
        edges =[]
        for j in range(minor_segments): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
        edge_loops.append(edges)
    bm.edges.ensure_lookup_table()
    for i in range(total_rings):
        try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % total_rings])
        except Exception: pass
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm

def create_cube_framework_bmesh(bm, size, minor_radius, minor_segments):
    L = size / 2.0
    verts_co =[
        mathutils.Vector(( L,  L,  L)), mathutils.Vector((-L,  L,  L)),
        mathutils.Vector((-L, -L,  L)), mathutils.Vector(( L, -L,  L)),
        mathutils.Vector(( L,  L, -L)), mathutils.Vector((-L,  L, -L)),
        mathutils.Vector((-L, -L, -L)), mathutils.Vector(( L, -L, -L)),
    ]
    edges_idx =[(0,1), (1,2), (2,3), (3,0), (4,5), (5,6), (6,7), (7,4), (0,4), (1,5), (2,6), (3,7)]
    for co in verts_co:
        geom = bmesh.ops.create_uvsphere(bm, u_segments=minor_segments, v_segments=max(minor_segments//2, 3), radius=minor_radius)
        bmesh.ops.translate(bm, verts=geom['verts'], vec=co)
    for idx1, idx2 in edges_idx:
        v1 = verts_co[idx1]; v2 = verts_co[idx2]
        dist = (v1 - v2).length
        geom = bmesh.ops.create_cone(bm, cap_ends=False, cap_tris=False, segments=minor_segments, radius1=minor_radius, radius2=minor_radius, depth=dist)
        axis = (v1 - v2).normalized()
        bmesh.ops.transform(bm, matrix=mathutils.Vector((0,0,1)).rotation_difference(axis).to_matrix().to_4x4(), verts=geom['verts'])
        bmesh.ops.translate(bm, verts=geom['verts'], vec=(v1 + v2) / 2.0)
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm

def apply_auto_smooth(mesh):
    if bpy.app.version < (4, 1, 0):
        try:
            if hasattr(mesh, "use_auto_smooth"):
                mesh.use_auto_smooth = True
                mesh.auto_smooth_angle = math.radians(30)
        except AttributeError: pass

# ==============================================================================
#  計算ロジック(直線の方程式をボックス制限内でクリッピング)
# ==============================================================================
def get_line_segment_in_bounds(M, C, limit):
    pts = []
    eps = 1e-4
    z1 = M * (-limit) + C
    if -limit - eps <= z1 <= limit + eps: pts.append((-limit, z1))
    z2 = M * limit + C
    if -limit - eps <= z2 <= limit + eps: pts.append((limit, z2))
    if abs(M) > 1e-6:
        x3 = (-limit - C) / M
        if -limit - eps <= x3 <= limit + eps: pts.append((x3, -limit))
        x4 = (limit - C) / M
        if -limit - eps <= x4 <= limit + eps: pts.append((x4, limit))
    unique_pts = []
    for p in pts:
        if not any(abs(p[0] - up[0]) < eps and abs(p[1] - up[1]) < eps for up in unique_pts):
            unique_pts.append(p)
    if len(unique_pts) >= 2: return unique_pts[0], unique_pts[1]
    return None

def create_cylinder_line(M, C, limit, thickness, mat, context):
    pts = get_line_segment_in_bounds(M, C, limit)
    if not pts: return None
    p1_2d, p2_2d = pts
    p1 = mathutils.Vector((p1_2d[0], 0.0, p1_2d[1]))
    p2 = mathutils.Vector((p2_2d[0], 0.0, p2_2d[1]))
    dist = (p2 - p1).length
    if dist < 1e-4: return None
    center = (p1 + p2) / 2.0
    bm = bmesh.new()
    geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=32, radius1=thickness, radius2=thickness, depth=dist)
    axis = (p2 - p1).normalized()
    up = mathutils.Vector((0, 0, 1))
    bmesh.ops.transform(bm, matrix=up.rotation_difference(axis).to_matrix().to_4x4(), verts=geom['verts'])
    bmesh.ops.translate(bm, verts=geom['verts'], vec=center)
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    mesh = bpy.data.meshes.new(f"EqCyl_Mesh")
    bm.to_mesh(mesh)
    bm.free(); apply_auto_smooth(mesh)
    obj = bpy.data.objects.new(f"EqLine_{datetime.now().strftime('%H%M%S')}", mesh)
    if context.collection: context.collection.objects.link(obj)
    else: context.scene.collection.objects.link(obj)
    obj.data.materials.append(mat)
    return obj

# ==============================================================================
#  マテリアル・プレビュー制御
# ==============================================================================

def create_unique_material(color, name_prefix="Mat_UniqueShape"):
    mat_name = f"{name_prefix}_{datetime.now().strftime('%M%S%f')[:5]}"
    mat = bpy.data.materials.new(name=mat_name)
    mat.use_nodes = True; mat.blend_method = 'BLEND'
    if mat.use_nodes:
        tree = mat.node_tree
        tree.nodes.clear()
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        out.location = (300, 0)
        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]
    cleanup_old_materials(name_prefix)
    return mat

def get_or_create_preview_material():
    mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
    if not mat:
        mat = bpy.data.materials.new(name=PREVIEW_MAT_NAME)
        mat.use_nodes = True; mat.blend_method = 'BLEND'
    return mat

def update_preview_material(mat, color):
    if mat.use_nodes:
        bsdf = None
        for node in mat.node_tree.nodes:
            if node.type == 'BSDF_PRINCIPLED': bsdf = node; break
        if not bsdf:
            mat.node_tree.nodes.clear()
            bsdf = mat.node_tree.nodes.new("ShaderNodeBsdfPrincipled")
            out = mat.node_tree.nodes.new("ShaderNodeOutputMaterial")
            mat.node_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_transform_matrix(props):
    rot_matrix = mathutils.Matrix.Identity(4)
    if props.torus_plane == 'YZ': rot_matrix = mathutils.Matrix.Rotation(math.radians(90.0), 4, 'Y')
    elif props.torus_plane == 'ZX': rot_matrix = mathutils.Matrix.Rotation(math.radians(-90.0), 4, 'X')
    user_rot = mathutils.Euler((math.radians(props.torus_rot[0]), math.radians(props.torus_rot[1]), math.radians(props.torus_rot[2])), 'XYZ').to_matrix().to_4x4()
    loc_matrix = mathutils.Matrix.Translation(mathutils.Vector(props.torus_loc))
    return loc_matrix @ user_rot @ rot_matrix

def generate_shape_bmesh(bm, props):
    sx = min(max(props.size_x, 0.01), 10000.0); sy = min(max(props.size_y, 0.01), 10000.0)
    mr = min(max(props.minor_radius, 0.001), 5000.0)
    if props.base_shape == 'CUBE': create_cube_framework_bmesh(bm, sx, mr, props.minor_segments)
    elif props.base_shape == 'SQUARE': create_square_torus_bmesh(bm, sx, props.corner_radius, mr, props.corner_segments, props.minor_segments)
    elif props.base_shape == 'CIRCLE': create_ellipse_torus_bmesh(bm, sx, sx, mr, props.major_segments, props.minor_segments)
    elif props.base_shape == 'ELLIPSE': create_ellipse_torus_bmesh(bm, sx, sy, mr, props.major_segments, props.minor_segments)

def update_preview_geometry(context):
    props = getattr(context.scene, PROPS_NAME, None)
    if not props: 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)
    
    obj = bpy.data.objects.get(PREVIEW_OBJ_NAME); guide_obj = bpy.data.objects.get(PREVIEW_GUIDE_NAME)
    if not props.show_preview:
        if obj: bpy.data.objects.remove(obj, do_unlink=True)
        if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)
        return

    final_matrix = get_transform_matrix(props)
    scene_mesh_name = f"PreviewMesh_{PREFIX}_{context.scene.name}"
    bm = bmesh.new()
    try:
        generate_shape_bmesh(bm, props)
        bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
        mesh = bpy.data.meshes.get(scene_mesh_name)
        if not mesh: mesh = bpy.data.meshes.new(scene_mesh_name)
        else: mesh.clear_geometry()
        bm.to_mesh(mesh)
        apply_auto_smooth(mesh); mesh.update(calc_edges=True)
    finally: bm.free()

    if not obj:
        obj = bpy.data.objects.new(PREVIEW_OBJ_NAME, mesh)
        col.objects.link(obj)
    elif obj.data != mesh: obj.data = mesh

    mat = get_or_create_preview_material()
    update_preview_material(mat, props.torus_color)
    if not obj.data.materials: obj.data.materials.append(mat)
    else: obj.data.materials[0] = mat

    if props.show_guide:
        bm_g = bmesh.new()
        try:
            if props.base_shape == 'CUBE': create_cube_guide_bmesh(bm_g, props.size_x)
            elif props.base_shape == 'SQUARE': create_square_guide_bmesh(bm_g, props.size_x)
            elif props.base_shape == 'CIRCLE': create_ellipse_guide_bmesh(bm_g, props.size_x, props.size_x, segments=props.major_segments)
            elif props.base_shape == 'ELLIPSE': create_ellipse_guide_bmesh(bm_g, props.size_x, props.size_y, segments=props.major_segments)
            bmesh.ops.transform(bm_g, matrix=final_matrix, verts=bm_g.verts)
            guide_mesh_name = scene_mesh_name + "_Guide"
            mesh_g = bpy.data.meshes.get(guide_mesh_name)
            if not mesh_g: mesh_g = bpy.data.meshes.new(guide_mesh_name)
            else: mesh_g.clear_geometry()
            bm_g.to_mesh(mesh_g)
            mesh_g.update(calc_edges=True)
        finally: bm_g.free()
        if not guide_obj:
            guide_obj = bpy.data.objects.new(PREVIEW_GUIDE_NAME, mesh_g)
            col.objects.link(guide_obj)
        elif guide_obj.data != mesh_g: guide_obj.data = mesh_g
        guide_obj.display_type = 'WIRE'; guide_obj.show_in_front = True
    else:
        if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)

_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)

# ★ 表示/非表示トグル用のアップデート関数
def update_cyl_visibility(self, context):
    prefixes = {
        'show_cyl_group1': "EqLine_1_over_aX_",
        'show_cyl_group2': "EqLine_Z_eq_X_",
        'show_cyl_group3': "EqLine_Z_eq_minus_X_",
        'show_cyl_group4': "EqLine_Z_eq_custom_bX_"
    }
    for prop_name, prefix in prefixes.items():
        is_visible = getattr(self, prop_name)
        for obj in bpy.data.objects:
            if obj.name.startswith(prefix):
                obj.hide_viewport = not is_visible
                obj.hide_render = not is_visible

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

class PG_TorusProps(PropertyGroup):
    show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
    show_guide: BoolProperty(name="Show Guide", default=CURRENT_DEFAULTS['show_guide'], update=on_update)
    torus_color: FloatVectorProperty(name="Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['torus_color'], update=on_update)
    base_shape: EnumProperty(name="Shape", items=[('CUBE', "Cube", ""), ('SQUARE', "Square", ""), ('CIRCLE', "Circle", ""), ('ELLIPSE', "Ellipse", "")], default=CURRENT_DEFAULTS['base_shape'], update=on_update)
    torus_plane: EnumProperty(name="Plane", items=[('XY', "XY Plane", ""), ('YZ', "YZ Plane", ""), ('ZX', "ZX Plane", "")], default=CURRENT_DEFAULTS['torus_plane'], update=on_update)
    torus_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['torus_loc'], update=on_update)
    torus_rot: FloatVectorProperty(name="Rotation", size=3, default=CURRENT_DEFAULTS['torus_rot'], update=on_update)
    size_x: FloatProperty(name="Size", default=CURRENT_DEFAULTS['size_x'], min=0.1, max=10000.0, update=on_update)
    size_y: FloatProperty(name="Size Y", default=CURRENT_DEFAULTS['size_y'], min=0.1, max=10000.0, update=on_update)
    corner_radius: FloatProperty(name="Corner Radius", default=CURRENT_DEFAULTS['corner_radius'], min=0.0, max=5000.0, update=on_update)
    minor_radius: FloatProperty(name="Tube Thickness", default=CURRENT_DEFAULTS['minor_radius'], min=0.01, max=5000.0, update=on_update)
    major_segments: IntProperty(name="Resolution", default=CURRENT_DEFAULTS['major_segments'], min=3, soft_max=128, update=on_update)
    corner_segments: IntProperty(name="Corner Segs", default=CURRENT_DEFAULTS['corner_segments'], min=1, soft_max=128, update=on_update)
    minor_segments: IntProperty(name="Tube Segs", default=CURRENT_DEFAULTS['minor_segments'], min=3, soft_max=128, update=on_update)

    cyl_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['cyl_thickness'], min=0.01, max=50.0)
    cyl_color: FloatVectorProperty(name="Color (1/a*X ± C)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['cyl_color'])
    zx_color: FloatVectorProperty(name="Color (Z=X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['zx_color'])
    minus_zx_color: FloatVectorProperty(name="Color (Z=-X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['minus_zx_color'])
    custom_zx_color: FloatVectorProperty(name="Color Custom", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['custom_zx_color'])
    
    cyl_slope_denom: FloatProperty(name="Denominator (a)", default=CURRENT_DEFAULTS['cyl_slope_denom'], min=0.001)
    cyl_offset: FloatProperty(name="Offset (C)", default=CURRENT_DEFAULTS['cyl_offset'], min=0.0)
    cyl_limit: FloatProperty(name="Limit Bounds", default=CURRENT_DEFAULTS['cyl_limit'], min=1.0)
    cyl_custom_denom: FloatProperty(name="Custom Denom (b)", default=CURRENT_DEFAULTS['cyl_custom_denom'])
    
    show_cyl_group1: BoolProperty(name="Vis G1", default=CURRENT_DEFAULTS['show_cyl_group1'], update=update_cyl_visibility)
    show_cyl_group2: BoolProperty(name="Vis G2", default=CURRENT_DEFAULTS['show_cyl_group2'], update=update_cyl_visibility)
    show_cyl_group3: BoolProperty(name="Vis G3", default=CURRENT_DEFAULTS['show_cyl_group3'], update=update_cyl_visibility)
    show_cyl_group4: BoolProperty(name="Vis G4", default=CURRENT_DEFAULTS['show_cyl_group4'], update=update_cyl_visibility)

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

class OT_CreateTorus(Operator):
    bl_idname = f"{OP_PREFIX}.create_torus"; bl_label = "Create Shape Torus"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        bm = bmesh.new()
        generate_shape_bmesh(bm, props)
        final_matrix = get_transform_matrix(props)
        bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
        mesh = bpy.data.meshes.new(f"Shape_Mesh")
        bm.to_mesh(mesh)
        bm.free(); apply_auto_smooth(mesh)
        name_dict = {'CUBE': "CubeFrame", 'SQUARE': "SqTorus", 'CIRCLE': "CircTorus", 'ELLIPSE': "ElpsTorus"}
        prefix_name = name_dict.get(props.base_shape, "Shape")
        obj = bpy.data.objects.new(f"{prefix_name}_{datetime.now().strftime('%H%M%S')}", mesh)
        if context.collection: context.collection.objects.link(obj)
        else: context.scene.collection.objects.link(obj)
        unique_mat = create_unique_material(props.torus_color, "Mat_Unique")
        obj.data.materials.append(unique_mat)
        bpy.ops.object.select_all(action='DESELECT')
        obj.select_set(True)
        context.view_layer.objects.active = obj
        self.report({'INFO'}, f"Created {prefix_name} Successfully!")
        return {'FINISHED'}

class OT_CreateEquationCylinders(Operator):
    bl_idname = f"{OP_PREFIX}.create_equation_cylinders"
    bl_label = "Create 6 Cylinders"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        a = props.cyl_slope_denom; C_val = props.cyl_offset; limit = props.cyl_limit; thickness = props.cyl_thickness
        b = props.cyl_custom_denom
        
        if abs(a) < 0.0001:
            self.report({'ERROR'}, "Denominator 'a' is too close to zero!")
            return {'CANCELLED'}
            
        M = 1.0 / a
        mat_eq = create_unique_material(props.cyl_color, "Mat_EqLine")
        mat_zx = create_unique_material(props.zx_color, "Mat_ZXLine")
        mat_mzx = create_unique_material(props.minus_zx_color, "Mat_MinusZXLine")
        mat_custom = create_unique_material(props.custom_zx_color, "Mat_CustomZXLine")
        
        created_objs = []
        
        # 1. Z = (1/a)X ± C (3 lines)
        for C in [-C_val, 0.0, C_val]:
            obj = create_cylinder_line(M, C, limit, thickness, mat_eq, context)
            if obj:
                obj.name = f"EqLine_1_over_aX_C{C}_{datetime.now().strftime('%H%M%S')}"
                obj.hide_viewport = not props.show_cyl_group1
                obj.hide_render = not props.show_cyl_group1
                created_objs.append(obj)
                
        # 2. Z = X (1 line)
        obj_zx = create_cylinder_line(1.0, 0.0, limit, thickness, mat_zx, context)
        if obj_zx:
            obj_zx.name = f"EqLine_Z_eq_X_{datetime.now().strftime('%H%M%S')}"
            obj_zx.hide_viewport = not props.show_cyl_group2
            obj_zx.hide_render = not props.show_cyl_group2
            created_objs.append(obj_zx)
            
        # 3. Z = -X (1 line)
        obj_mzx = create_cylinder_line(-1.0, 0.0, limit, thickness, mat_mzx, context)
        if obj_mzx:
            obj_mzx.name = f"EqLine_Z_eq_minus_X_{datetime.now().strftime('%H%M%S')}"
            obj_mzx.hide_viewport = not props.show_cyl_group3
            obj_mzx.hide_render = not props.show_cyl_group3
            created_objs.append(obj_mzx)
            
        # 4. Z = (1/b)X (1 line)
        if abs(b) < 0.0001:
            self.report({'WARNING'}, "Custom Denominator 'b' is zero. Skipped creating Custom Line.")
        else:
            obj_custom = create_cylinder_line(1.0 / b, 0.0, limit, thickness, mat_custom, context)
            if obj_custom:
                obj_custom.name = f"EqLine_Z_eq_custom_bX_{datetime.now().strftime('%H%M%S')}"
                obj_custom.hide_viewport = not props.show_cyl_group4
                obj_custom.hide_render = not props.show_cyl_group4
                created_objs.append(obj_custom)
            
        bpy.ops.object.select_all(action='DESELECT')
        for obj in created_objs: obj.select_set(True)
        if created_objs:
            context.view_layer.objects.active = created_objs[-1]
            self.report({'INFO'}, f"Created {len(created_objs)} Cylinders Successfully!")
        else:
            self.report({'WARNING'}, "No cylinders created. Lines might be entirely outside the bounds.")
            
        return {'FINISHED'}

class OT_CopyIntersectionInfo(Operator):
    bl_idname = f"{OP_PREFIX}.copy_intersection"
    bl_label = "Copy Intersections"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        a = props.cyl_slope_denom; C_val = props.cyl_offset; b = props.cyl_custom_denom
        
        text = "Intersection Points:\n"
        text += f"Equations & Parameters:\n"
        text += f" Base Lines : Z = (1/{a:.4f})*X ± {C_val:.4f}\n\n"
        
        text += "[ Z = X  and  Base Lines ]\n"
        if abs(1.0 - a * 1.0) < 0.0001: text += " Lines are parallel.\n"
        else:
            x_m = (a * (-C_val)) / (1.0 - a); x_p = (a * C_val) / (1.0 - a)
            text += f" Line (C = -{C_val:.1f}): X = {x_m:.4f}, Z = {x_m:.4f}\n"
            text += f" Line (C =  0.0): X = 0.0000, Z = 0.0000\n"
            text += f" Line (C = +{C_val:.1f}): X = {x_p:.4f}, Z = {x_p:.4f}\n"
            
        text += "\n[ Z = -X  and  Base Lines ]\n"
        if abs(1.0 - a * (-1.0)) < 0.0001: text += " Lines are parallel.\n"
        else:
            xm_m = (a * (-C_val)) / (1.0 + a); xm_p = (a * C_val) / (1.0 + a)
            text += f" Line (C = -{C_val:.1f}): X = {xm_m:.4f}, Z = {-xm_m:.4f}\n"
            text += f" Line (C =  0.0): X = 0.0000, Z = 0.0000\n"
            text += f" Line (C = +{C_val:.1f}): X = {xm_p:.4f}, Z = {-xm_p:.4f}\n"

        text += f"\n[ Z = (1/{b:.4f})X  and  Base Lines ]\n"
        d = a - b
        if abs(b) < 0.0001: text += " Denominator 'b' is zero. Invalid.\n"
        elif abs(d) < 0.0001: text += " Lines are parallel.\n"
        else:
            xb_m = (a * b * (-C_val)) / d; xb_p = (a * b * C_val) / d
            text += f" Line (C = -{C_val:.1f}): X = {xb_m:.4f}, Z = {xb_m/b:.4f}\n"
            text += f" Line (C =  0.0): X = 0.0000, Z = 0.0000\n"
            text += f" Line (C = +{C_val:.1f}): X = {xb_p:.4f}, Z = {xb_p/b:.4f}\n"
            
        context.window_manager.clipboard = text
        self.report({'INFO'}, "Intersection Data Copied to Clipboard!")
        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()
        c, l, r = props.torus_color, props.torus_loc, props.torus_rot
        cc, zc, mzc, czc = props.cyl_color, props.zx_color, props.minus_zx_color, props.custom_zx_color
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_preview": {props.show_preview},\n'
        new_dict += f'    "show_guide": {props.show_guide},\n'
        new_dict += f'    "torus_color": ({c[0]:.4f}, {c[1]:.4f}, {c[2]:.4f}, {c[3]:.4f}),\n'
        new_dict += f'    "torus_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
        new_dict += f'    "torus_rot": ({r[0]:.4f}, {r[1]:.4f}, {r[2]:.4f}),\n'
        new_dict += f'    "base_shape": "{props.base_shape}",\n'
        new_dict += f'    "torus_plane": "{props.torus_plane}",\n'
        new_dict += f'    "size_x": {props.size_x:.4f},\n'
        new_dict += f'    "size_y": {props.size_y:.4f},\n'
        new_dict += f'    "corner_radius": {props.corner_radius:.4f},\n'
        new_dict += f'    "minor_radius": {props.minor_radius:.4f},\n'
        new_dict += f'    "major_segments": {props.major_segments},\n'
        new_dict += f'    "corner_segments": {props.corner_segments},\n'
        new_dict += f'    "minor_segments": {props.minor_segments},\n'
        new_dict += f'    "cyl_thickness": {props.cyl_thickness:.4f},\n'
        new_dict += f'    "cyl_color": ({cc[0]:.4f}, {cc[1]:.4f}, {cc[2]:.4f}, {cc[3]:.4f}),\n'
        new_dict += f'    "zx_color": ({zc[0]:.4f}, {zc[1]:.4f}, {zc[2]:.4f}, {zc[3]:.4f}),\n'
        new_dict += f'    "minus_zx_color": ({mzc[0]:.4f}, {mzc[1]:.4f}, {mzc[2]:.4f}, {mzc[3]:.4f}),\n'
        new_dict += f'    "custom_zx_color": ({czc[0]:.4f}, {czc[1]:.4f}, {czc[2]:.4f}, {czc[3]:.4f}),\n'
        new_dict += f'    "cyl_slope_denom": {props.cyl_slope_denom:.4f},\n'
        new_dict += f'    "cyl_offset": {props.cyl_offset:.4f},\n'
        new_dict += f'    "cyl_limit": {props.cyl_limit:.4f},\n'
        new_dict += f'    "cyl_custom_denom": {props.cyl_custom_denom:.4f},\n'
        new_dict += f'    "show_cyl_group1": {props.show_cyl_group1},\n'
        new_dict += f'    "show_cyl_group2": {props.show_cyl_group2},\n'
        new_dict += f'    "show_cyl_group3": {props.show_cyl_group3},\n'
        new_dict += f'    "show_cyl_group4": {props.show_cyl_group4},\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
            if SOURCE_ID_TAG not in final_code: return {'CANCELLED'}
            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 safely!")
        except Exception as e: return {'CANCELLED'}
        return {'FINISHED'}

class OT_Reset(Operator):
    bl_idname = f"{OP_PREFIX}.reset"; bl_label = "Reset Transform"
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME)
        p.torus_loc = (0,0,0); p.torus_rot = (0,0,0); p.torus_plane = 'XY'; p.base_shape = 'SQUARE'
        p.size_x = 10.0; p.size_y = 5.0; p.corner_radius = 0.0; p.minor_radius = 0.5
        p.cyl_thickness = 0.5; p.cyl_color = (0.1, 0.6, 0.9, 1.0)
        p.zx_color = (0.9, 0.2, 0.2, 1.0); p.minus_zx_color = (0.9, 0.8, 0.2, 1.0)
        p.custom_zx_color = (0.2, 0.8, 0.9, 1.0)
        p.cyl_slope_denom = 0.6; p.cyl_offset = 10.0; p.cyl_limit = 50.0; p.cyl_custom_denom = -1.6
        p.show_cyl_group1 = True; p.show_cyl_group2 = True
        p.show_cyl_group3 = True; p.show_cyl_group4 = True
        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 = "Remove Addon"
    def execute(self, context):
        bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
        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: layout.label(text="Reload Script"); return
        row = layout.row(); row.scale_y = 1.2
        row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
        layout.separator()
        layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
        box = layout.box()
        if not props.show_preview: box.label(text="Preview is Hidden", icon='INFO')
        box.prop(props, "torus_color")
        col = box.column(align=True)
        col.prop(props, "base_shape"); col.prop(props, "torus_plane")
        col.separator()
        col.prop(props, "torus_loc"); col.prop(props, "torus_rot")
        box.separator()
        box.prop(props, "show_guide", icon='MESH_GRID', text="Show Guide Wire")
        
        col_s = box.column(align=True)
        if props.base_shape == 'ELLIPSE': col_s.prop(props, "size_x", text="Size X"); col_s.prop(props, "size_y", text="Size Y")
        else: col_s.prop(props, "size_x", text="Size")
        
        row_cr = col_s.row(); row_cr.enabled = (props.base_shape == 'SQUARE')
        row_cr.prop(props, "corner_radius")
        if props.corner_radius <= 0.001 and props.base_shape == 'SQUARE': row_cr.label(text="[90° Mode]", icon='SNAP_VERTEX')
            
        col_s.prop(props, "minor_radius")
        row_seg = box.row()
        if props.base_shape in['CIRCLE', 'ELLIPSE']: row_seg.prop(props, "major_segments", text="Resolution")
        elif props.base_shape == 'SQUARE': row_seg.prop(props, "corner_segments", text="Corner Segs")
        else: row_seg.label(text="[Cube has fixed corners]")
        box.row().prop(props, "minor_segments")
        box.operator(OT_Reset.bl_idname, icon='LOOP_BACK')
        layout.separator()
        col_exec = layout.column(); col_exec.scale_y = 1.5
        icons = {'CUBE': 'MESH_CUBE', 'SQUARE': 'MESH_PLANE', 'CIRCLE': 'MESH_CIRCLE', 'ELLIPSE': 'MESH_CIRCLE'}
        texts = {'CUBE': "Create Cube Frame", 'SQUARE': "Create Square Torus", 'CIRCLE': "Create Circle Torus", 'ELLIPSE': "Create Ellipse Torus"}
        col_exec.operator(OT_CreateTorus.bl_idname, icon=icons.get(props.base_shape, 'MESH_TORUS'), text=texts.get(props.base_shape, "Create Torus"))

class PT_EquationCylindersPanel(Panel):
    bl_label = "Equation Cylinders (6 Lines)"
    bl_idname = f"{PREFIX}_PT_eq_cylinders"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return
        
        a = props.cyl_slope_denom
        C_val = props.cyl_offset
        b = props.cyl_custom_denom
        
        box = layout.box()
        box.label(text="Math Equations (Y=0 Plane):", icon='FILE_TEXT')
        box.label(text=f"  G1: Z = ( 1 / {a:.2f} ) * X ± {C_val:.2f}")
        box.label(text=f"  G2: Z = X")
        box.label(text=f"  G3: Z = -X")
        box.label(text=f"  G4: Z = ( 1 / {b:.2f} ) * X")
        
        box.separator()
        col = box.column(align=True)
        col.prop(props, "cyl_slope_denom", text="Denominator (a)")
        col.prop(props, "cyl_offset", text="Offset (C)")
        col.prop(props, "cyl_limit", text="Limit Bounds (-L to L)")
        col.prop(props, "cyl_custom_denom", text="Custom Denom (b)")
        
        box.separator()
        box.prop(props, "cyl_thickness", text="Thickness")
        
        # ★ カラー設定 & 表示トグル
        col_c = box.column(align=True)
        r1 = col_c.row(align=True)
        r1.prop(props, "show_cyl_group1", text="", icon='HIDE_OFF' if props.show_cyl_group1 else 'HIDE_ON')
        r1.prop(props, "cyl_color", text=f"Z=(1/{a:.2f})X ± {C_val:.1f}")
        
        r2 = col_c.row(align=True)
        r2.prop(props, "show_cyl_group2", text="", icon='HIDE_OFF' if props.show_cyl_group2 else 'HIDE_ON')
        r2.prop(props, "zx_color", text="Z = X")
        
        r3 = col_c.row(align=True)
        r3.prop(props, "show_cyl_group3", text="", icon='HIDE_OFF' if props.show_cyl_group3 else 'HIDE_ON')
        r3.prop(props, "minus_zx_color", text="Z = -X")

        r4 = col_c.row(align=True)
        r4.prop(props, "show_cyl_group4", text="", icon='HIDE_OFF' if props.show_cyl_group4 else 'HIDE_ON')
        r4.prop(props, "custom_zx_color", text=f"Z = (1/{b:.2f})X")
        
        # ★ 交点表示エリア
        box.separator()
        box_int = box.box()
        box_int.label(text=f"Intersections with Z=(1/{a:.2f})X ± {C_val:.1f} :", icon='DRIVER')
        
        col_pts = box_int.column()
        row1 = col_pts.row(); col1 = row1.column(); col2 = row1.column()
        row2 = col_pts.row(); col3 = row2.column(); col4 = row2.column()
        
        # [ Z = X ] 側
        col1.label(text="[ with Z = X ]")
        if abs(1.0 - a * 1.0) < 0.0001: col1.label(text="Parallel")
        else:
            x_minus = (a * (-C_val)) / (1.0 - a); x_plus  = (a * C_val) / (1.0 - a)
            col1.label(text=f"C=-{C_val:.1f} : ({x_minus:.1f}, {x_minus:.1f})")
            col1.label(text=f"C= 0.0 : (0.0, 0.0)")
            col1.label(text=f"C=+{C_val:.1f} : ({x_plus:.1f}, {x_plus:.1f})")
            
        # [ Z = -X ] 側
        col2.label(text="[ with Z = -X ]")
        if abs(1.0 - a * (-1.0)) < 0.0001: col2.label(text="Parallel")
        else:
            xm_minus = (a * (-C_val)) / (1.0 + a); xm_plus  = (a * C_val) / (1.0 + a)
            col2.label(text=f"C=-{C_val:.1f} : ({xm_minus:.1f}, {-xm_minus:.1f})")
            col2.label(text=f"C= 0.0 : (0.0, 0.0)")
            col2.label(text=f"C=+{C_val:.1f} : ({xm_plus:.1f}, {-xm_plus:.1f})")
            
        # [ Z = (1/b)X ] 側
        col3.label(text=f"[ with Z = (1/{b:.2f})X ]")
        d = a - b
        if abs(b) < 0.0001: col3.label(text="Invalid (b=0)")
        elif abs(d) < 0.0001: col3.label(text="Parallel")
        else:
            xb_minus = (a * b * (-C_val)) / d; xb_plus = (a * b * C_val) / d
            col3.label(text=f"C=-{C_val:.1f} : ({xb_minus:.1f}, {xb_minus/b:.1f})")
            col3.label(text=f"C= 0.0 : (0.0, 0.0)")
            col3.label(text=f"C=+{C_val:.1f} : ({xb_plus:.1f}, {xb_plus/b:.1f})")
            
        box_int.operator(OT_CopyIntersectionInfo.bl_idname, icon='COPYDOWN', text="Copy Intersections")
        col_exec = box.column(); col_exec.scale_y = 1.2
        col_exec.operator(OT_CreateEquationCylinders.bl_idname, icon='MESH_CYLINDER', text="Create 6 Cylinders")

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_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        for l in ADDON_LINKS: self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"]).url = l["url"]

class PT_RemovePanel(Panel):
    bl_label = "System"; bl_idname = f"{PREFIX}_PT_remove"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): self.layout.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Remove Addon")

# ==============================================================================
#  REGISTER
# ==============================================================================
classes = (
    PG_TorusProps, 
    OT_CreateTorus, 
    OT_CreateEquationCylinders, 
    OT_CopyIntersectionInfo, 
    OT_CopyFullScript, 
    OT_Reset, 
    OT_OpenUrl, 
    OT_RemoveAddon, 
    PT_MainPanel, 
    PT_EquationCylindersPanel, 
    PT_LinksPanel, 
    PT_RemovePanel
)

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_TorusProps))
    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()
Intersection Points:
Parameters: a = 0.6000, C = 10.0000

[ Z = X  and  Z = (1/a)*X + C ]
 Line (C = -10.0): X = -15.0000, Z = -15.0000
 Line (C =  0.0): X = 0.0000, Z = 0.0000
 Line (C = +10.0): X = 15.0000, Z = 15.0000

[ Z = -X  and  Z = (1/a)*X + C ]
 Line (C = -10.0): X = -3.7500, Z = 3.7500
 Line (C =  0.0): X = 0.0000, Z = 0.0000
 Line (C = +10.0): X = 3.7500, Z = -3.7500

[ Z = -1.60X  and  Z = (1/a)*X + C ]
 Line (C = -10.0): X = -3.0612, Z = 4.8980
 Line (C =  0.0): X = 0.0000, Z = 0.0000
 Line (C = +10.0): X = 3.0612, Z = -4.8980

# Copied: 2026-03-24 21:03:00
import bpy
import bmesh
import webbrowser
import math
import mathutils
import time
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime

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

PREFIX       = "ShapeTorus20260324"
ADDON_NAME   = "zionad 520[ Shape-Torus ]"
TAB_NAME     = "[ Shape Torus copy ]   "
PANEL_TITLE  = "Multi-Shape Generator"
AUTHOR       = "zionadchat"

# ★ このスクリプト自身のID
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SHAPE_TORUS_2026_03_24_V9_MULTI_SHAPE ###"

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

bl_info = {
    "name": f"{ADDON_NAME} {PREFIX}",
    "author": AUTHOR,
    "version": (9, 4, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": f"Multi-Shape Torus & Equation Cylinders - {PREFIX}",
    "category": "3D View",
}

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

# ★ リンク設定
ADDON_LINKS = (
    {"label": "時空図 交点 2060407", "url": "<https://www.notion.so/2060407-33af5dacaf43808d86bbf0d54d4d0dd5>"},
    {"label": "Prefix トーラス正方形 20260324", "url": "<https://www.notion.so/Prefix-20260324-32df5dacaf4380528980db6a989d6306>"},
)

# ==============================================================================
#  デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_preview": True,
    "show_guide": True,
    "torus_color": (0.0391, 0.8000, 0.1647, 1.0000),
    "torus_loc": (0.0000, 0.0000, 0.0000),
    "torus_rot": (0.0000, 0.0000, 0.0000),
    "base_shape": "SQUARE",
    "torus_plane": "XY",
    "size_x": 10.0000,
    "size_y": 5.0000,
    "corner_radius": 0.0000,
    "minor_radius": 0.5000,
    "major_segments": 32,
    "corner_segments": 8,
    "minor_segments": 16,
    "cyl_thickness": 0.5000,
    "cyl_color": (0.1000, 0.6000, 0.9000, 1.0000),
    "zx_color": (0.9000, 0.2000, 0.2000, 1.0000),
    "minus_zx_color": (0.9000, 0.8000, 0.2000, 1.0000),
    "cyl_slope_denom": 0.6000,
    "cyl_offset": 10.0000,
    "cyl_limit": 50.0000,
}
# <END_DICT>

# ==============================================================================
#  データ クリーンアップ管理
# ==============================================================================

PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_OBJ_NAME = f"[Preview] Shape_{PREFIX}"
PREVIEW_GUIDE_NAME = f"[Preview] Guide_{PREFIX}"
PREVIEW_MAT_NAME = f"PreviewMat_{PREFIX}"

def cleanup_preview_data():
    for name in[PREVIEW_OBJ_NAME, PREVIEW_GUIDE_NAME]:
        obj = bpy.data.objects.get(name)
        if obj:
            mesh = obj.data
            bpy.data.objects.remove(obj, do_unlink=True)
            if mesh and mesh.users == 0:
                bpy.data.meshes.remove(mesh)
                
    meshes_to_remove =[m for m in bpy.data.meshes if m.name.startswith(f"PreviewMesh_{PREFIX}")]
    for m in meshes_to_remove:
        if m.users == 0: bpy.data.meshes.remove(m)
    mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
    if mat and mat.users == 0: bpy.data.materials.remove(mat)
    col = bpy.data.collections.get(PREVIEW_COL_NAME)
    if col and len(col.objects) == 0: bpy.data.collections.remove(col)

def cleanup_old_materials(prefix="Mat_UniqueShape", limit=50):
    mats =[m for m in bpy.data.materials if m.name.startswith(prefix)]
    if len(mats) > limit:
        for m in mats[:-limit]:
            if m.users == 0: bpy.data.materials.remove(m)

# ==============================================================================
#  ジオメトリ エンジン
# ==============================================================================

def create_square_guide_bmesh(bm, size):
    S = size / 2.0
    v1 = bm.verts.new((S, S, 0)); v2 = bm.verts.new((-S, S, 0))
    v3 = bm.verts.new((-S, -S, 0)); v4 = bm.verts.new((S, -S, 0))
    bm.verts.ensure_lookup_table()
    bm.edges.new((v1, v2)); bm.edges.new((v2, v3)); bm.edges.new((v3, v4)); bm.edges.new((v4, v1))
    return bm

def create_cube_guide_bmesh(bm, size):
    geom = bmesh.ops.create_cube(bm, size=size)
    faces =[f for f in bm.faces]
    bmesh.ops.delete(bm, geom=faces, context='FACES_ONLY')
    return bm

def create_ellipse_guide_bmesh(bm, size_x, size_y, segments=64):
    a = size_x / 2.0; b = size_y / 2.0
    verts =[]
    for i in range(segments):
        t = i * 2.0 * math.pi / segments
        verts.append(bm.verts.new((a * math.cos(t), b * math.sin(t), 0)))
    bm.verts.ensure_lookup_table()
    for i in range(segments): bm.edges.new((verts[i], verts[(i + 1) % segments]))
    return bm

def create_ellipse_torus_bmesh(bm, size_x, size_y, minor_radius, major_segments, minor_segments):
    a = size_x / 2.0; b = size_y / 2.0
    rings =[]
    for i in range(major_segments):
        t = i * 2.0 * math.pi / major_segments
        p = mathutils.Vector((a * math.cos(t), b * math.sin(t), 0))
        n = mathutils.Vector((b * math.cos(t), a * math.sin(t), 0)).normalized()
        up = mathutils.Vector((0, 0, 1))
        ring =[]
        for j in range(minor_segments):
            theta = j * 2.0 * math.pi / minor_segments
            offset = n * (minor_radius * math.cos(theta)) + up * (minor_radius * math.sin(theta))
            ring.append(bm.verts.new(p + offset))
        rings.append(ring)
    bm.verts.ensure_lookup_table()
    edge_loops = []
    for ring in rings:
        edges =[]
        for j in range(minor_segments): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
        edge_loops.append(edges)
    bm.edges.ensure_lookup_table()
    for i in range(major_segments):
        try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % major_segments])
        except Exception: pass
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm

def create_square_torus_bmesh(bm, size, corner_radius, minor_radius, corner_segments, minor_segments):
    half_size = size / 2.0
    actual_corner_radius = min(max(corner_radius, 0.0), half_size)
    rings =[]; EPS = 1e-6
    if actual_corner_radius < EPS:
        L = half_size
        corners =[
            (mathutils.Vector((L, L, 0)), mathutils.Vector((1, 1, 0)).normalized()),
            (mathutils.Vector((-L, L, 0)), mathutils.Vector((-1, 1, 0)).normalized()),
            (mathutils.Vector((-L, -L, 0)), mathutils.Vector((-1, -1, 0)).normalized()),
            (mathutils.Vector((L, -L, 0)), mathutils.Vector((1, -1, 0)).normalized())
        ]
        scale_xy = 1.0 / math.cos(math.pi / 4)
        for p, n in corners:
            b = mathutils.Vector((0, 0, 1))
            ring =[]
            for j in range(minor_segments):
                theta = j * 2.0 * math.pi / minor_segments
                offset = n * (minor_radius * math.cos(theta) * scale_xy) + b * (minor_radius * math.sin(theta))
                ring.append(bm.verts.new(p + offset))
            rings.append(ring)
    else:
        L = half_size - actual_corner_radius
        pts = []
        for q in range(4):
            cx = L if q in[0, 3] else -L
            cy = L if q in [0, 1] else -L
            for i in range(corner_segments + 1):
                angle = q * (math.pi / 2) + i * (math.pi / 2) / corner_segments
                pts.append((mathutils.Vector((cx + actual_corner_radius * math.cos(angle), cy + actual_corner_radius * math.sin(angle), 0)), mathutils.Vector((math.cos(angle), math.sin(angle), 0))))
        unique_pts =[]
        for p, n in pts:
            if not unique_pts or (unique_pts[-1][0] - p).length > EPS: unique_pts.append((p, n))
        if len(unique_pts) > 1 and (unique_pts[-1][0] - unique_pts[0][0]).length < EPS: unique_pts.pop()
        for p, n in unique_pts:
            b = mathutils.Vector((0, 0, 1))
            ring =[]
            for j in range(minor_segments):
                theta = j * 2.0 * math.pi / minor_segments
                ring.append(bm.verts.new(p + n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))))
            rings.append(ring)
    bm.verts.ensure_lookup_table()
    total_rings = len(rings)
    if total_rings < 3: return bm
    edge_loops = []
    for ring in rings:
        edges =[]
        for j in range(minor_segments): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
        edge_loops.append(edges)
    bm.edges.ensure_lookup_table()
    for i in range(total_rings):
        try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % total_rings])
        except Exception: pass
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm

def create_cube_framework_bmesh(bm, size, minor_radius, minor_segments):
    L = size / 2.0
    verts_co =[
        mathutils.Vector(( L,  L,  L)), mathutils.Vector((-L,  L,  L)),
        mathutils.Vector((-L, -L,  L)), mathutils.Vector(( L, -L,  L)),
        mathutils.Vector(( L,  L, -L)), mathutils.Vector((-L,  L, -L)),
        mathutils.Vector((-L, -L, -L)), mathutils.Vector(( L, -L, -L)),
    ]
    edges_idx =[(0,1), (1,2), (2,3), (3,0), (4,5), (5,6), (6,7), (7,4), (0,4), (1,5), (2,6), (3,7)]
    for co in verts_co:
        geom = bmesh.ops.create_uvsphere(bm, u_segments=minor_segments, v_segments=max(minor_segments//2, 3), radius=minor_radius)
        bmesh.ops.translate(bm, verts=geom['verts'], vec=co)
    for idx1, idx2 in edges_idx:
        v1 = verts_co[idx1]; v2 = verts_co[idx2]
        dist = (v1 - v2).length
        geom = bmesh.ops.create_cone(bm, cap_ends=False, cap_tris=False, segments=minor_segments, radius1=minor_radius, radius2=minor_radius, depth=dist)
        axis = (v1 - v2).normalized()
        bmesh.ops.transform(bm, matrix=mathutils.Vector((0,0,1)).rotation_difference(axis).to_matrix().to_4x4(), verts=geom['verts'])
        bmesh.ops.translate(bm, verts=geom['verts'], vec=(v1 + v2) / 2.0)
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm

def apply_auto_smooth(mesh):
    if bpy.app.version < (4, 1, 0):
        try:
            if hasattr(mesh, "use_auto_smooth"):
                mesh.use_auto_smooth = True
                mesh.auto_smooth_angle = math.radians(30)
        except AttributeError: pass

# ==============================================================================
#  計算ロジック(直線の方程式をボックス制限内でクリッピング)
# ==============================================================================
def get_line_segment_in_bounds(M, C, limit):
    pts = []
    eps = 1e-4

    z1 = M * (-limit) + C
    if -limit - eps <= z1 <= limit + eps: pts.append((-limit, z1))
        
    z2 = M * limit + C
    if -limit - eps <= z2 <= limit + eps: pts.append((limit, z2))
        
    if abs(M) > 1e-6:
        x3 = (-limit - C) / M
        if -limit - eps <= x3 <= limit + eps: pts.append((x3, -limit))
            
    if abs(M) > 1e-6:
        x4 = (limit - C) / M
        if -limit - eps <= x4 <= limit + eps: pts.append((x4, limit))
            
    unique_pts = []
    for p in pts:
        if not any(abs(p[0] - up[0]) < eps and abs(p[1] - up[1]) < eps for up in unique_pts):
            unique_pts.append(p)
            
    if len(unique_pts) >= 2:
        return unique_pts[0], unique_pts[1]
    return None

def create_cylinder_line(M, C, limit, thickness, mat, context):
    pts = get_line_segment_in_bounds(M, C, limit)
    if not pts: return None
        
    p1_2d, p2_2d = pts
    p1 = mathutils.Vector((p1_2d[0], 0.0, p1_2d[1]))
    p2 = mathutils.Vector((p2_2d[0], 0.0, p2_2d[1]))
    
    dist = (p2 - p1).length
    if dist < 1e-4: return None
    
    center = (p1 + p2) / 2.0
    bm = bmesh.new()
    geom = bmesh.ops.create_cone(
        bm, cap_ends=True, cap_tris=False, segments=32,
        radius1=thickness, radius2=thickness, depth=dist
    )
    
    axis = (p2 - p1).normalized()
    up = mathutils.Vector((0, 0, 1))
    rot = up.rotation_difference(axis)
    
    bmesh.ops.transform(bm, matrix=rot.to_matrix().to_4x4(), verts=geom['verts'])
    bmesh.ops.translate(bm, verts=geom['verts'], vec=center)
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
    for f in bm.faces: f.smooth = True
    
    mesh = bpy.data.meshes.new(f"EqCyl_Mesh")
    bm.to_mesh(mesh)
    bm.free()
    apply_auto_smooth(mesh)
    
    name_label = f"EqLine_{datetime.now().strftime('%H%M%S')}"
    obj = bpy.data.objects.new(name_label, mesh)
    if context.collection: context.collection.objects.link(obj)
    else: context.scene.collection.objects.link(obj)
    obj.data.materials.append(mat)
    
    return obj

# ==============================================================================
#  マテリアル・プレビュー制御
# ==============================================================================

def create_unique_material(color, name_prefix="Mat_UniqueShape"):
    mat_name = f"{name_prefix}_{datetime.now().strftime('%M%S%f')[:5]}"
    mat = bpy.data.materials.new(name=mat_name)
    mat.use_nodes = True; mat.blend_method = 'BLEND'
    if mat.use_nodes:
        tree = mat.node_tree
        tree.nodes.clear()
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        out.location = (300, 0)
        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]
    cleanup_old_materials(name_prefix)
    return mat

def get_or_create_preview_material():
    mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
    if not mat:
        mat = bpy.data.materials.new(name=PREVIEW_MAT_NAME)
        mat.use_nodes = True; mat.blend_method = 'BLEND'
    return mat

def update_preview_material(mat, color):
    if mat.use_nodes:
        bsdf = None
        for node in mat.node_tree.nodes:
            if node.type == 'BSDF_PRINCIPLED': bsdf = node; break
        if not bsdf:
            mat.node_tree.nodes.clear()
            bsdf = mat.node_tree.nodes.new("ShaderNodeBsdfPrincipled")
            out = mat.node_tree.nodes.new("ShaderNodeOutputMaterial")
            mat.node_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_transform_matrix(props):
    rot_matrix = mathutils.Matrix.Identity(4)
    if props.torus_plane == 'YZ': rot_matrix = mathutils.Matrix.Rotation(math.radians(90.0), 4, 'Y')
    elif props.torus_plane == 'ZX': rot_matrix = mathutils.Matrix.Rotation(math.radians(-90.0), 4, 'X')
    user_rot = mathutils.Euler((math.radians(props.torus_rot[0]), math.radians(props.torus_rot[1]), math.radians(props.torus_rot[2])), 'XYZ').to_matrix().to_4x4()
    loc_matrix = mathutils.Matrix.Translation(mathutils.Vector(props.torus_loc))
    return loc_matrix @ user_rot @ rot_matrix

def generate_shape_bmesh(bm, props):
    sx = min(max(props.size_x, 0.01), 10000.0)
    sy = min(max(props.size_y, 0.01), 10000.0)
    mr = min(max(props.minor_radius, 0.001), 5000.0)
    if props.base_shape == 'CUBE': create_cube_framework_bmesh(bm, sx, mr, props.minor_segments)
    elif props.base_shape == 'SQUARE': create_square_torus_bmesh(bm, sx, props.corner_radius, mr, props.corner_segments, props.minor_segments)
    elif props.base_shape == 'CIRCLE': create_ellipse_torus_bmesh(bm, sx, sx, mr, props.major_segments, props.minor_segments)
    elif props.base_shape == 'ELLIPSE': create_ellipse_torus_bmesh(bm, sx, sy, mr, props.major_segments, props.minor_segments)

def update_preview_geometry(context):
    props = getattr(context.scene, PROPS_NAME, None)
    if not props: 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)
    
    obj = bpy.data.objects.get(PREVIEW_OBJ_NAME)
    guide_obj = bpy.data.objects.get(PREVIEW_GUIDE_NAME)
    if not props.show_preview:
        if obj: bpy.data.objects.remove(obj, do_unlink=True)
        if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)
        return

    final_matrix = get_transform_matrix(props)
    scene_mesh_name = f"PreviewMesh_{PREFIX}_{context.scene.name}"
    bm = bmesh.new()
    try:
        generate_shape_bmesh(bm, props)
        bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
        mesh = bpy.data.meshes.get(scene_mesh_name)
        if not mesh: mesh = bpy.data.meshes.new(scene_mesh_name)
        else: mesh.clear_geometry()
        bm.to_mesh(mesh)
        apply_auto_smooth(mesh)
        mesh.update(calc_edges=True)
    finally: bm.free()

    if not obj:
        obj = bpy.data.objects.new(PREVIEW_OBJ_NAME, mesh)
        col.objects.link(obj)
    elif obj.data != mesh: obj.data = mesh

    mat = get_or_create_preview_material()
    update_preview_material(mat, props.torus_color)
    if not obj.data.materials: obj.data.materials.append(mat)
    else: obj.data.materials[0] = mat

    if props.show_guide:
        bm_g = bmesh.new()
        try:
            if props.base_shape == 'CUBE': create_cube_guide_bmesh(bm_g, props.size_x)
            elif props.base_shape == 'SQUARE': create_square_guide_bmesh(bm_g, props.size_x)
            elif props.base_shape == 'CIRCLE': create_ellipse_guide_bmesh(bm_g, props.size_x, props.size_x, segments=props.major_segments)
            elif props.base_shape == 'ELLIPSE': create_ellipse_guide_bmesh(bm_g, props.size_x, props.size_y, segments=props.major_segments)
            bmesh.ops.transform(bm_g, matrix=final_matrix, verts=bm_g.verts)
            guide_mesh_name = scene_mesh_name + "_Guide"
            mesh_g = bpy.data.meshes.get(guide_mesh_name)
            if not mesh_g: mesh_g = bpy.data.meshes.new(guide_mesh_name)
            else: mesh_g.clear_geometry()
            bm_g.to_mesh(mesh_g)
            mesh_g.update(calc_edges=True)
        finally: bm_g.free()
        if not guide_obj:
            guide_obj = bpy.data.objects.new(PREVIEW_GUIDE_NAME, mesh_g)
            col.objects.link(guide_obj)
        elif guide_obj.data != mesh_g: guide_obj.data = mesh_g
        guide_obj.display_type = 'WIRE'
        guide_obj.show_in_front = True
    else:
        if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)

_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_TorusProps(PropertyGroup):
    show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
    show_guide: BoolProperty(name="Show Guide", default=CURRENT_DEFAULTS['show_guide'], update=on_update)
    torus_color: FloatVectorProperty(name="Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['torus_color'], update=on_update)
    base_shape: EnumProperty(name="Shape", items=[('CUBE', "Cube", ""), ('SQUARE', "Square", ""), ('CIRCLE', "Circle", ""), ('ELLIPSE', "Ellipse", "")], default=CURRENT_DEFAULTS['base_shape'], update=on_update)
    torus_plane: EnumProperty(name="Plane", items=[('XY', "XY Plane", ""), ('YZ', "YZ Plane", ""), ('ZX', "ZX Plane", "")], default=CURRENT_DEFAULTS['torus_plane'], update=on_update)
    torus_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['torus_loc'], update=on_update)
    torus_rot: FloatVectorProperty(name="Rotation", size=3, default=CURRENT_DEFAULTS['torus_rot'], update=on_update)
    size_x: FloatProperty(name="Size", default=CURRENT_DEFAULTS['size_x'], min=0.1, max=10000.0, update=on_update)
    size_y: FloatProperty(name="Size Y", default=CURRENT_DEFAULTS['size_y'], min=0.1, max=10000.0, update=on_update)
    corner_radius: FloatProperty(name="Corner Radius", default=CURRENT_DEFAULTS['corner_radius'], min=0.0, max=5000.0, update=on_update)
    minor_radius: FloatProperty(name="Tube Thickness", default=CURRENT_DEFAULTS['minor_radius'], min=0.01, max=5000.0, update=on_update)
    major_segments: IntProperty(name="Resolution", default=CURRENT_DEFAULTS['major_segments'], min=3, soft_max=128, update=on_update)
    corner_segments: IntProperty(name="Corner Segs", default=CURRENT_DEFAULTS['corner_segments'], min=1, soft_max=128, update=on_update)
    minor_segments: IntProperty(name="Tube Segs", default=CURRENT_DEFAULTS['minor_segments'], min=3, soft_max=128, update=on_update)

    cyl_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['cyl_thickness'], min=0.01, max=50.0)
    cyl_color: FloatVectorProperty(name="Color (a*X+C)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['cyl_color'])
    zx_color: FloatVectorProperty(name="Color (Z=X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['zx_color'])
    minus_zx_color: FloatVectorProperty(name="Color (Z=-X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['minus_zx_color'])
    cyl_slope_denom: FloatProperty(name="Denominator (a)", default=CURRENT_DEFAULTS['cyl_slope_denom'], min=0.001)
    cyl_offset: FloatProperty(name="Offset (C)", default=CURRENT_DEFAULTS['cyl_offset'], min=0.0)
    cyl_limit: FloatProperty(name="Limit Bounds", default=CURRENT_DEFAULTS['cyl_limit'], min=1.0)

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

class OT_CreateTorus(Operator):
    bl_idname = f"{OP_PREFIX}.create_torus"; bl_label = "Create Shape Torus"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        bm = bmesh.new()
        generate_shape_bmesh(bm, props)
        final_matrix = get_transform_matrix(props)
        bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
        mesh = bpy.data.meshes.new(f"Shape_Mesh")
        bm.to_mesh(mesh)
        bm.free(); apply_auto_smooth(mesh)
        name_dict = {'CUBE': "CubeFrame", 'SQUARE': "SqTorus", 'CIRCLE': "CircTorus", 'ELLIPSE': "ElpsTorus"}
        prefix_name = name_dict.get(props.base_shape, "Shape")
        obj = bpy.data.objects.new(f"{prefix_name}_{datetime.now().strftime('%H%M%S')}", mesh)
        if context.collection: context.collection.objects.link(obj)
        else: context.scene.collection.objects.link(obj)
        unique_mat = create_unique_material(props.torus_color, "Mat_Unique")
        obj.data.materials.append(unique_mat)
        bpy.ops.object.select_all(action='DESELECT')
        obj.select_set(True)
        context.view_layer.objects.active = obj
        self.report({'INFO'}, f"Created {prefix_name} Successfully!")
        return {'FINISHED'}

class OT_CreateEquationCylinders(Operator):
    bl_idname = f"{OP_PREFIX}.create_equation_cylinders"
    bl_label = "Create 5 Cylinders"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        a = props.cyl_slope_denom; C_val = props.cyl_offset; limit = props.cyl_limit; thickness = props.cyl_thickness
        
        if abs(a) < 0.0001:
            self.report({'ERROR'}, "Denominator 'a' is too close to zero!")
            return {'CANCELLED'}
            
        M = 1.0 / a
        mat_eq = create_unique_material(props.cyl_color, "Mat_EqLine")
        mat_zx = create_unique_material(props.zx_color, "Mat_ZXLine")
        mat_mzx = create_unique_material(props.minus_zx_color, "Mat_MinusZXLine")
        
        created_objs = []
        
        # 1. Z = (1/a)X ± C (3 lines)
        for C in [-C_val, 0.0, C_val]:
            obj = create_cylinder_line(M, C, limit, thickness, mat_eq, context)
            if obj:
                obj.name = f"EqLine_1_over_aX_C{C}_{datetime.now().strftime('%H%M%S')}"
                created_objs.append(obj)
                
        # 2. Z = X (1 line)
        obj_zx = create_cylinder_line(1.0, 0.0, limit, thickness, mat_zx, context)
        if obj_zx:
            obj_zx.name = f"EqLine_Z_eq_X_{datetime.now().strftime('%H%M%S')}"
            created_objs.append(obj_zx)
            
        # 3. Z = -X (1 line)
        obj_mzx = create_cylinder_line(-1.0, 0.0, limit, thickness, mat_mzx, context)
        if obj_mzx:
            obj_mzx.name = f"EqLine_Z_eq_minus_X_{datetime.now().strftime('%H%M%S')}"
            created_objs.append(obj_mzx)
            
        bpy.ops.object.select_all(action='DESELECT')
        for obj in created_objs: obj.select_set(True)
        if created_objs:
            context.view_layer.objects.active = created_objs[-1]
            self.report({'INFO'}, f"Created {len(created_objs)} Cylinders Successfully!")
        else:
            self.report({'WARNING'}, "No cylinders created. Lines might be entirely outside the bounds.")
            
        return {'FINISHED'}

class OT_CopyIntersectionInfo(Operator):
    bl_idname = f"{OP_PREFIX}.copy_intersection"
    bl_label = "Copy Intersections"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        a = props.cyl_slope_denom
        C_val = props.cyl_offset
        
        text = "Intersection Points:\n"
        text += f"Parameters: a = {a:.4f}, C = {C_val:.4f}\n\n"
        
        text += "[ Z = X  and  Z = (1/a)*X + C ]\n"
        if abs(a - 1.0) < 0.0001:
            text += " Lines are parallel. (a = 1)\n"
        else:
            x_m = (a * (-C_val)) / (a - 1.0)
            x_p = (a * C_val) / (a - 1.0)
            text += f" Line (C = -{C_val:.1f}): X = {x_m:.4f}, Z = {x_m:.4f}\n"
            text += f" Line (C =  0.0): X = 0.0000, Z = 0.0000\n"
            text += f" Line (C = +{C_val:.1f}): X = {x_p:.4f}, Z = {x_p:.4f}\n"
            
        text += "\n[ Z = -X  and  Z = (1/a)*X + C ]\n"
        if abs(a + 1.0) < 0.0001:
            text += " Lines are parallel. (a = -1)\n"
        else:
            xm_m = (-a * (-C_val)) / (a + 1.0)
            xm_p = (-a * C_val) / (a + 1.0)
            text += f" Line (C = -{C_val:.1f}): X = {xm_m:.4f}, Z = {-xm_m:.4f}\n"
            text += f" Line (C =  0.0): X = 0.0000, Z = 0.0000\n"
            text += f" Line (C = +{C_val:.1f}): X = {xm_p:.4f}, Z = {-xm_p:.4f}\n"
            
        context.window_manager.clipboard = text
        self.report({'INFO'}, "Intersection Data Copied to Clipboard!")
        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()
        c, l, r = props.torus_color, props.torus_loc, props.torus_rot
        cc, zc, mzc = props.cyl_color, props.zx_color, props.minus_zx_color
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_preview": {props.show_preview},\n'
        new_dict += f'    "show_guide": {props.show_guide},\n'
        new_dict += f'    "torus_color": ({c[0]:.4f}, {c[1]:.4f}, {c[2]:.4f}, {c[3]:.4f}),\n'
        new_dict += f'    "torus_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
        new_dict += f'    "torus_rot": ({r[0]:.4f}, {r[1]:.4f}, {r[2]:.4f}),\n'
        new_dict += f'    "base_shape": "{props.base_shape}",\n'
        new_dict += f'    "torus_plane": "{props.torus_plane}",\n'
        new_dict += f'    "size_x": {props.size_x:.4f},\n'
        new_dict += f'    "size_y": {props.size_y:.4f},\n'
        new_dict += f'    "corner_radius": {props.corner_radius:.4f},\n'
        new_dict += f'    "minor_radius": {props.minor_radius:.4f},\n'
        new_dict += f'    "major_segments": {props.major_segments},\n'
        new_dict += f'    "corner_segments": {props.corner_segments},\n'
        new_dict += f'    "minor_segments": {props.minor_segments},\n'
        new_dict += f'    "cyl_thickness": {props.cyl_thickness:.4f},\n'
        new_dict += f'    "cyl_color": ({cc[0]:.4f}, {cc[1]:.4f}, {cc[2]:.4f}, {cc[3]:.4f}),\n'
        new_dict += f'    "zx_color": ({zc[0]:.4f}, {zc[1]:.4f}, {zc[2]:.4f}, {zc[3]:.4f}),\n'
        new_dict += f'    "minus_zx_color": ({mzc[0]:.4f}, {mzc[1]:.4f}, {mzc[2]:.4f}, {mzc[3]:.4f}),\n'
        new_dict += f'    "cyl_slope_denom": {props.cyl_slope_denom:.4f},\n'
        new_dict += f'    "cyl_offset": {props.cyl_offset:.4f},\n'
        new_dict += f'    "cyl_limit": {props.cyl_limit:.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
            if SOURCE_ID_TAG not in final_code: return {'CANCELLED'}
            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 safely!")
        except Exception as e: return {'CANCELLED'}
        return {'FINISHED'}

class OT_Reset(Operator):
    bl_idname = f"{OP_PREFIX}.reset"; bl_label = "Reset Transform"
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME)
        p.torus_loc = (0,0,0); p.torus_rot = (0,0,0); p.torus_plane = 'XY'; p.base_shape = 'SQUARE'
        p.size_x = 10.0; p.size_y = 5.0; p.corner_radius = 0.0; p.minor_radius = 0.5
        p.cyl_thickness = 0.5; p.cyl_color = (0.1, 0.6, 0.9, 1.0)
        p.zx_color = (0.9, 0.2, 0.2, 1.0); p.minus_zx_color = (0.9, 0.8, 0.2, 1.0)
        p.cyl_slope_denom = 0.6; p.cyl_offset = 10.0; p.cyl_limit = 50.0
        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 = "Remove Addon"
    def execute(self, context):
        bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
        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: layout.label(text="Reload Script"); return
        row = layout.row(); row.scale_y = 1.2
        row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
        layout.separator()
        layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
        box = layout.box()
        if not props.show_preview: box.label(text="Preview is Hidden", icon='INFO')
        box.prop(props, "torus_color")
        col = box.column(align=True)
        col.prop(props, "base_shape"); col.prop(props, "torus_plane")
        col.separator()
        col.prop(props, "torus_loc"); col.prop(props, "torus_rot")
        box.separator()
        box.prop(props, "show_guide", icon='MESH_GRID', text="Show Guide Wire")
        
        col_s = box.column(align=True)
        if props.base_shape == 'ELLIPSE': col_s.prop(props, "size_x", text="Size X"); col_s.prop(props, "size_y", text="Size Y")
        else: col_s.prop(props, "size_x", text="Size")
        
        row_cr = col_s.row(); row_cr.enabled = (props.base_shape == 'SQUARE')
        row_cr.prop(props, "corner_radius")
        if props.corner_radius <= 0.001 and props.base_shape == 'SQUARE': row_cr.label(text="[90° Mode]", icon='SNAP_VERTEX')
            
        col_s.prop(props, "minor_radius")
        row_seg = box.row()
        if props.base_shape in['CIRCLE', 'ELLIPSE']: row_seg.prop(props, "major_segments", text="Resolution")
        elif props.base_shape == 'SQUARE': row_seg.prop(props, "corner_segments", text="Corner Segs")
        else: row_seg.label(text="[Cube has fixed corners]")
        box.row().prop(props, "minor_segments")
        box.operator(OT_Reset.bl_idname, icon='LOOP_BACK')
        layout.separator()
        col_exec = layout.column(); col_exec.scale_y = 1.5
        icons = {'CUBE': 'MESH_CUBE', 'SQUARE': 'MESH_PLANE', 'CIRCLE': 'MESH_CIRCLE', 'ELLIPSE': 'MESH_CIRCLE'}
        texts = {'CUBE': "Create Cube Frame", 'SQUARE': "Create Square Torus", 'CIRCLE': "Create Circle Torus", 'ELLIPSE': "Create Ellipse Torus"}
        col_exec.operator(OT_CreateTorus.bl_idname, icon=icons.get(props.base_shape, 'MESH_TORUS'), text=texts.get(props.base_shape, "Create Torus"))

class PT_EquationCylindersPanel(Panel):
    bl_label = "Equation Cylinders (5 Lines)"
    bl_idname = f"{PREFIX}_PT_eq_cylinders"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return
        
        box = layout.box()
        box.label(text="Math Equations (Y=0 Plane):", icon='FILE_TEXT')
        box.label(text="  Z = ( 1 / a ) * X ± C")
        box.label(text="  Z = X   and   Z = -X")
        box.separator()
        
        col = box.column(align=True)
        col.prop(props, "cyl_slope_denom", text="Denominator (a)")
        col.prop(props, "cyl_offset", text="Offset (C)")
        col.prop(props, "cyl_limit", text="Limit Bounds (-L to L)")
        
        box.separator()
        box.prop(props, "cyl_thickness", text="Thickness")
        
        row_c1 = box.row()
        row_c1.prop(props, "cyl_color", text="(1/a)*X ± C")
        row_c2 = box.row()
        row_c2.prop(props, "zx_color", text="Z = X")
        row_c2.prop(props, "minus_zx_color", text="Z = -X")
        
        # ★ 交点表示エリア (Z=X と Z=-X)
        box.separator()
        box_int = box.box()
        box_int.label(text="Intersections:", icon='DRIVER')
        
        a = props.cyl_slope_denom
        C_val = props.cyl_offset
        
        col_pts = box_int.column()
        row_pts = col_pts.row()
        col_L = row_pts.column()
        col_R = row_pts.column()
        
        # [ Z = X ] 側
        col_L.label(text="[ with Z = X ]")
        if abs(a - 1.0) < 0.0001:
            col_L.label(text="Parallel (a=1)")
        else:
            x_minus = (a * (-C_val)) / (a - 1.0)
            x_plus  = (a * C_val) / (a - 1.0)
            col_L.label(text=f"C=-{C_val:.1f} : ({x_minus:.1f}, {x_minus:.1f})")
            col_L.label(text=f"C= 0.0 : (0.0, 0.0)")
            col_L.label(text=f"C=+{C_val:.1f} : ({x_plus:.1f}, {x_plus:.1f})")
            
        # [ Z = -X ] 側
        col_R.label(text="[ with Z = -X ]")
        if abs(a + 1.0) < 0.0001:
            col_R.label(text="Parallel (a=-1)")
        else:
            xm_minus = (-a * (-C_val)) / (a + 1.0)
            xm_plus  = (-a * C_val) / (a + 1.0)
            col_R.label(text=f"C=-{C_val:.1f} : ({xm_minus:.1f}, {-xm_minus:.1f})")
            col_R.label(text=f"C= 0.0 : (0.0, 0.0)")
            col_R.label(text=f"C=+{C_val:.1f} : ({xm_plus:.1f}, {-xm_plus:.1f})")
            
        box_int.operator(OT_CopyIntersectionInfo.bl_idname, icon='COPYDOWN', text="Copy Intersections")
        
        col_exec = box.column()
        col_exec.scale_y = 1.2
        col_exec.operator(OT_CreateEquationCylinders.bl_idname, icon='MESH_CYLINDER', text="Create 5 Cylinders")

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_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        for l in ADDON_LINKS: self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"]).url = l["url"]

class PT_RemovePanel(Panel):
    bl_label = "System"; bl_idname = f"{PREFIX}_PT_remove"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): self.layout.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Remove Addon")

# ==============================================================================
#  REGISTER
# ==============================================================================
classes = (
    PG_TorusProps, 
    OT_CreateTorus, 
    OT_CreateEquationCylinders, 
    OT_CopyIntersectionInfo, 
    OT_CopyFullScript, 
    OT_Reset, 
    OT_OpenUrl, 
    OT_RemoveAddon, 
    PT_MainPanel, 
    PT_EquationCylindersPanel, 
    PT_LinksPanel, 
    PT_RemovePanel
)

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_TorusProps))
    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()
Intersection Points ( Z = X  and  Z = (1/a)*X + C ):
Parameters: a = 0.6000, C = 10.0000

Line (C = -10.0): X = 15.00, Z = 15.00
Line (C =  0.0): X = 0.00, Z = 0.00
Line (C = +10.0): X = -15.00, Z = -15.00