blender Million 2026

Prefix トーラス正方形 20260324

安定性 改善









# Copied: 2026-03-24 22:11: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       = "SquareTorus20260324"
ADDON_NAME   = "zionad 520[ Sq-Torus ]"
TAB_NAME     = "[ Sq Torus copy ]   "
PANEL_TITLE  = "Square Torus Generator"
AUTHOR       = "zionadchat"

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

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

bl_info = {
    "name": f"{ADDON_NAME} {PREFIX}",
    "author": AUTHOR,
    "version": (7, 0, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": f"Topology-Perfect Square Torus Generator - {PREFIX}",
    "category": "3D View",
}

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

ADDON_LINKS = (
    {"label": "Prefix トーラス正方形 20260324", "url": "<https://www.notion.so/Prefix-20260324-32df5dacaf4380528980db6a989d6306>"},
)

# ==============================================================================
#  デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_preview": True,
    "show_square_guide": True,
    "torus_color": (0.0391, 0.8000, 0.1647, 0.8000),
    "torus_loc": (0.0000, 0.0000, 0.0000),
    "torus_rot": (0.0000, 30.0000, 0.0000),
    "square_size": 10.0000,
    "corner_radius": 0.0000,
    "minor_radius": 0.5000,
    "corner_segments": 8,
    "minor_segments": 16,
    "torus_plane": "XY",
}
# <END_DICT>

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

PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_OBJ_NAME = f"[Preview] SqTorus_{PREFIX}"
PREVIEW_GUIDE_NAME = f"[Preview] SqGuide_{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_UniqueSqTorus", 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, square_size):
    S = square_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_square_torus_bmesh(bm, square_size, corner_radius, minor_radius, corner_segments, minor_segments):
    square_size = min(max(square_size, 0.01), 10000.0)
    minor_radius = min(max(minor_radius, 0.001), square_size)
    minor_segments = max(minor_segments, 3)
    
    half_size = square_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
                x = cx + actual_corner_radius * math.cos(angle)
                y = cy + actual_corner_radius * math.sin(angle)
                pts.append((mathutils.Vector((x, y, 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
                offset = n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))
                ring.append(bm.verts.new(p + offset))
            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):
            v1 = ring[j]
            v2 = ring[(j + 1) % minor_segments]
            edges.append(bm.edges.new((v1, v2)))
        edge_loops.append(edges)
        
    bm.edges.ensure_lookup_table()

    for i in range(total_rings):
        next_i = (i + 1) % total_rings
        try:
            bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[next_i])
        except Exception:
            pass
            
    # ★ 順序保証: remove_doubles -> smooth -> recalc_face_normals
    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 create_unique_material(color, name_prefix="Mat_UniqueSqTorus"):
    timestamp = datetime.now().strftime('%M%S%f')[:5] 
    mat_name = f"{name_prefix}_{timestamp}"
    
    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")
        bsdf.location = (0, 0)
        
        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 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:
        create_square_torus_bmesh(
            bm, 
            square_size=props.square_size, 
            corner_radius=props.corner_radius, 
            minor_radius=props.minor_radius,
            corner_segments=props.corner_segments,
            minor_segments=props.minor_segments
        )
        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()
    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_square_guide:
        bm_g = bmesh.new()
        try:
            create_square_guide_bmesh(bm_g, props.square_size)
            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()
        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_square_guide: BoolProperty(name="Show Square Guide", default=CURRENT_DEFAULTS['show_square_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)
    
    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 (Deg)", size=3, default=CURRENT_DEFAULTS['torus_rot'], update=on_update)
    
    square_size: FloatProperty(name="Square Size", default=CURRENT_DEFAULTS['square_size'], 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)
    
    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)

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

class OT_CreateTorus(Operator):
    bl_idname = f"{OP_PREFIX}.create_torus"
    bl_label = "Create Square Torus"
    bl_options = {'REGISTER', 'UNDO'}  # ★ UX重視: UNDOを復活
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        
        bm = bmesh.new()
        create_square_torus_bmesh(
            bm, 
            square_size=props.square_size, 
            corner_radius=props.corner_radius, 
            minor_radius=props.minor_radius,
            corner_segments=props.corner_segments,
            minor_segments=props.minor_segments
        )
        
        final_matrix = get_transform_matrix(props)
        bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
        
        mesh = bpy.data.meshes.new(f"SquareTorus_Mesh")
        bm.to_mesh(mesh)
        bm.free()
        
        apply_auto_smooth(mesh)
        
        obj = bpy.data.objects.new(f"SqTorus_{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_UniqueSqTorus")
        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'}, "Created Topology-Perfect Square Torus!")
        return {'FINISHED'}

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

        code = target_text.as_string()
        c, l, r = props.torus_color, props.torus_loc, props.torus_rot
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_preview": {props.show_preview},\n'
        new_dict += f'    "show_square_guide": {props.show_square_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'    "square_size": {props.square_size:.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'    "corner_segments": {props.corner_segments},\n'
        new_dict += f'    "minor_segments": {props.minor_segments},\n'
        new_dict += f'    "torus_plane": "{props.torus_plane}",\n'
        new_dict += "}\n"

        try:
            tag_start = "# <BEGIN" + "_DICT>"
            tag_end = "# <END" + "_DICT>"
            
            # ★ 1. 安全な存在チェック
            if tag_start not in code or tag_end not in code:
                self.report({'ERROR'}, "DICT tags missing! Script might be corrupted.")
                return {'CANCELLED'}
            
            # ★ 2. 最大1回のみの安全な分割 (IndexError回避)
            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
            
            # ★ 3. SOURCE_ID_TAG の生存確認 (防御的プログラミング)
            if SOURCE_ID_TAG not in final_code:
                self.report({'ERROR'}, "Critical Error: SOURCE_ID_TAG lost during copy.")
                return {'CANCELLED'}
            
            lines = final_code.split("\n")
            if len(lines) > 0 and lines[0].startswith("# Copied:"):
                time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                lines[0] = f"# Copied: {time_str}"
            
            final_code = "\n".join(lines)
            
            context.window_manager.clipboard = final_code
            self.report({'INFO'}, "Code copied with absolute safety!")
        except Exception as e: 
            self.report({'ERROR'}, f"Copy failed: {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.square_size = 10.0
        p.corner_radius = 0.0
        p.minor_radius = 0.5
        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, "torus_plane")
        col.prop(props, "torus_loc")
        col.prop(props, "torus_rot")
        
        box.separator()
        
        box.prop(props, "show_square_guide", icon='MESH_PLANE')
        col_s = box.column(align=True)
        col_s.prop(props, "square_size")
        
        row_cr = col_s.row()
        row_cr.prop(props, "corner_radius")
        if props.corner_radius <= 0.001:
            row_cr.label(text="[90° Mode]", icon='SNAP_VERTEX')
            
        col_s.prop(props, "minor_radius")
        
        row_seg = box.row()
        row_seg.prop(props, "corner_segments")
        row_seg.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
        col_exec.operator(OT_CreateTorus.bl_idname, icon='MESH_TORUS', text="Create Square Torus")

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_CopyFullScript, OT_Reset, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, 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()
y軸 逆回転に
# Copied: 2026-03-24 20:04: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       = "SquareTorus20260324"
ADDON_NAME   = "zionad 520[ Sq-Torus ]"
TAB_NAME     = "[ Sq Torus copy ]   "
PANEL_TITLE  = "Square Torus Generator"
AUTHOR       = "zionadchat"

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

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

bl_info = {
    "name": f"{ADDON_NAME} {PREFIX}",
    "author": AUTHOR,
    "version": (7, 0, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": f"Topology-Perfect Square Torus Generator - {PREFIX}",
    "category": "3D View",
}

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

ADDON_LINKS = (
    {"label": "Prefix トーラス正方形 20260324", "url": "<https://www.notion.so/Prefix-20260324-32df5dacaf4380528980db6a989d6306>"},
)

# ==============================================================================
#  デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_preview": True,
    "show_square_guide": True,
    "torus_color": (0.0391, 0.8000, 0.1647, 0.8000),
    "torus_loc": (0.0000, 0.0000, 0.0000),
    "torus_rot": (0.0000, 0.0000, 0.0000),
    "square_size": 10.0000,
    "corner_radius": 0.0000,
    "minor_radius": 0.5000,
    "corner_segments": 8,
    "minor_segments": 16,
    "torus_plane": "XY",
}
# <END_DICT>

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

PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_OBJ_NAME = f"[Preview] SqTorus_{PREFIX}"
PREVIEW_GUIDE_NAME = f"[Preview] SqGuide_{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_UniqueSqTorus", 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, square_size):
    S = square_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_square_torus_bmesh(bm, square_size, corner_radius, minor_radius, corner_segments, minor_segments):
    square_size = min(max(square_size, 0.01), 10000.0)
    minor_radius = min(max(minor_radius, 0.001), square_size)
    minor_segments = max(minor_segments, 3)
    
    half_size = square_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
                x = cx + actual_corner_radius * math.cos(angle)
                y = cy + actual_corner_radius * math.sin(angle)
                pts.append((mathutils.Vector((x, y, 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
                offset = n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))
                ring.append(bm.verts.new(p + offset))
            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):
            v1 = ring[j]
            v2 = ring[(j + 1) % minor_segments]
            edges.append(bm.edges.new((v1, v2)))
        edge_loops.append(edges)
        
    bm.edges.ensure_lookup_table()

    for i in range(total_rings):
        next_i = (i + 1) % total_rings
        try:
            bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[next_i])
        except Exception:
            pass
            
    # ★ 順序保証: remove_doubles -> smooth -> recalc_face_normals
    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 create_unique_material(color, name_prefix="Mat_UniqueSqTorus"):
    timestamp = datetime.now().strftime('%M%S%f')[:5] 
    mat_name = f"{name_prefix}_{timestamp}"
    
    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")
        bsdf.location = (0, 0)
        
        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 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:
        create_square_torus_bmesh(
            bm, 
            square_size=props.square_size, 
            corner_radius=props.corner_radius, 
            minor_radius=props.minor_radius,
            corner_segments=props.corner_segments,
            minor_segments=props.minor_segments
        )
        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()
    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_square_guide:
        bm_g = bmesh.new()
        try:
            create_square_guide_bmesh(bm_g, props.square_size)
            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()
        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_square_guide: BoolProperty(name="Show Square Guide", default=CURRENT_DEFAULTS['show_square_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)
    
    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 (Deg)", size=3, default=CURRENT_DEFAULTS['torus_rot'], update=on_update)
    
    square_size: FloatProperty(name="Square Size", default=CURRENT_DEFAULTS['square_size'], 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)
    
    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)

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

class OT_CreateTorus(Operator):
    bl_idname = f"{OP_PREFIX}.create_torus"
    bl_label = "Create Square Torus"
    bl_options = {'REGISTER', 'UNDO'}  # ★ UX重視: UNDOを復活
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        
        bm = bmesh.new()
        create_square_torus_bmesh(
            bm, 
            square_size=props.square_size, 
            corner_radius=props.corner_radius, 
            minor_radius=props.minor_radius,
            corner_segments=props.corner_segments,
            minor_segments=props.minor_segments
        )
        
        final_matrix = get_transform_matrix(props)
        bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
        
        mesh = bpy.data.meshes.new(f"SquareTorus_Mesh")
        bm.to_mesh(mesh)
        bm.free()
        
        apply_auto_smooth(mesh)
        
        obj = bpy.data.objects.new(f"SqTorus_{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_UniqueSqTorus")
        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'}, "Created Topology-Perfect Square Torus!")
        return {'FINISHED'}

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

        code = target_text.as_string()
        c, l, r = props.torus_color, props.torus_loc, props.torus_rot
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_preview": {props.show_preview},\n'
        new_dict += f'    "show_square_guide": {props.show_square_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'    "square_size": {props.square_size:.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'    "corner_segments": {props.corner_segments},\n'
        new_dict += f'    "minor_segments": {props.minor_segments},\n'
        new_dict += f'    "torus_plane": "{props.torus_plane}",\n'
        new_dict += "}\n"

        try:
            tag_start = "# <BEGIN" + "_DICT>"
            tag_end = "# <END" + "_DICT>"
            
            # ★ 1. 安全な存在チェック
            if tag_start not in code or tag_end not in code:
                self.report({'ERROR'}, "DICT tags missing! Script might be corrupted.")
                return {'CANCELLED'}
            
            # ★ 2. 最大1回のみの安全な分割 (IndexError回避)
            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
            
            # ★ 3. SOURCE_ID_TAG の生存確認 (防御的プログラミング)
            if SOURCE_ID_TAG not in final_code:
                self.report({'ERROR'}, "Critical Error: SOURCE_ID_TAG lost during copy.")
                return {'CANCELLED'}
            
            lines = final_code.split("\n")
            if len(lines) > 0 and lines[0].startswith("# Copied:"):
                time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                lines[0] = f"# Copied: {time_str}"
            
            final_code = "\n".join(lines)
            
            context.window_manager.clipboard = final_code
            self.report({'INFO'}, "Code copied with absolute safety!")
        except Exception as e: 
            self.report({'ERROR'}, f"Copy failed: {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.square_size = 10.0
        p.corner_radius = 0.0
        p.minor_radius = 0.5
        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, "torus_plane")
        col.prop(props, "torus_loc")
        col.prop(props, "torus_rot")
        
        box.separator()
        
        box.prop(props, "show_square_guide", icon='MESH_PLANE')
        col_s = box.column(align=True)
        col_s.prop(props, "square_size")
        
        row_cr = col_s.row()
        row_cr.prop(props, "corner_radius")
        if props.corner_radius <= 0.001:
            row_cr.label(text="[90° Mode]", icon='SNAP_VERTEX')
            
        col_s.prop(props, "minor_radius")
        
        row_seg = box.row()
        row_seg.prop(props, "corner_segments")
        row_seg.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
        col_exec.operator(OT_CreateTorus.bl_idname, icon='MESH_TORUS', text="Create Square Torus")

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_CopyFullScript, OT_Reset, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, 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 19:58: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       = "SquareTorus20260324"
ADDON_NAME   = "zionad 520[ Sq-Torus ]"
TAB_NAME     = "[ Sq Torus copy ]   "
PANEL_TITLE  = "Square Torus Generator"
AUTHOR       = "zionadchat"

SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SQUARE_TORUS_2026_03_24_V6_SAFE ###"

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

bl_info = {
    "name": f"{ADDON_NAME} {PREFIX}",
    "author": AUTHOR,
    "version": (6, 2, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": f"Topology-Perfect Square Torus Generator - {PREFIX}",
    "category": "3D View",
}

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

ADDON_LINKS = (
    {"label": "Prefix トーラス正方形 20260324", "url": "<https://www.notion.so/Prefix-20260324-32df5dacaf4380528980db6a989d6306>"},
)

# ==============================================================================
#  デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_preview": True,
    "show_square_guide": True,
    "torus_color": (0.0391, 0.8000, 0.1647, 0.8000),
    "torus_loc": (0.0000, 0.0000, 0.0000),
    "torus_rot": (0.0000, 0.0000, 0.0000),
    "square_size": 10.0000,
    "corner_radius": 0.0000,
    "minor_radius": 0.5000,
    "corner_segments": 8,
    "minor_segments": 16,
    "torus_plane": "XY",
}
# <END_DICT>

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

PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_OBJ_NAME = f"[Preview] SqTorus_{PREFIX}"
PREVIEW_GUIDE_NAME = f"[Preview] SqGuide_{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_UniqueSqTorus", 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, square_size):
    S = square_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_square_torus_bmesh(bm, square_size, corner_radius, minor_radius, corner_segments, minor_segments):
    square_size = min(max(square_size, 0.01), 10000.0)
    minor_radius = min(max(minor_radius, 0.001), square_size)
    minor_segments = max(minor_segments, 3)
    
    half_size = square_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
                x = cx + actual_corner_radius * math.cos(angle)
                y = cy + actual_corner_radius * math.sin(angle)
                pts.append((mathutils.Vector((x, y, 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
                offset = n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))
                ring.append(bm.verts.new(p + offset))
            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):
            v1 = ring[j]
            v2 = ring[(j + 1) % minor_segments]
            edges.append(bm.edges.new((v1, v2)))
        edge_loops.append(edges)
        
    bm.edges.ensure_lookup_table()

    for i in range(total_rings):
        next_i = (i + 1) % total_rings
        try:
            bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[next_i])
        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 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 create_unique_material(color, name_prefix="Mat_UniqueSqTorus"):
    timestamp = datetime.now().strftime('%M%S%f')[:5] 
    mat_name = f"{name_prefix}_{timestamp}"
    
    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")
        bsdf.location = (0, 0)
        
        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 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:
        create_square_torus_bmesh(
            bm, 
            square_size=props.square_size, 
            corner_radius=props.corner_radius, 
            minor_radius=props.minor_radius,
            corner_segments=props.corner_segments,
            minor_segments=props.minor_segments
        )
        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()
    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_square_guide:
        bm_g = bmesh.new()
        try:
            create_square_guide_bmesh(bm_g, props.square_size)
            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()
        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_square_guide: BoolProperty(name="Show Square Guide", default=CURRENT_DEFAULTS['show_square_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)
    
    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 (Deg)", size=3, default=CURRENT_DEFAULTS['torus_rot'], update=on_update)
    
    square_size: FloatProperty(name="Square Size", default=CURRENT_DEFAULTS['square_size'], 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)
    
    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)

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

class OT_CreateTorus(Operator):
    bl_idname = f"{OP_PREFIX}.create_torus"
    bl_label = "Create Square Torus"
    bl_options = {'REGISTER'}
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        
        bm = bmesh.new()
        create_square_torus_bmesh(
            bm, 
            square_size=props.square_size, 
            corner_radius=props.corner_radius, 
            minor_radius=props.minor_radius,
            corner_segments=props.corner_segments,
            minor_segments=props.minor_segments
        )
        
        final_matrix = get_transform_matrix(props)
        bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
        
        mesh = bpy.data.meshes.new("SquareTorus_Mesh")
        bm.to_mesh(mesh)
        bm.free()
        
        apply_auto_smooth(mesh)
        
        obj = bpy.data.objects.new(f"SqTorus_{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_UniqueSqTorus")
        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'}, "Created Topology-Perfect Square Torus!")
        return {'FINISHED'}

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

        code = target_text.as_string()
        c, l, r = props.torus_color, props.torus_loc, props.torus_rot
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_preview": {props.show_preview},\n'
        new_dict += f'    "show_square_guide": {props.show_square_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'    "square_size": {props.square_size:.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'    "corner_segments": {props.corner_segments},\n'
        new_dict += f'    "minor_segments": {props.minor_segments},\n'
        new_dict += f'    "torus_plane": "{props.torus_plane}",\n'
        new_dict += "}\n"

        try:
            # ★ 文字化け防止策: 正規表現を完全に排除した安全な文字列置換
            tag_start = "# <BEGIN" + "_DICT>"
            tag_end = "# <END" + "_DICT>"
            
            pre_code = code.split(tag_start)[0]
            post_code = code.split(tag_end)[1]
            
            final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
            
            lines = final_code.split("\n")
            if len(lines) > 0 and lines[0].startswith("# Copied:"):
                time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                lines[0] = f"# Copied: {time_str}"
            
            final_code = "\n".join(lines)
            
            context.window_manager.clipboard = final_code
            self.report({'INFO'}, "Code copied safely!")
        except Exception as e: 
            self.report({'ERROR'}, f"Copy failed: {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.square_size = 10.0
        p.corner_radius = 0.0
        p.minor_radius = 0.5
        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, "torus_plane")
        col.prop(props, "torus_loc")
        col.prop(props, "torus_rot")
        
        box.separator()
        
        box.prop(props, "show_square_guide", icon='MESH_PLANE')
        col_s = box.column(align=True)
        col_s.prop(props, "square_size")
        
        row_cr = col_s.row()
        row_cr.prop(props, "corner_radius")
        if props.corner_radius <= 0.001:
            row_cr.label(text="[90° Mode]", icon='SNAP_VERTEX')
            
        col_s.prop(props, "minor_radius")
        
        row_seg = box.row()
        row_seg.prop(props, "corner_segments")
        row_seg.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
        col_exec.operator(OT_CreateTorus.bl_idname, icon='MESH_TORUS', text="Create Square Torus")

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_CopyFullScript, OT_Reset, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, 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 19:11:00
import bpy
import bmesh
import webbrowser
import math
import mathutils
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime

# ==============================================================================
#  【 基本設定エリア 】 先頭で Prefix や 表示名 を変更できます
# ==============================================================================

PREFIX       = "SquareTorus20260324"         # 内部ID用プレフィックス (大文字は自動で小文字化されます)
ADDON_NAME   = "zionad 520[ Sq-Torus ]"     # アドオンの表示名 (環境設定などに表示)
TAB_NAME     = "[ Sq Torus copy ]   "    # 3Dビューサイドバー(Nパネル)のタブ名
PANEL_TITLE  = "Square Torus Generator"     # メインパネルのタイトル名
AUTHOR       = "zionadchat"                 # 作者名

# ★ このスクリプト自身のID (コピー機能でテキスト検索に使用されます)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SQUARE_TORUS_2026_03_24_V4_STABLE ###"

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

bl_info = {
    "name": f"{ADDON_NAME} {PREFIX}",
    "author": AUTHOR,
    "version": (4, 2, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": f"Stable Square Torus Generator - {PREFIX}",
    "category": "3D View",
}

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

ADDON_LINKS = (
    {"label": "Prefix トーラス正方形 20260324", "url": "<https://www.notion.so/Prefix-20260324-32df5dacaf4380528980db6a989d6306>"},
)

# ==============================================================================
#  デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_preview": True,
    "show_square_guide": True,
    "torus_color": (0.0391, 0.8000, 0.1647, 0.8000),
    "torus_loc": (0.0000, 0.0000, 0.0000),
    "torus_rot": (0.0000, 0.0000, 0.0000),
    "square_size": 10.0000,
    "corner_radius": 0.0000,
    "minor_radius": 0.5000,
    "corner_segments": 8,
    "minor_segments": 16,
    "torus_plane": "XY",
}
# <END_DICT>

# ==============================================================================
#  データ クリーンアップ管理 (安定化対応)
# ==============================================================================

PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_OBJ_NAME = f"[Preview] SqTorus_{PREFIX}"
PREVIEW_GUIDE_NAME = f"[Preview] SqGuide_{PREFIX}"
PREVIEW_MESH_NAME = f"PreviewMesh_{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)
    
    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:
        bpy.data.collections.remove(col)

# ==============================================================================
#  数学的 角丸正方形トーラス生成 & ガイド生成
# ==============================================================================

def create_square_guide_bmesh(bm, square_size):
    S = square_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_square_torus_bmesh(bm, square_size, corner_radius, minor_radius, corner_segments, minor_segments):
    half_size = square_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
                x = cx + actual_corner_radius * math.cos(angle)
                y = cy + actual_corner_radius * math.sin(angle)
                nx = math.cos(angle)
                ny = math.sin(angle)
                pts.append((mathutils.Vector((x, y, 0)), mathutils.Vector((nx, ny, 0))))
                
        for p, n in pts:
            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)) + b * (minor_radius * math.sin(theta))
                ring.append(bm.verts.new(p + offset))
            rings.append(ring)
            
    bm.verts.ensure_lookup_table()
    
    total_rings = len(rings)
    for i in range(total_rings):
        next_i = (i + 1) % total_rings
        ring1 = rings[i]
        ring2 = rings[next_i]
        for j in range(minor_segments):
            next_j = (j + 1) % minor_segments
            try:
                # ★ 頂点重複時などのクラッシュ防止
                bm.faces.new((ring1[j], ring2[j], ring2[next_j], ring1[next_j]))
            except ValueError:
                pass
            
    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):
    """ 直角部分の影を綺麗にする(Blender 4.1の非推奨警告対応) """
    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 create_unique_material(color, name_prefix="Mat"):
    timestamp = datetime.now().strftime('%M%S%f')[:5] 
    mat_name = f"{name_prefix}_{timestamp}"
    
    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")
        bsdf.location = (0, 0)
        
        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]
            
    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 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)

    # --- 四角いトーラス本体の更新 ---
    bm = bmesh.new()
    try:
        create_square_torus_bmesh(
            bm, 
            square_size=props.square_size, 
            corner_radius=props.corner_radius, 
            minor_radius=props.minor_radius,
            corner_segments=props.corner_segments,
            minor_segments=props.minor_segments
        )
        bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
        
        mesh = bpy.data.meshes.get(PREVIEW_MESH_NAME)
        if not mesh: 
            mesh = bpy.data.meshes.new(PREVIEW_MESH_NAME)
        else: 
            mesh.clear_geometry()
        bm.to_mesh(mesh)
        apply_auto_smooth(mesh)
        mesh.update()
    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_square_guide:
        bm_g = bmesh.new()
        try:
            create_square_guide_bmesh(bm_g, props.square_size)
            bmesh.ops.transform(bm_g, matrix=final_matrix, verts=bm_g.verts)
            
            mesh_g = bpy.data.meshes.get(PREVIEW_MESH_NAME + "_Guide")
            if not mesh_g: 
                mesh_g = bpy.data.meshes.new(PREVIEW_MESH_NAME + "_Guide")
            else: 
                mesh_g.clear_geometry()
            bm_g.to_mesh(mesh_g)
            mesh_g.update()
        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
def delayed_update():
    """ 安全にコンテキストをチェックしてからプレビューを更新する """
    global _timer
    _timer = None
    
    ctx = bpy.context
    if not ctx or not ctx.scene:
        return None
        
    # 編集モード等でのクラッシュ防止
    if ctx.mode != 'OBJECT':
        return None

    update_preview_geometry(ctx)
    return None

def on_update(self, context):
    global _timer
    if _timer: 
        try: 
            bpy.app.timers.unregister(_timer)
        except Exception: 
            pass
        _timer = None
    _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)

# ==============================================================================
#  PROPERTIES (フリーズ防止の soft_max 導入)
# ==============================================================================

class PG_TorusProps(PropertyGroup):
    show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
    show_square_guide: BoolProperty(name="Show Square Guide", default=CURRENT_DEFAULTS['show_square_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)
    
    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 (Deg)", size=3, default=CURRENT_DEFAULTS['torus_rot'], update=on_update)
    
    square_size: FloatProperty(name="Square Size", default=CURRENT_DEFAULTS['square_size'], min=0.1, update=on_update)
    corner_radius: FloatProperty(name="Corner Radius", default=CURRENT_DEFAULTS['corner_radius'], min=0.0, update=on_update)
    minor_radius: FloatProperty(name="Tube Thickness", default=CURRENT_DEFAULTS['minor_radius'], min=0.01, 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)

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

class OT_CreateTorus(Operator):
    bl_idname = f"{OP_PREFIX}.create_torus"
    bl_label = "Create Square Torus"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        
        bm = bmesh.new()
        create_square_torus_bmesh(
            bm, 
            square_size=props.square_size, 
            corner_radius=props.corner_radius, 
            minor_radius=props.minor_radius,
            corner_segments=props.corner_segments,
            minor_segments=props.minor_segments
        )
        
        final_matrix = get_transform_matrix(props)
        bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
        
        mesh = bpy.data.meshes.new(f"SquareTorus_Mesh")
        bm.to_mesh(mesh)
        bm.free()
        
        apply_auto_smooth(mesh)
        
        obj = bpy.data.objects.new(f"SqTorus_{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_UniqueSqTorus")
        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'}, "Created Square Torus!")
        return {'FINISHED'}

class OT_CopyFullScript(Operator):
    bl_idname = f"{OP_PREFIX}.copy_script"
    bl_label = "Copy Script"
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        target_text = None
        for t in bpy.data.texts:
            if SOURCE_ID_TAG in t.as_string(): target_text = t; break
        
        # ★ ユーザーへの丁寧なエラー通知
        if not target_text:
            self.report({'WARNING'}, "Source script not found in Text Editor. Please keep the script in Blender's Text Editor to use 'Copy Script'.")
            return {'CANCELLED'}

        code = target_text.as_string()
        c, l, r = props.torus_color, props.torus_loc, props.torus_rot
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_preview": {props.show_preview},\n'
        new_dict += f'    "show_square_guide": {props.show_square_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'    "square_size": {props.square_size:.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'    "corner_segments": {props.corner_segments},\n'
        new_dict += f'    "minor_segments": {props.minor_segments},\n'
        new_dict += f'    "torus_plane": "{props.torus_plane}",\n'
        new_dict += "}\n"

        try:
            start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
            pre, post = code.split(start)[0], code.split(end)[1]
            # ★ タイムスタンプフォーマット変更
            final = f"# Copied: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
            context.window_manager.clipboard = final
            self.report({'INFO'}, "Code copied!")
        except: 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.square_size = 10.0
        p.corner_radius = 0.0
        p.minor_radius = 0.5
        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, "torus_plane")
        col.prop(props, "torus_loc")
        col.prop(props, "torus_rot")
        
        box.separator()
        
        box.prop(props, "show_square_guide", icon='MESH_PLANE')
        col_s = box.column(align=True)
        col_s.prop(props, "square_size")
        
        row_cr = col_s.row()
        row_cr.prop(props, "corner_radius")
        if props.corner_radius <= 0.001:
            row_cr.label(text="[90° Mode]", icon='SNAP_VERTEX')
            
        col_s.prop(props, "minor_radius")
        
        row_seg = box.row()
        row_seg.prop(props, "corner_segments")
        row_seg.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
        col_exec.operator(OT_CreateTorus.bl_idname, icon='MESH_TORUS', text="Create Square Torus")

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_CopyFullScript, OT_Reset, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, 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: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_TorusProps))
    bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)

def unregister():
    # ★ 安定化: unregister時のタイマー解除とデータのクリーンアップ
    global _timer
    if _timer:
        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): 
        bpy.utils.unregister_class(c)

if __name__ == "__main__": 
    register()