blender Million 2026

Prefix トーラス正方形 20260324










# Copied: 10:35: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_V3 ###"

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

bl_info = {
    "name": f"{ADDON_NAME} {PREFIX}",
    "author": AUTHOR,
    "version": (4, 1, 1),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": f"Unique Material 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>

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

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 =[]
    
    # ★ Corner Radius が 0 の場合、完全な直角(90度)で斜めに繋がるよう生成
    if actual_corner_radius <= 0.001:
        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) # 直角接合のためのスケール(約1.414倍)
        
        for p, n in corners:
            b = mathutils.Vector((0, 0, 1)) # Z方向
            ring =[]
            for j in range(minor_segments):
                theta = j * 2.0 * math.pi / minor_segments
                # 法線方向を √2 倍に伸ばして斜め接合(マイタージョイント)を実現
                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 =[]
        # 0: 右上, 1: 左上, 2: 左下, 3: 右下 の4つの角を順に生成
        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
            bm.faces.new((ring1[j], ring2[j], ring2[next_j], ring1[next_j]))
            
    # 滑らかに表示
    for f in bm.faces:
        f.smooth = True
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm

def apply_auto_smooth(mesh):
    """ 直角部分の影を綺麗にするための自動スムース設定 """
    try:
        if hasattr(mesh, "use_auto_smooth"):
            mesh.use_auto_smooth = True
            mesh.auto_smooth_angle = math.radians(30)
    except: 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

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

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 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)
        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
    if bpy.context and bpy.context.scene: update_preview_geometry(bpy.context)
    return None

def on_update(self, context):
    global _timer
    if _timer: 
        try: bpy.app.timers.unregister(_timer)
        except: pass
    _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, 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, update=on_update)
    minor_segments: IntProperty(name="Tube Segs", default=CURRENT_DEFAULTS['minor_segments'], min=3, 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({'ERROR'}, "Script source not found.")
            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('%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")
        
        # 角の丸み (0で直角になることを明示)
        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():
    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()
# Copied: 15:00:01
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       = "SquareTorus20260227"         # 内部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_02_27_V2 ###"

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

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

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

ADDON_LINKS = (
    {"label": "Code Copy Template", "url": "<https://www.notion.so/Code-copy-20260221>"},
    {"label": "Theory Background", "url": "<https://www.notion.so/Einstein-from-20260119>"},
)

# ==============================================================================
#  デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <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>

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

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 =[]
    
    # ★ Corner Radius が 0 の場合、完全な直角(90度)で斜めに繋がるよう生成
    if actual_corner_radius <= 0.001:
        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) # 直角接合のためのスケール(約1.414倍)
        
        for p, n in corners:
            b = mathutils.Vector((0, 0, 1)) # Z方向
            ring =[]
            for j in range(minor_segments):
                theta = j * 2.0 * math.pi / minor_segments
                # 法線方向を √2 倍に伸ばして斜め接合(マイタージョイント)を実現
                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 =[]
        # 0: 右上, 1: 左上, 2: 左下, 3: 右下 の4つの角を順に生成
        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
            bm.faces.new((ring1[j], ring2[j], ring2[next_j], ring1[next_j]))
            
    # 滑らかに表示
    for f in bm.faces:
        f.smooth = True
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm

def apply_auto_smooth(mesh):
    """ 直角部分の影を綺麗にするための自動スムース設定 """
    try:
        if hasattr(mesh, "use_auto_smooth"):
            mesh.use_auto_smooth = True
            mesh.auto_smooth_angle = math.radians(30)
    except: 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

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

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 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)
        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
    if bpy.context and bpy.context.scene: update_preview_geometry(bpy.context)
    return None

def on_update(self, context):
    global _timer
    if _timer: 
        try: bpy.app.timers.unregister(_timer)
        except: pass
    _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, 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, update=on_update)
    minor_segments: IntProperty(name="Tube Segs", default=CURRENT_DEFAULTS['minor_segments'], min=3, 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({'ERROR'}, "Script source not found.")
            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('%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")
        
        # 角の丸み (0で直角になることを明示)
        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():
    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()
「Corner Radius(角の丸み)」を 0 に設定した際に、完全な90度(直角)の枠が生成されるように専用の計算ロジックを追加しました。
単純に丸みをゼロにすると角が潰れてしまうため、額縁のように斜め45度で綺麗に接合される「マイタージョイント」方式を自動で計算するようにしています。
また、直角になった角の影が綺麗に表示されるように「自動スムース(Auto Smooth)」の処理も内部で追加しました。
# Copied: 15:00:01
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       = "Torus20260227"              # 内部ID用プレフィックス (大文字は自動で小文字化されます)
ADDON_NAME   = "zionad 520[ Torus Gen ]"    # アドオンの表示名 (環境設定などに表示)
TAB_NAME     = "   [ Torus copy ]   "       # 3Dビューサイドバー(Nパネル)のタブ名
PANEL_TITLE  = "Torus Generator"            # メインパネルのタイトル名
AUTHOR       = "zionadchat"                 # 作者名

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

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

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

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

ADDON_LINKS = (
    {"label": "Code Copy Template", "url": "<https://www.notion.so/Code-copy-20260221>"},
    {"label": "Theory Background", "url": "<https://www.notion.so/Einstein-from-20260119>"},
)

# ==============================================================================
#  デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_preview": 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),
    "torus_major_radius": 5.0000,
    "torus_minor_radius": 1.0000,
    "torus_major_segments": 48,
    "torus_minor_segments": 16,
    "torus_plane": "XY",
}
# <END_DICT>

# ==============================================================================
#  数学的トーラス生成 (bmesh.opsにtorusが無いため自作)
# ==============================================================================

def create_torus_bmesh(bm, major_segments, minor_segments, major_radius, minor_radius):
    """ メモリ上で直接トーラスの頂点と面を計算して生成する超軽量関数 """
    verts =[]
    # 頂点の生成
    for i in range(major_segments):
        phi = i * 2.0 * math.pi / major_segments
        cos_phi = math.cos(phi)
        sin_phi = math.sin(phi)
        
        row =[]
        for j in range(minor_segments):
            theta = j * 2.0 * math.pi / minor_segments
            cos_theta = math.cos(theta)
            sin_theta = math.sin(theta)
            
            x = (major_radius + minor_radius * cos_theta) * cos_phi
            y = (major_radius + minor_radius * cos_theta) * sin_phi
            z = minor_radius * sin_theta
            
            row.append(bm.verts.new((x, y, z)))
        verts.append(row)
        
    bm.verts.ensure_lookup_table()
    
    # 面(ポリゴン)の生成
    for i in range(major_segments):
        for j in range(minor_segments):
            v1 = verts[i][j]
            v2 = verts[(i + 1) % major_segments][j]
            v3 = verts[(i + 1) % major_segments][(j + 1) % minor_segments]
            v4 = verts[i][(j + 1) % minor_segments]
            bm.faces.new((v1, v2, v3, v4))
            
    # スムーズシェード化と法線計算
    for f in bm.faces:
        f.smooth = True
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    
    return bm

# ==============================================================================
#  マテリアル作成ロジック (実体化用)
# ==============================================================================

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'
    mat.diffuse_color = color
    
    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

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

PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_OBJ_NAME = f"[Preview] Torus_{PREFIX}"
PREVIEW_MESH_NAME = f"PreviewMesh_{PREFIX}"
PREVIEW_MAT_NAME = f"PreviewMat_{PREFIX}"

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):
    mat.diffuse_color = 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_torus_transform_matrix(props):
    rot_matrix = mathutils.Matrix.Identity(4)
    # 平面設定 (ベースはXY平面)
    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')
        
    # XYZ ユーザー回転
    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)
        context.scene.collection.children.link(col)
    
    obj = bpy.data.objects.get(PREVIEW_OBJ_NAME)

    if not props.show_preview:
        if obj: bpy.data.objects.remove(obj, do_unlink=True)
        return

    bm = bmesh.new()
    try:
        # 自作のトーラス生成関数を呼び出し
        create_torus_bmesh(
            bm, 
            major_segments=props.torus_major_segments, 
            minor_segments=props.torus_minor_segments, 
            major_radius=props.torus_major_radius, 
            minor_radius=props.torus_minor_radius
        )
        
        final_matrix = get_torus_transform_matrix(props)
        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)
        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

_timer = None
def delayed_update():
    global _timer
    _timer = None
    if bpy.context and bpy.context.scene: update_preview_geometry(bpy.context)
    return None

def on_update(self, context):
    global _timer
    if _timer: 
        try: bpy.app.timers.unregister(_timer)
        except: pass
    _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)
    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)
    
    torus_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['torus_major_radius'], min=0.01, update=on_update)
    torus_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['torus_minor_radius'], min=0.01, update=on_update)
    
    torus_major_segments: IntProperty(name="Major Segments", default=CURRENT_DEFAULTS['torus_major_segments'], min=3, update=on_update)
    torus_minor_segments: IntProperty(name="Minor Segments", default=CURRENT_DEFAULTS['torus_minor_segments'], min=3, update=on_update)

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

class OT_CreateTorus(Operator):
    bl_idname = f"{OP_PREFIX}.create_torus"
    bl_label = "Create Torus"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        
        bm = bmesh.new()
        
        # 自作のトーラス生成関数を呼び出し
        create_torus_bmesh(
            bm, 
            major_segments=props.torus_major_segments, 
            minor_segments=props.torus_minor_segments, 
            major_radius=props.torus_major_radius, 
            minor_radius=props.torus_minor_radius
        )
        
        final_matrix = get_torus_transform_matrix(props)
        bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
        
        mesh = bpy.data.meshes.new(f"Torus_Mesh")
        bm.to_mesh(mesh)
        bm.free()
        
        obj = bpy.data.objects.new(f"Torus_{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_UniqueTorus")
        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 Torus with Unique Material")
        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({'ERROR'}, "Script source not found.")
            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'    "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'    "torus_major_radius": {props.torus_major_radius:.4f},\n'
        new_dict += f'    "torus_minor_radius": {props.torus_minor_radius:.4f},\n'
        new_dict += f'    "torus_major_segments": {props.torus_major_segments},\n'
        new_dict += f'    "torus_minor_segments": {props.torus_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('%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.torus_major_radius = 5.0
        p.torus_minor_radius = 1.0
        p.torus_major_segments = 48
        p.torus_minor_segments = 16
        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")
        
        row_r = box.row()
        row_r.prop(props, "torus_major_radius", text="Maj Radius")
        row_r.prop(props, "torus_minor_radius", text="Min Radius")
        
        row_s = box.row()
        row_s.prop(props, "torus_major_segments", text="Maj Segs")
        row_s.prop(props, "torus_minor_segments", text="Min Segs")
        
        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 Torus (Unique)")

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():
    """ 登録時に3Dビューのサイドバーを自動で開く """
    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))
    # 0.1秒後にサイドバーを開く
    bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)

def unregister():
    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()