blender Million 2026

エディタに アドオン貼り 202260325

https://note.com/zionadmillion/n/n5d271c94fa74











bl_info = {
    "name": "zionad 520[ Sq-Torus ] SquareTorus20260324",
    "author": "zionadchat",
    "version": (7, 0, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": "Topology-Perfect Square Torus Generator & Script Loader",
    "category": "3D View",
}

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     = "[ addon text editor ]  "
PANEL_TITLE  = "Square Torus Generator"
AUTHOR       = "zionadchat"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SQUARE_TORUS_2026_03_24_V7_FINAL ###"

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>

# ==============================================================================
#  【 内包する追加スクリプトの文字列定義 】
#   ※以下の指定された場所に、対象のスクリプトをペーストしてください。
# ==============================================================================

# ① 図形ジェネレーター (b5200_zukkei...) のコードを以下に貼り付け
ZUKKEI_SCRIPT_CONTENT = r'''
# ▼▼▼ ここに「図形&配列ジェネレーター」の全コードを貼り付けてください ▼▼▼

#20260319 合体版
import bpy
import bmesh
import math
import random
import datetime
import webbrowser
from mathutils import Vector, Euler, Matrix
from bpy.props import FloatProperty, FloatVectorProperty, EnumProperty, IntProperty, BoolProperty, StringProperty, PointerProperty
from bpy.types import Operator, Panel, PropertyGroup

# ==============================================================================
#  設定エリア & ID管理
# ==============================================================================

PREFIX = "B5200_Zukkei_Array_View_20260319_v2"
TAB_NAME = "   b5200[  図形作成  ]   "
OP_PREFIX = "b200_zukkei"
PROPS_NAME = f"{OP_PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: B5200_ZUKKEI_ARRAY_6_16_0 ###"

# 透視投影 視座関連の定数パラメーター
VIEW_RESET_BTN_TEXT = "Reset View (0, -10, 10)"  
VIEW_POS_INIT = (0.0, -10.0, 10.0)

bl_info = {
    "name": f"zionad b5200 Zukkei & Array & View {PREFIX}",
    "author": "zionadchat",
    "version": (6, 17, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar",
    "description": "視座コントロール & リアルタイムプレビュー対応 実体化切り離し機能付き図形・配列ジェネレーター",
    "category": "3D View",
}

# ==============================================================================
#  リンク集
# ==============================================================================

THIS_LINKS =[
    {"label": "b5200 図形作成 20260317版", "url": "<https://www.notion.so/b5200-20260317-326f5dacaf4380b4ad6afb3fe0f9e619>"},
    {"label": "b200 図形作成 20250721", "url": "<https://memo2017.hatenablog.com/entry/2025/07/21/115312>"},
    {"label": "カメラ 固定 Git 管理 20250711", "url": "<https://memo2017.hatenablog.com/entry/2025/07/11/131157>"},
]
NEW_DOC_LINKS =[
    {"label": "blender アドオン 公開", "url": "<https://ivory-handsaw-95b.notion.site/blender-230b3deba7a280d7b610e0e3cdc178da>"},
    {"label": "完成品 目次", "url": "<https://mokuji000zionad.hatenablog.com/entry/2025/05/30/135936>"},
]
DOC_LINKS =[
    {"label": "812 地球儀 経度 緯度でのコントロール 20250302", "url": "<https://sortphotos2025.hatenablog.jp/entry/2025/03/02/211757>"},
    {"label": "アドオン目次 from 20250227", "url": "<https://sortphotos2025.hatenablog.jp/entry/2025/02/27/201251>"},
]
SOCIAL_LINKS =[
    {"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
    {"label": "Posfie zionad2022", "url": "<https://posfie.com/t/zionad2022>"},
    {"label": "X (Twitter) zionadchat", "url": "<https://x.com/zionadchat>"},
]

# ==============================================================================
#  デフォルト値設定 (コピー機能で書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "slider_limit": 300.0000,
    "view_pos": (0.0000, -10.0000, 10.0000),

    "show_preview": True,
    "collection_name": "MyCollection",
    "sub_collection_name": "Generated_Shapes",
    "object_name_prefix": "Shape",
    
    "primitive_type": "CUBE",
    "use_solidify": False,
    "solidify_thickness": 0.1000,
    
    "cube_size": 2.0000,
    "cuboid_dimensions": (2.0000, 2.0000, 2.0000),
    "plane_size_x": 2.0000,
    "plane_size_y": 2.0000,
    "circle_radius": 1.0000,
    "circle_vertices": 32,
    "ellipse_radius_x": 1.0000,
    "ellipse_radius_y": 0.5000,
    "ellipse_vertices": 64,
    "uv_sphere_radius": 1.0000,
    "uv_sphere_segments": 32,
    "uv_sphere_rings": 16,
    "ico_sphere_radius": 1.0000,
    "ico_sphere_subdivisions": 2,
    
    "cylinder_radius": 1.0000,
    "cylinder_mode": "CENTER",
    "cylinder_depth": 2.0000,
    "cylinder_point_top": (0.0000, 0.0000, 2.0000),
    "cylinder_point_bottom": (0.0000, 0.0000, 0.0000),
    "cylinder_vertices": 32,
    "cylinder_cap_top": False,
    "cylinder_cap_bottom": False,
    
    "frustum_mode": "CENTER",
    "frustum_radius_top": 0.0000,
    "frustum_radius_bottom": 1.0000,
    "frustum_height": 2.0000,
    "frustum_vertices": 32,
    "frustum_cap_top": False,
    "frustum_cap_bottom": False,
    "frustum_point_top": (0.0000, 0.0000, 2.0000),
    "frustum_point_bottom": (0.0000, 0.0000, 0.0000),
    
    "image_cylinder_radius": 30.0000,
    "image_cylinder_depth": 60.0000,
    "image_cylinder_vertices": 64,
    "image_cylinder_uv_offset": (0.0000, 0.0000),
    "image_cylinder_uv_scale": (1.0000, 1.0000),
    "image_cylinder_alpha_outer": 0.0000,
    "image_cylinder_alpha_inner": 1.0000,
    
    "cube_frame_size": 2.0000,
    "cube_frame_radius": 0.0500,
    "cube_frame_vertices": 16,
    
    "cuboid_frame_dimensions": (2.0000, 2.0000, 2.0000),
    "cuboid_frame_radius": 0.0500,
    "cuboid_frame_vertices": 16,

    "torus_major_radius": 1.0000,
    "torus_minor_radius": 0.2500,
    "torus_major_segments": 48,
    "torus_minor_segments": 12,
    "monkey_size": 1.0000,

    "sphere_spacing": 0.5000,
    "sphere_elem_radius": 0.1000,
    "sphere_size": 4.0000,
    "sphere_radius_val": 2.0000,
    "sphere_segments": 12,
    "sphere_rings": 6,

    "torus_count": 5,
    "torus_spacing": 1.0000,
    "torus_minor_radius_arr": 0.1000,

    "grid_count_x": 5,
    "grid_count_y": 5,
    "grid_count_z": 5,
    "grid_spacing_x": 1.0000,
    "grid_spacing_y": 1.0000,
    "grid_spacing_z": 1.0000,
    "grid_radius": 0.0500,
    "grid_vertices": 16,
    
    "location": (0.0000, 0.0000, 0.0000),
    "scale_uniform": True,
    "scale_factor": 1.0000,
    "scale_vector": (1.0000, 1.0000, 1.0000),
    "additional_rotation_x": 0.0000,
    "additional_rotation_y": 0.0000,
    "additional_rotation_z": 0.0000,
    
    "color_mode": "PRESET",
    "preset_set": "A",
    "preset_color": "1",
    "custom_color": (0.8000, 0.8000, 0.8000, 1.0000),
    "face_alpha": 1.0000,
    
    "preset_color_1": (0.9000, 0.2000, 0.2000, 1.0000),
    "preset_color_2": (1.0000, 0.5000, 0.1000, 1.0000),
    "preset_color_3": (1.0000, 0.8000, 0.0000, 1.0000),
    "preset_color_4": (0.4000, 0.8000, 0.2000, 1.0000),
    "preset_color_5": (0.1000, 0.7000, 0.8000, 1.0000),
    "preset_color_6": (0.2000, 0.4000, 0.9000, 1.0000),
    "preset_color_7": (0.6000, 0.3000, 0.8000, 1.0000),
    "preset_color_8": (1.0000, 0.9000, 0.6000, 1.0000),
    "preset_color_9": (0.1000, 0.5000, 0.4000, 1.0000),
    "preset_color_10": (0.7000, 0.4000, 0.2000, 1.0000),
    "preset_color_b_1": (0.9000, 0.1000, 0.1000, 1.0000),
    "preset_color_b_2": (1.0000, 0.5000, 0.2000, 1.0000),
    "preset_color_b_3": (0.9000, 0.9000, 0.1000, 1.0000),
    "preset_color_b_4": (0.4000, 0.9000, 0.1000, 1.0000),
    "preset_color_b_5": (0.1000, 0.9000, 0.9000, 1.0000),
    "preset_color_b_6": (0.1000, 0.1000, 0.9000, 1.0000),
    "preset_color_b_7": (0.6000, 0.1000, 0.9000, 1.0000),
    "preset_color_b_8": (0.9500, 0.9500, 0.8000, 1.0000),
    "preset_color_b_9": (0.1000, 0.6000, 0.6000, 1.0000),
    "preset_color_b_10": (0.2500, 0.5000, 0.1500, 1.0000),
    "preset_color_c_1": (0.6000, 1.0000, 0.6000, 1.0000),
    "preset_color_c_2": (0.3000, 0.9000, 0.4000, 1.0000),
    "preset_color_c_3": (0.1000, 0.7000, 0.2000, 1.0000),
    "preset_color_c_4": (0.0000, 0.5000, 0.1000, 1.0000),
    "preset_color_c_5": (0.5000, 0.8000, 0.2000, 1.0000),
    "preset_color_c_6": (0.2000, 0.6000, 0.5000, 1.0000),
    "preset_color_c_7": (0.7000, 0.9000, 0.3000, 1.0000),
    "preset_color_c_8": (0.4000, 0.7000, 0.7000, 1.0000),
    "preset_color_c_9": (0.0000, 0.8000, 0.6000, 1.0000),
    "preset_color_c_10": (0.1000, 0.3000, 0.1000, 1.0000),

    "show_main_docs": True,
    "show_new_docs": True,
    "show_old_docs": False,
    "show_social": False,
}
# <END_DICT>

# ==============================================================================
#  透視投影 ビュー同期ロジック
# ==============================================================================
_is_updating_view = False

def update_view_position(self, context):
    global _is_updating_view
    if _is_updating_view: return
    
    props = getattr(context.scene, PROPS_NAME, None)
    if not props: return

    limit = props.slider_limit
    v = list(props.view_pos)
    clamped = False
    for i in range(3):
        if v[i] > limit: v[i] = limit; clamped = True
        elif v[i] < -limit: v[i] = -limit; clamped = True
            
    if clamped:
        _is_updating_view = True
        props.view_pos = v
        _is_updating_view = False
    
    _is_updating_view = True
    try:
        cam_pos = Vector(props.view_pos)
        for window in 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':
                            r3d = space.region_3d
                            r3d.view_perspective = 'PERSP'
                            target_pos = Vector(r3d.view_location)
                            rel_pos = cam_pos - target_pos
                            dist = rel_pos.length
                            if dist > 0.001:
                                r3d.view_distance = dist
                                r3d.view_rotation = rel_pos.to_track_quat('Z', 'Y')
    finally:
        _is_updating_view = False

def view_sync_timer():
    global _is_updating_view
    if _is_updating_view: return 0.05
    
    context = bpy.context
    if getattr(context, "scene", None) is None: return 0.05
    props = getattr(context.scene, PROPS_NAME, None)
    if not props: return 0.05

    r3d = None
    target_area = None
    for window in 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':
                        r3d = space.region_3d
                        target_area = area
                        break
                if r3d: break
        if r3d: break

    if r3d and target_area:
        target_pos = Vector(r3d.view_location)
        actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
        
        current_pos = Vector(props.view_pos)
        if (current_pos - actual_cam_pos).length > 0.001:
            _is_updating_view = True
            max_val = max(abs(actual_cam_pos.x), abs(actual_cam_pos.y), abs(actual_cam_pos.z))
            if max_val > props.slider_limit:
                props.slider_limit = max_val + 50.0 
                
            props.view_pos = actual_cam_pos
            _is_updating_view = False
            target_area.tag_redraw()
            
    return 0.05

# ==============================================================================
#  コアロジック (図形メッシュ生成 & プレビュー管理)
# ==============================================================================

PREVIEW_COL_NAME = f"{OP_PREFIX}_Preview_Zone"
PREVIEW_TAG = f"{OP_PREFIX}_preview_tag"

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

def post_creation_cap_handling(obj, cap_top, cap_bottom, direction=None):
    if cap_top and cap_bottom: return
    if not obj or obj.type != 'MESH': return
    if not cap_top and not cap_bottom: return
    
    bpy.ops.object.mode_set(mode='EDIT')
    bpy.ops.mesh.select_all(action='DESELECT')
    
    bm = bmesh.from_edit_mesh(obj.data)
    bm.faces.ensure_lookup_table()
    
    top_face, bottom_face = None, None
    max_proj, min_proj = -float('inf'), float('inf')
    world_matrix = obj.matrix_world
    
    if direction is None:
        direction = Vector((0, 0, 1))
    else:
        direction = direction.normalized()
    
    for face in bm.faces:
        face_center_world = world_matrix @ face.calc_center_median()
        proj = face_center_world.dot(direction)
        if proj > max_proj: 
            max_proj = proj
            top_face = face
        if proj < min_proj: 
            min_proj = proj
            bottom_face = face
            
    faces_to_delete =[]
    if not cap_top and top_face: faces_to_delete.append(top_face)
    if not cap_bottom and bottom_face: faces_to_delete.append(bottom_face)
    
    if faces_to_delete:
        bmesh.ops.delete(bm, geom=faces_to_delete, context='FACES')
        bmesh.update_edit_mesh(obj.data)
        
    bpy.ops.object.mode_set(mode='OBJECT')

def setup_sphere_instances_node_tree(obj, props):
    """ポイント上に球体を爆速でインスタンス化するジオメトリノードを構築"""
    modifier_name = "Array_GeoNodes"
    mod = obj.modifiers.get(modifier_name)
    if not mod:
        mod = obj.modifiers.new(name=modifier_name, type='NODES')
    
    node_tree = bpy.data.node_groups.new(name="SphereArray_Nodes", type='GeometryNodeTree')
    mod.node_group = node_tree
    
    # 5.0対応 Group Input/Output
    node_in = node_tree.nodes.new('NodeGroupInput')
    node_tree.interface.new_socket(name="Geometry", in_out='INPUT', socket_type='NodeSocketGeometry')
    
    node_out = node_tree.nodes.new('NodeGroupOutput')
    node_tree.interface.new_socket(name="Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry')
    
    node_inst = node_tree.nodes.new('GeometryNodeInstanceOnPoints')
    
    node_sphere = node_tree.nodes.new('GeometryNodeMeshUVSphere')
    node_sphere.inputs['Radius'].default_value = props.sphere_elem_radius
    node_sphere.inputs['Segments'].default_value = props.sphere_segments
    node_sphere.inputs['Rings'].default_value = props.sphere_rings
    
    node_realize = node_tree.nodes.new('GeometryNodeRealizeInstances')
    node_set_mat = node_tree.nodes.new('GeometryNodeSetMaterial')
    
    # リンク接続
    node_tree.links.new(node_in.outputs['Geometry'], node_inst.inputs['Points'])
    node_tree.links.new(node_sphere.outputs['Mesh'], node_inst.inputs['Instance'])
    node_tree.links.new(node_inst.outputs['Instances'], node_realize.inputs['Geometry'])
    node_tree.links.new(node_realize.outputs['Geometry'], node_set_mat.inputs['Geometry'])
    node_tree.links.new(node_set_mat.outputs['Geometry'], node_out.inputs['Geometry'])

def create_primitive_object(context, props, name):
    old_objs = set(bpy.data.objects)
    prim_type = props.primitive_type
    use_panel_transform = True

    win = context.window_manager.windows[0]
    area = next((a for a in win.screen.areas if a.type == 'VIEW_3D'), None)
    region = next((r for r in area.regions if r.type == 'WINDOW'), None) if area else None

    try:
        with context.temp_override(window=win, area=area, region=region):
            if prim_type == 'CUBE':
                bpy.ops.mesh.primitive_cube_add(size=props.cube_size, align='WORLD', location=(0, 0, 0))
            elif prim_type == 'CUBOID':
                bpy.ops.mesh.primitive_cube_add(size=1.0, align='WORLD', location=(0, 0, 0))
                context.active_object.dimensions = props.cuboid_dimensions
            elif prim_type == 'PLANE':
                bpy.ops.mesh.primitive_plane_add(size=1.0, align='WORLD', location=(0, 0, 0))
                context.active_object.dimensions = (props.plane_size_x, props.plane_size_y, 0)
            elif prim_type == 'CIRCLE':
                bpy.ops.mesh.primitive_circle_add(vertices=props.circle_vertices, radius=props.circle_radius, fill_type='NGON', align='WORLD', location=(0, 0, 0))
            elif prim_type == 'ELLIPSE':
                bpy.ops.mesh.primitive_circle_add(vertices=props.ellipse_vertices, radius=1.0, fill_type='NGON', align='WORLD', location=(0, 0, 0))
                context.active_object.scale.x = props.ellipse_radius_x
                context.active_object.scale.y = props.ellipse_radius_y
            elif prim_type == 'UV_SPHERE':
                bpy.ops.mesh.primitive_uv_sphere_add(radius=props.uv_sphere_radius, segments=props.uv_sphere_segments, ring_count=props.uv_sphere_rings, align='WORLD', location=(0, 0, 0))
            elif prim_type == 'ICO_SPHERE':
                bpy.ops.mesh.primitive_ico_sphere_add(radius=props.ico_sphere_radius, subdivisions=props.ico_sphere_subdivisions, align='WORLD', location=(0, 0, 0))
            
            elif prim_type == 'CYLINDER':
                cap_ends = props.cylinder_cap_top or props.cylinder_cap_bottom
                direction = Vector((0, 0, 1))
                
                if props.cylinder_mode == 'CENTER':
                    bpy.ops.mesh.primitive_cylinder_add(vertices=props.cylinder_vertices, radius=props.cylinder_radius, depth=props.cylinder_depth, end_fill_type='NGON' if cap_ends else 'NOTHING', align='WORLD', location=(0, 0, 0))
                elif props.cylinder_mode == 'POINTS_NORMAL':
                    use_panel_transform = False
                    p_top, p_bottom = Vector(props.cylinder_point_top), Vector(props.cylinder_point_bottom)
                    direction = p_top - p_bottom
                    height = direction.length
                    if height < 1e-4: return None
                    bpy.ops.mesh.primitive_cylinder_add(vertices=props.cylinder_vertices, radius=props.cylinder_radius, depth=height, end_fill_type='NGON' if cap_ends else 'NOTHING', align='WORLD', location=(0, 0, 0))
                    obj = context.active_object
                    obj.location = (p_top + p_bottom) / 2
                    obj.rotation_euler = direction.to_track_quat('Z', 'Y').to_euler('XYZ')
                elif props.cylinder_mode == 'POINTS_HORIZONTAL':
                    use_panel_transform = False
                    p_top, p_bottom = Vector(props.cylinder_point_top), Vector(props.cylinder_point_bottom)
                    direction = p_top - p_bottom
                    dz = direction.z
                    if abs(dz) < 1e-4: return None
                    height = abs(dz)
                    
                    bpy.ops.mesh.primitive_cylinder_add(vertices=props.cylinder_vertices, radius=props.cylinder_radius, depth=height, end_fill_type='NGON' if cap_ends else 'NOTHING', align='WORLD', location=(0, 0, 0))
                    obj = context.active_object
                    
                    dx, dy = direction.x, direction.y
                    sign_z = 1 if dz > 0 else -1
                    bpy.ops.object.mode_set(mode='EDIT')
                    bm = bmesh.from_edit_mesh(obj.data)
                    for v in bm.verts:
                        factor = v.co.z / height
                        v.co.x += factor * dx * sign_z
                        v.co.y += factor * dy * sign_z
                    bmesh.update_edit_mesh(obj.data)
                    bpy.ops.object.mode_set(mode='OBJECT')
                    obj.location = (p_top + p_bottom) / 2
                    
                post_creation_cap_handling(context.active_object, props.cylinder_cap_top, props.cylinder_cap_bottom, direction)
            
            elif prim_type == 'FRUSTUM':
                cap_ends = props.frustum_cap_top or props.frustum_cap_bottom
                direction = Vector((0, 0, 1))
                radius_bottom, radius_top = props.frustum_radius_bottom, props.frustum_radius_top
                
                if props.frustum_mode == 'CENTER':
                    bpy.ops.mesh.primitive_cone_add(vertices=props.frustum_vertices, radius1=radius_bottom, radius2=radius_top, depth=props.frustum_height, end_fill_type='NGON' if cap_ends else 'NOTHING', align='WORLD', location=(0, 0, 0))
                elif props.frustum_mode == 'POINTS_NORMAL':
                    use_panel_transform = False
                    p_top, p_bottom = Vector(props.frustum_point_top), Vector(props.frustum_point_bottom)
                    direction = p_top - p_bottom
                    height = direction.length
                    if height < 1e-4: return None
                    bpy.ops.mesh.primitive_cone_add(vertices=props.frustum_vertices, radius1=radius_bottom, radius2=radius_top, depth=height, end_fill_type='NGON' if cap_ends else 'NOTHING', align='WORLD', location=(0, 0, 0))
                    obj = context.active_object
                    obj.location = (p_top + p_bottom) / 2
                    obj.rotation_euler = direction.to_track_quat('Z', 'Y').to_euler('XYZ')
                elif props.frustum_mode == 'POINTS_HORIZONTAL':
                    use_panel_transform = False
                    p_top, p_bottom = Vector(props.frustum_point_top), Vector(props.frustum_point_bottom)
                    direction = p_top - p_bottom
                    dz = direction.z
                    if abs(dz) < 1e-4: return None
                    height = abs(dz)
                    
                    if dz > 0:
                        rad_bottom, rad_top = radius_bottom, radius_top
                    else:
                        rad_bottom, rad_top = radius_top, radius_bottom

                    bpy.ops.mesh.primitive_cone_add(vertices=props.frustum_vertices, radius1=rad_bottom, radius2=rad_top, depth=height, end_fill_type='NGON' if cap_ends else 'NOTHING', align='WORLD', location=(0, 0, 0))
                    obj = context.active_object
                    
                    dx, dy = direction.x, direction.y
                    sign_z = 1 if dz > 0 else -1
                    bpy.ops.object.mode_set(mode='EDIT')
                    bm = bmesh.from_edit_mesh(obj.data)
                    for v in bm.verts:
                        factor = v.co.z / height
                        v.co.x += factor * dx * sign_z
                        v.co.y += factor * dy * sign_z
                    bmesh.update_edit_mesh(obj.data)
                    bpy.ops.object.mode_set(mode='OBJECT')
                    obj.location = (p_top + p_bottom) / 2
                    
                post_creation_cap_handling(context.active_object, props.frustum_cap_top, props.frustum_cap_bottom, direction)
            
            elif prim_type == 'IMAGE_CYLINDER':
                bpy.ops.mesh.primitive_cylinder_add(
                    vertices=props.image_cylinder_vertices, 
                    radius=props.image_cylinder_radius, 
                    depth=props.image_cylinder_depth, 
                    end_fill_type='NOTHING', 
                    align='WORLD', 
                    location=(0, 0, 0)
                )

            elif prim_type in ('CUBE_FRAME', 'CUBOID_FRAME'):
                if prim_type == 'CUBE_FRAME':
                    s_x = s_y = s_z = props.cube_frame_size / 2.0
                    r = props.cube_frame_radius
                    v = props.cube_frame_vertices
                else:
                    s_x = props.cuboid_frame_dimensions[0] / 2.0
                    s_y = props.cuboid_frame_dimensions[1] / 2.0
                    s_z = props.cuboid_frame_dimensions[2] / 2.0
                    r = props.cuboid_frame_radius
                    v = props.cuboid_frame_vertices
                
                pts =[
                    Vector((-s_x, -s_y, -s_z)), Vector(( s_x, -s_y, -s_z)), Vector(( s_x,  s_y, -s_z)), Vector((-s_x,  s_y, -s_z)),
                    Vector((-s_x, -s_y,  s_z)), Vector(( s_x, -s_y,  s_z)), Vector(( s_x,  s_y,  s_z)), Vector((-s_x,  s_y,  s_z))
                ]
                edges =[
                    (0,1), (1,2), (2,3), (3,0),
                    (4,5), (5,6), (6,7), (7,4),
                    (0,4), (1,5), (2,6), (3,7)
                ]
                
                mesh = bpy.data.meshes.new("TempBase")
                base_obj = bpy.data.objects.new("TempBase", mesh)
                context.collection.objects.link(base_obj)
                base_obj.location = (0, 0, 0)
                parts = [base_obj]
                
                for idx1, idx2 in edges:
                    p1, p2 = pts[idx1], pts[idx2]
                    direction = p2 - p1
                    length = direction.length
                    
                    bpy.ops.mesh.primitive_cylinder_add(vertices=v, radius=r, depth=length, end_fill_type='NGON', align='WORLD', location=(p1+p2)/2)
                    obj_part = context.active_object
                    obj_part.rotation_euler = direction.to_track_quat('Z', 'Y').to_euler('XYZ')
                    parts.append(obj_part)
                    
                if len(parts) > 1:
                    bpy.ops.object.select_all(action='DESELECT')
                    for obj_part in parts:
                        obj_part.select_set(True)
                    context.view_layer.objects.active = base_obj
                    bpy.ops.object.join()

            elif prim_type == 'TORUS':
                bpy.ops.mesh.primitive_torus_add(major_segments=props.torus_major_segments, minor_segments=props.torus_minor_segments, major_radius=props.torus_major_radius, minor_radius=props.torus_minor_radius, align='WORLD', location=(0, 0, 0))
            elif prim_type == 'MONKEY':
                bpy.ops.mesh.primitive_monkey_add(size=props.monkey_size, align='WORLD', location=(0, 0, 0))

            # --- 配列図形 ---
            elif prim_type.startswith('SPHERE_'):
                locations =[]
                spacing = props.sphere_spacing
                size = props.sphere_size
                radius = props.sphere_radius_val
                
                if prim_type == 'SPHERE_SQUARE_EDGE':
                    count = max(1, round(size / spacing))
                    actual_spacing = size / count
                    half = size / 2.0
                    for i in range(count):
                        f = i * actual_spacing
                        locations.append(Vector((-half + f, -half, 0)))
                        locations.append(Vector((half, -half + f, 0)))
                        locations.append(Vector((half - f, half, 0)))
                        locations.append(Vector((-half, half - f, 0)))
                        
                elif prim_type == 'SPHERE_SQUARE_AREA':
                    count = max(1, round(size / spacing))
                    actual_spacing = size / count
                    half = size / 2.0
                    for i in range(count + 1):
                        x = -half + i * actual_spacing
                        for j in range(count + 1):
                            y = -half + j * actual_spacing
                            locations.append(Vector((x, y, 0)))
                            
                elif prim_type == 'SPHERE_CUBE_EDGE':
                    count = max(1, round(size / spacing))
                    actual_spacing = size / count
                    half = size / 2.0
                    for i in range(count):
                        f = i * actual_spacing
                        locations.extend([
                            Vector((-half + f, -half, -half)), Vector((half, -half + f, -half)),
                            Vector((half - f, half, -half)), Vector((-half, half - f, -half)),
                            Vector((-half + f, -half, half)), Vector((half, -half + f, half)),
                            Vector((half - f, half, half)), Vector((-half, half - f, half)),
                            Vector((-half, -half, -half + f)), Vector((half, -half, -half + f)),
                            Vector((half, half, -half + f)), Vector((-half, half, -half + f))
                        ])
                        
                elif prim_type in ('SPHERE_CUBE_SURFACE', 'SPHERE_CUBE_VOLUME'):
                    count = max(1, round(size / spacing))
                    actual_spacing = size / count
                    half = size / 2.0
                    for i in range(count + 1):
                        x = -half + i * actual_spacing
                        for j in range(count + 1):
                            y = -half + j * actual_spacing
                            for k in range(count + 1):
                                z = -half + k * actual_spacing
                                if prim_type == 'SPHERE_CUBE_SURFACE':
                                    if not (i == 0 or i == count or j == 0 or j == count or k == 0 or k == count):
                                        continue
                                locations.append(Vector((x, y, z)))
                                
                elif prim_type == 'SPHERE_CIRCLE_EDGE':
                    circ = 2.0 * math.pi * radius
                    count = max(3, round(circ / spacing))
                    d_theta = 2.0 * math.pi / count
                    for i in range(count):
                        theta = i * d_theta
                        locations.append(Vector((radius * math.cos(theta), radius * math.sin(theta), 0)))
                        
                elif prim_type == 'SPHERE_CIRCLE_AREA': # グリッド状の円内
                    count = math.ceil(radius / spacing)
                    for i in range(-count, count + 1):
                        x = i * spacing
                        for j in range(-count, count + 1):
                            y = j * spacing
                            if x*x + y*y <= radius * radius + 1e-5:
                                locations.append(Vector((x, y, 0)))

                elif prim_type == 'SPHERE_CIRCLE_SPIRAL': # 螺旋渦の円内(ひまわりの種)
                    area = math.pi * radius**2
                    count = max(1, int(area / (spacing**2)))
                    phi = math.pi * (3.0 - math.sqrt(5.0)) # 黄金角
                    for i in range(count):
                        r = radius * math.sqrt((i + 0.5) / count)
                        theta = i * phi
                        x = r * math.cos(theta)
                        y = r * math.sin(theta)
                        locations.append(Vector((x, y, 0.0)))
                                
                elif prim_type == 'SPHERE_SPHERE_SURFACE': # フィボナッチ球(表面)
                    area = 4 * math.pi * radius * radius
                    N = max(4, int(round(area / (spacing * spacing))))
                    phi = math.pi * (3.0 - math.sqrt(5.0))
                    for i in range(N):
                        z = 1.0 - (i / float(N - 1)) * 2.0
                        r_xy = math.sqrt(max(0.0, 1.0 - z * z))
                        theta = phi * i
                        x = math.cos(theta) * r_xy
                        y = math.sin(theta) * r_xy
                        locations.append(Vector((x * radius, y * radius, z * radius)))
                        
                elif prim_type == 'SPHERE_SPHERE_VOLUME': # グリッド状の球内
                    count = math.ceil(radius / spacing)
                    for i in range(-count, count + 1):
                        x = i * spacing
                        for j in range(-count, count + 1):
                            y = j * spacing
                            for k in range(-count, count + 1):
                                z = k * spacing
                                if x*x + y*y + z*z <= radius * radius + 1e-5:
                                    locations.append(Vector((x, y, z)))
                                    
                elif prim_type == 'SPHERE_SPHERE_SPIRAL': # 螺旋渦の球内(タマネギ状のフィボナッチ球)
                    r_count = max(1, int(radius / spacing))
                    phi = math.pi * (3.0 - math.sqrt(5.0))
                    locations.append(Vector((0,0,0))) # 中心点
                    for r_idx in range(1, r_count + 1):
                        r_current = r_idx * spacing
                        if r_current > radius: continue
                        layer_count = max(1, int((4 * math.pi * r_current**2) / (spacing**2)))
                        for i in range(layer_count):
                            z = 1.0 - (i / float(layer_count - 1 if layer_count > 1 else 1)) * 2.0
                            r_xy = math.sqrt(max(0.0, 1.0 - z * z))
                            theta = i * phi
                            x = r_xy * math.cos(theta)
                            y = r_xy * math.sin(theta)
                            locations.append(Vector((x * r_current, y * r_current, z * r_current)))
                
                # 安全装置 (ノードなので10万個まで許容)
                MAX_SPHERES = 100000
                if len(locations) > MAX_SPHERES:
                    print(f"[{PREFIX}] Warning: Too many spheres ({len(locations)}). Limited to {MAX_SPHERES}.")
                    locations = locations[:MAX_SPHERES]

                if locations:
                    mesh = bpy.data.meshes.new("TempSphereArray")
                    mesh.from_pydata(locations, [],[])
                    mesh.update()
                    obj = bpy.data.objects.new("TempSphereArray", mesh)
                    context.collection.objects.link(obj)
                    context.view_layer.objects.active = obj
                    obj.select_set(True)
                    
                    setup_sphere_instances_node_tree(obj, props)

            elif prim_type == 'TORUS_CONCENTRIC':
                mesh = bpy.data.meshes.new("TempConcentricTorus")
                base_obj = bpy.data.objects.new("TempConcentricTorus", mesh)
                context.collection.objects.link(base_obj)
                base_obj.location = (0, 0, 0)
                parts =[base_obj]
                
                for i in range(1, props.torus_count + 1):
                    maj_r = i * props.torus_spacing
                    bpy.ops.mesh.primitive_torus_add(
                        major_segments=props.torus_major_segments, 
                        minor_segments=props.torus_minor_segments, 
                        major_radius=maj_r, 
                        minor_radius=props.torus_minor_radius_arr, 
                        align='WORLD', location=(0, 0, 0)
                    )
                    parts.append(context.active_object)

                if len(parts) > 1:
                    bpy.ops.object.select_all(action='DESELECT')
                    for p in parts:
                        p.select_set(True)
                    context.view_layer.objects.active = base_obj
                    bpy.ops.object.join()

            elif prim_type == 'CYLINDER_GRID':
                bm = bmesh.new()
                cx = props.grid_count_x
                cy = props.grid_count_y
                cz = props.grid_count_z
                sx = props.grid_spacing_x
                sy = props.grid_spacing_y
                sz = props.grid_spacing_z
                r = props.grid_radius
                v = props.grid_vertices

                wx = (cx - 1) * sx
                wy = (cy - 1) * sy
                wz = (cz - 1) * sz

                # X軸に平行な線
                if cx > 1:
                    for j in range(cy):
                        for k in range(cz):
                            y = -wy/2.0 + j * sy
                            z = -wz/2.0 + k * sz
                            mat = Matrix.Translation((0, y, z)) @ Euler((0, math.radians(90), 0), 'XYZ').to_matrix().to_4x4()
                            bmesh.ops.create_cone(bm, cap_ends=True, segments=v, radius1=r, radius2=r, depth=wx, matrix=mat)

                # Y軸に平行な線
                if cy > 1:
                    for i in range(cx):
                        for k in range(cz):
                            x = -wx/2.0 + i * sx
                            z = -wz/2.0 + k * sz
                            mat = Matrix.Translation((x, 0, z)) @ Euler((math.radians(90), 0, 0), 'XYZ').to_matrix().to_4x4()
                            bmesh.ops.create_cone(bm, cap_ends=True, segments=v, radius1=r, radius2=r, depth=wy, matrix=mat)

                # Z軸に平行な線
                if cz > 1:
                    for i in range(cx):
                        for j in range(cy):
                            x = -wx/2.0 + i * sx
                            y = -wy/2.0 + j * sy
                            mat = Matrix.Translation((x, y, 0))
                            bmesh.ops.create_cone(bm, cap_ends=True, segments=v, radius1=r, radius2=r, depth=wz, matrix=mat)

                mesh = bpy.data.meshes.new("TempGrid")
                bm.to_mesh(mesh)
                bm.free()
                obj = bpy.data.objects.new("TempGrid", mesh)
                context.collection.objects.link(obj)
                context.view_layer.objects.active = obj
                obj.select_set(True)

    except Exception as e:
        print(f"Error creating primitive: {e}")
        return None

    new_objs = set(bpy.data.objects) - old_objs
    if not new_objs: return None
    obj = list(new_objs)[0]
    
    obj.name = name

    if use_panel_transform:
        if props.scale_uniform: 
            scale_vec = Vector((props.scale_factor, props.scale_factor, props.scale_factor))
        else: 
            scale_vec = Vector(props.scale_vector)
        
        obj.scale.x *= scale_vec.x
        obj.scale.y *= scale_vec.y
        obj.scale.z *= scale_vec.z

        obj.location = props.location
        obj.rotation_euler.rotate(Euler(map(math.radians, (props.additional_rotation_x, props.additional_rotation_y, props.additional_rotation_z)), 'XYZ'))

    if props.use_solidify and props.primitive_type != 'IMAGE_CYLINDER':
        mod = obj.modifiers.new(name="Solidify", type='SOLIDIFY')
        mod.thickness = props.solidify_thickness

    return obj

def apply_primitive_material(obj, props, is_preview=False):
    if not obj.data: return
    timestamp = datetime.datetime.now().strftime('%M%S%f')[:5]
    mat_prefix = "Mat_Prev" if is_preview else "Mat_Entity"
    
    mat = bpy.data.materials.new(name=f"{mat_prefix}_{obj.name}_{timestamp}")
    mat.use_nodes = True
    obj.data.materials.append(mat)
    
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    if not bsdf: return

    if props.primitive_type == 'IMAGE_CYLINDER':
        if hasattr(mat, "blend_method"): mat.blend_method = 'BLEND'
        if hasattr(mat, "shadow_method"): mat.shadow_method = 'NONE'
        
        tex_node = mat.node_tree.nodes.new('ShaderNodeTexImage')
        tex_node.location = (-400, 200)
        
        tex_coord = mat.node_tree.nodes.new('ShaderNodeTexCoord')
        tex_coord.location = (-800, 200)
        mapping = mat.node_tree.nodes.new('ShaderNodeMapping')
        mapping.location = (-600, 200)
        
        mapping.inputs['Location'].default_value[0] = props.image_cylinder_uv_offset[0]
        mapping.inputs['Location'].default_value[1] = props.image_cylinder_uv_offset[1]
        
        mapping.inputs['Scale'].default_value[0] = -1.0 * props.image_cylinder_uv_scale[0]
        mapping.inputs['Scale'].default_value[1] = props.image_cylinder_uv_scale[1]
        
        mat.node_tree.links.new(tex_coord.outputs['UV'], mapping.inputs['Vector'])
        mat.node_tree.links.new(mapping.outputs['Vector'], tex_node.inputs['Vector'])
        
        if props.image_cylinder_image:
            tex_node.image = props.image_cylinder_image
            mat.node_tree.links.new(tex_node.outputs['Color'], bsdf.inputs['Base Color'])
            if 'Emission Color' in bsdf.inputs: mat.node_tree.links.new(tex_node.outputs['Color'], bsdf.inputs['Emission Color'])
            elif 'Emission' in bsdf.inputs: mat.node_tree.links.new(tex_node.outputs['Color'], bsdf.inputs['Emission'])
            if 'Emission Strength' in bsdf.inputs: bsdf.inputs['Emission Strength'].default_value = 1.0
        else:
            bsdf.inputs['Base Color'].default_value = (0.2, 0.2, 0.2, 1.0)
            
        geo_node = mat.node_tree.nodes.new('ShaderNodeNewGeometry')
        geo_node.location = (-400, -100)
        
        map_node = mat.node_tree.nodes.new('ShaderNodeMapRange')
        map_node.location = (-200, -100)
        map_node.inputs['To Min'].default_value = props.image_cylinder_alpha_outer
        map_node.inputs['To Max'].default_value = props.image_cylinder_alpha_inner
        
        mat.node_tree.links.new(geo_node.outputs['Backfacing'], map_node.inputs['Value'])
        mat.node_tree.links.new(map_node.outputs['Result'], bsdf.inputs['Alpha'])
        
    else:
        color_to_set = (0.8, 0.8, 0.8, 1.0)
        if props.color_mode == 'RANDOM_ALL':
            color_to_set = (random.random(), random.random(), random.random(), 1.0)
        elif props.color_mode == 'PICKER':
            color_to_set = props.custom_color
        elif props.color_mode == 'PRESET':
            preset_key = f"preset_color_{props.preset_set.lower()}_{props.preset_color}" if props.preset_set != 'A' else f"preset_color_{props.preset_color}"
            color_to_set = getattr(props, preset_key)
        
        bsdf.inputs['Base Color'].default_value = color_to_set
        if 'Alpha' in bsdf.inputs:
            bsdf.inputs['Alpha'].default_value = props.face_alpha
            
        if hasattr(mat, "blend_method"):
            mat.blend_method = 'BLEND' if props.face_alpha < 1.0 else 'OPAQUE'
            
    # GeoNodes経由でマテリアルを確実に反映
    if props.primitive_type.startswith('SPHERE_'):
        mod = obj.modifiers.get("Array_GeoNodes")
        if mod and mod.node_group:
            for node in mod.node_group.nodes:
                if node.type == 'SET_MATERIAL':
                    node.inputs['Material'].default_value = mat

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)
    
    for o in list(col.objects):
        if o.get(PREVIEW_TAG):
            bpy.data.objects.remove(o, do_unlink=True)
            
    for m in bpy.data.meshes:
        if m.users == 0: bpy.data.meshes.remove(m)
    for mat in bpy.data.materials:
        if mat.users == 0 and mat.name.startswith("Mat_Prev"): bpy.data.materials.remove(mat)
        
    # 配列図形用ノードグループのお掃除
    for ng in bpy.data.node_groups:
        if ng.users == 0 and ng.name.startswith("SphereArray_Nodes"): 
            bpy.data.node_groups.remove(ng)

    if not props.show_preview: return

    name = f"[Preview] {props.primitive_type}"
    obj = create_primitive_object(context, props, name)
    if not obj: return

    for c in obj.users_collection: c.objects.unlink(obj)
    col.objects.link(obj)

    obj[PREVIEW_TAG] = True
    apply_primitive_material(obj, props, is_preview=True)
    obj.select_set(False)

def place_object_in_hierarchical_collection(obj, collection_path_names):
    if not obj or not collection_path_names: return
    target_parent_collection = bpy.context.scene.collection
    
    for name in collection_path_names:
        found_child = target_parent_collection.children.get(name)
        if not found_child:
            found_child = bpy.data.collections.new(name)
            target_parent_collection.children.link(found_child)
        target_parent_collection = found_child
        
    for coll in obj.users_collection:
        coll.objects.unlink(obj)
    try:
        if obj.name not in target_parent_collection.objects:
            target_parent_collection.objects.link(obj)
    except Exception as e:
        print(f"Error linking object {obj.name}: {e}")

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

def make_color_update(set_val, idx):
    def update_cb(self, context):
        if self.color_mode == 'PRESET':
            self.preset_set = set_val
            self.preset_color = str(idx)
        on_update(self, context)
    return update_cb

class PG_B200Props(PropertyGroup):
    # --- View Control Props ---
    slider_limit: FloatProperty(name="Range Limit", default=CURRENT_DEFAULTS.get('slider_limit', 300.0), min=10.0, max=10000.0)
    view_pos: FloatVectorProperty(name="View Position", size=3, soft_min=-10000.0, soft_max=10000.0, 
                                  default=CURRENT_DEFAULTS.get('view_pos', VIEW_POS_INIT), 
                                  update=update_view_position)

    # --- Zukkei & Array Props ---
    show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
    
    collection_name: StringProperty(name="Collection", default=CURRENT_DEFAULTS['collection_name'])
    sub_collection_name: StringProperty(name="Sub-Collection", default=CURRENT_DEFAULTS['sub_collection_name'])
    object_name_prefix: StringProperty(name="Object Name", default=CURRENT_DEFAULTS['object_name_prefix'])

    primitive_type: EnumProperty(
        name="Primitive Type",
        items=[
            ('CUBE', "Cube", ""), ('CUBOID', "Cuboid", ""), ('PLANE', "Plane", ""),
            ('CIRCLE', "Circle", ""), ('ELLIPSE', "Ellipse", ""), ('UV_SPHERE', "UV Sphere", ""),
            ('ICO_SPHERE', "Ico Sphere", ""), ('CYLINDER', "Cylinder", ""), 
            ('FRUSTUM', "Frustum (Cone)", ""), ('IMAGE_CYLINDER', "Image Cylinder (パノラマ筒)", ""),
            ('CUBE_FRAME', "Cube Frame (辺を円柱で)", ""),
            ('CUBOID_FRAME', "Cuboid Frame (辺を円柱で)", ""),
            ('TORUS', "Torus", ""), ('MONKEY', "Monkey", ""),
            # 配列図形の追加分
            ('SPHERE_SQUARE_EDGE', "Sphere: Square Perimeter (正方形の辺)", ""),
            ('SPHERE_SQUARE_AREA', "Sphere: Square Area (正方形の面)", ""),
            ('SPHERE_CUBE_EDGE', "Sphere: Cube Edges (立方体の辺)", ""),
            ('SPHERE_CUBE_SURFACE', "Sphere: Cube Surface (立方体の表面)", ""),
            ('SPHERE_CUBE_VOLUME', "Sphere: Cube Volume (立方体の中身)", ""),
            ('SPHERE_CIRCLE_EDGE', "Sphere: Circle Perimeter (円の円周)", ""),
            ('SPHERE_CIRCLE_AREA', "Sphere: Circle Area (円の面内グリッド)", ""),
            ('SPHERE_CIRCLE_SPIRAL', "Sphere: Circle Spiral (円内を螺旋渦で)", ""),
            ('SPHERE_SPHERE_SURFACE', "Sphere: Sphere Surface (球の表面)", ""),
            ('SPHERE_SPHERE_VOLUME', "Sphere: Sphere Volume (球体内グリッド)", ""),
            ('SPHERE_SPHERE_SPIRAL', "Sphere: Sphere Spiral (球内を螺旋渦で)", ""),
            ('TORUS_CONCENTRIC', "Torus: Concentric (同心円トーラス)", ""),
            ('CYLINDER_GRID', "Cylinder: Grid 3D (円柱の3D格子)", "")
        ],
        default=CURRENT_DEFAULTS['primitive_type'], update=on_update
    )

    use_solidify: BoolProperty(name="面に厚みを加える", default=CURRENT_DEFAULTS['use_solidify'], update=on_update)
    solidify_thickness: FloatProperty(name="厚み", default=CURRENT_DEFAULTS['solidify_thickness'], update=on_update)

    cube_size: FloatProperty(name="Size", default=CURRENT_DEFAULTS['cube_size'], min=0.001, update=on_update)
    cuboid_dimensions: FloatVectorProperty(name="Dimensions", default=CURRENT_DEFAULTS['cuboid_dimensions'], min=0.001, subtype='XYZ', size=3, update=on_update)
    plane_size_x: FloatProperty(name="Size X", default=CURRENT_DEFAULTS['plane_size_x'], min=0.001, update=on_update)
    plane_size_y: FloatProperty(name="Size Y", default=CURRENT_DEFAULTS['plane_size_y'], min=0.001, update=on_update)
    circle_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['circle_radius'], min=0.001, update=on_update)
    circle_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['circle_vertices'], min=3, update=on_update)
    ellipse_radius_x: FloatProperty(name="Radius X", default=CURRENT_DEFAULTS['ellipse_radius_x'], min=0.001, update=on_update)
    ellipse_radius_y: FloatProperty(name="Radius Y", default=CURRENT_DEFAULTS['ellipse_radius_y'], min=0.001, update=on_update)
    ellipse_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['ellipse_vertices'], min=3, update=on_update)
    uv_sphere_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['uv_sphere_radius'], min=0.001, update=on_update)
    uv_sphere_segments: IntProperty(name="Segments", default=CURRENT_DEFAULTS['uv_sphere_segments'], min=3, update=on_update)
    uv_sphere_rings: IntProperty(name="Rings", default=CURRENT_DEFAULTS['uv_sphere_rings'], min=2, update=on_update)
    ico_sphere_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['ico_sphere_radius'], min=0.001, update=on_update)
    ico_sphere_subdivisions: IntProperty(name="Subdivisions", default=CURRENT_DEFAULTS['ico_sphere_subdivisions'], min=1, max=10, update=on_update)
    
    CYL_FRUSTUM_MODES =[
        ('CENTER', "Center, Height", "中心座標と高さを指定して作成"),
        ('POINTS_NORMAL', "Points (Axis Normal)", "両端指定: 上下面が中心軸に対して直角"),
        ('POINTS_HORIZONTAL', "Points (Horizontal Caps)", "両端指定: 上下面がXY平面に平行(シアー変形)")
    ]
    
    cylinder_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['cylinder_radius'], min=0.001, update=on_update)
    cylinder_mode: EnumProperty(name="Creation Mode", items=CYL_FRUSTUM_MODES, default=CURRENT_DEFAULTS['cylinder_mode'], update=on_update)
    cylinder_depth: FloatProperty(name="Depth", default=CURRENT_DEFAULTS['cylinder_depth'], min=0.001, update=on_update)
    cylinder_point_top: FloatVectorProperty(name="Top Point", subtype='TRANSLATION', default=CURRENT_DEFAULTS['cylinder_point_top'], size=3, update=on_update)
    cylinder_point_bottom: FloatVectorProperty(name="Bottom Point", subtype='TRANSLATION', default=CURRENT_DEFAULTS['cylinder_point_bottom'], size=3, update=on_update)
    cylinder_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['cylinder_vertices'], min=3, update=on_update)
    cylinder_cap_top: BoolProperty(name="Fill Top Cap", default=CURRENT_DEFAULTS['cylinder_cap_top'], update=on_update)
    cylinder_cap_bottom: BoolProperty(name="Fill Bottom Cap", default=CURRENT_DEFAULTS['cylinder_cap_bottom'], update=on_update)
    
    frustum_mode: EnumProperty(name="Creation Mode", items=CYL_FRUSTUM_MODES, default=CURRENT_DEFAULTS['frustum_mode'], update=on_update)
    frustum_radius_top: FloatProperty(name="Top Radius (+Z)", default=CURRENT_DEFAULTS['frustum_radius_top'], min=0.0, update=on_update)
    frustum_radius_bottom: FloatProperty(name="Bottom Radius (-Z)", default=CURRENT_DEFAULTS['frustum_radius_bottom'], min=0.001, update=on_update)
    frustum_height: FloatProperty(name="Height", default=CURRENT_DEFAULTS['frustum_height'], min=0.001, update=on_update)
    frustum_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['frustum_vertices'], min=3, update=on_update)
    frustum_cap_top: BoolProperty(name="Fill Top Cap", default=CURRENT_DEFAULTS['frustum_cap_top'], update=on_update)
    frustum_cap_bottom: BoolProperty(name="Fill Bottom Cap", default=CURRENT_DEFAULTS['frustum_cap_bottom'], update=on_update)
    frustum_point_top: FloatVectorProperty(name="Top Point", subtype='TRANSLATION', default=CURRENT_DEFAULTS['frustum_point_top'], size=3, update=on_update)
    frustum_point_bottom: FloatVectorProperty(name="Bottom Point", subtype='TRANSLATION', default=CURRENT_DEFAULTS['frustum_point_bottom'], size=3, update=on_update)
    
    image_cylinder_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['image_cylinder_radius'], min=0.001, update=on_update)
    image_cylinder_depth: FloatProperty(name="Height", default=CURRENT_DEFAULTS['image_cylinder_depth'], min=0.001, update=on_update)
    image_cylinder_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['image_cylinder_vertices'], min=3, update=on_update)
    image_cylinder_image: PointerProperty(type=bpy.types.Image, name="Image", update=on_update)
    image_cylinder_uv_offset: FloatVectorProperty(name="UV Offset", size=2, default=CURRENT_DEFAULTS['image_cylinder_uv_offset'], update=on_update)
    image_cylinder_uv_scale: FloatVectorProperty(name="UV Scale", size=2, default=CURRENT_DEFAULTS['image_cylinder_uv_scale'], update=on_update)
    image_cylinder_alpha_outer: FloatProperty(name="Outer Alpha (外側の透明度)", default=CURRENT_DEFAULTS['image_cylinder_alpha_outer'], min=0.0, max=1.0, update=on_update)
    image_cylinder_alpha_inner: FloatProperty(name="Inner Alpha (内側の透明度)", default=CURRENT_DEFAULTS['image_cylinder_alpha_inner'], min=0.0, max=1.0, update=on_update)

    cube_frame_size: FloatProperty(name="Size", default=CURRENT_DEFAULTS['cube_frame_size'], min=0.001, update=on_update)
    cube_frame_radius: FloatProperty(name="Radius (Thickness)", default=CURRENT_DEFAULTS['cube_frame_radius'], min=0.001, update=on_update)
    cube_frame_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['cube_frame_vertices'], min=3, update=on_update)

    cuboid_frame_dimensions: FloatVectorProperty(name="Dimensions", default=CURRENT_DEFAULTS['cuboid_frame_dimensions'], min=0.001, subtype='XYZ', size=3, update=on_update)
    cuboid_frame_radius: FloatProperty(name="Radius (Thickness)", default=CURRENT_DEFAULTS['cuboid_frame_radius'], min=0.001, update=on_update)
    cuboid_frame_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['cuboid_frame_vertices'], min=3, update=on_update)

    torus_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['torus_major_radius'], min=0.001, update=on_update)
    torus_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['torus_minor_radius'], min=0.001, 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)
    monkey_size: FloatProperty(name="Size", default=CURRENT_DEFAULTS['monkey_size'], min=0.001, update=on_update)

    # 配列図形用プロパティ
    sphere_spacing: FloatProperty(name="間隔", default=CURRENT_DEFAULTS.get('sphere_spacing', 0.5), min=0.01, update=on_update)
    sphere_elem_radius: FloatProperty(name="球の半径", default=CURRENT_DEFAULTS.get('sphere_elem_radius', 0.1), min=0.001, update=on_update)
    sphere_size: FloatProperty(name="全体サイズ", default=CURRENT_DEFAULTS.get('sphere_size', 4.0), min=0.001, update=on_update)
    sphere_radius_val: FloatProperty(name="全体半径", default=CURRENT_DEFAULTS.get('sphere_radius_val', 2.0), min=0.001, update=on_update)
    sphere_segments: IntProperty(name="Segments", default=CURRENT_DEFAULTS.get('sphere_segments', 12), min=3, update=on_update)
    sphere_rings: IntProperty(name="Rings", default=CURRENT_DEFAULTS.get('sphere_rings', 6), min=2, update=on_update)

    torus_count: IntProperty(name="同心円の数", default=CURRENT_DEFAULTS.get('torus_count', 5), min=1, update=on_update)
    torus_spacing: FloatProperty(name="間隔", default=CURRENT_DEFAULTS.get('torus_spacing', 1.0), min=0.01, update=on_update)
    torus_minor_radius_arr: FloatProperty(name="トーラスの太さ", default=CURRENT_DEFAULTS.get('torus_minor_radius_arr', 0.1), min=0.001, update=on_update)

    grid_count_x: IntProperty(name="Count X", default=CURRENT_DEFAULTS.get('grid_count_x', 5), min=1, update=on_update)
    grid_count_y: IntProperty(name="Count Y", default=CURRENT_DEFAULTS.get('grid_count_y', 5), min=1, update=on_update)
    grid_count_z: IntProperty(name="Count Z", default=CURRENT_DEFAULTS.get('grid_count_z', 5), min=1, update=on_update)
    
    grid_spacing_x: FloatProperty(name="Spacing X", default=CURRENT_DEFAULTS.get('grid_spacing_x', 1.0), min=0.01, update=on_update)
    grid_spacing_y: FloatProperty(name="Spacing Y", default=CURRENT_DEFAULTS.get('grid_spacing_y', 1.0), min=0.01, update=on_update)
    grid_spacing_z: FloatProperty(name="Spacing Z", default=CURRENT_DEFAULTS.get('grid_spacing_z', 1.0), min=0.01, update=on_update)
    
    grid_radius: FloatProperty(name="円柱の太さ", default=CURRENT_DEFAULTS.get('grid_radius', 0.05), min=0.001, update=on_update)
    grid_vertices: IntProperty(name="円柱の頂点数", default=CURRENT_DEFAULTS.get('grid_vertices', 16), min=3, update=on_update)

    location: FloatVectorProperty(name="Location", subtype='TRANSLATION', default=CURRENT_DEFAULTS['location'], size=3, update=on_update)
    scale_uniform: BoolProperty(name="Uniform Scale", default=CURRENT_DEFAULTS['scale_uniform'], update=on_update)
    scale_factor: FloatProperty(name="Scale", default=CURRENT_DEFAULTS['scale_factor'], min=0.001, update=on_update)
    scale_vector: FloatVectorProperty(name="Scale", default=CURRENT_DEFAULTS['scale_vector'], min=0.001, size=3, update=on_update)
    additional_rotation_x: FloatProperty(name="Rotation X", default=CURRENT_DEFAULTS['additional_rotation_x'], update=on_update)
    additional_rotation_y: FloatProperty(name="Rotation Y", default=CURRENT_DEFAULTS['additional_rotation_y'], update=on_update)
    additional_rotation_z: FloatProperty(name="Rotation Z", default=CURRENT_DEFAULTS['additional_rotation_z'], update=on_update)

    color_mode: EnumProperty(name="Color Mode", items=[('RANDOM_ALL', "Random All", ""), ('PICKER', "Color Picker", ""), ('PRESET', "Preset", "")], default=CURRENT_DEFAULTS['color_mode'], update=on_update)
    preset_set: EnumProperty(name="Preset Set", items=[('A', "Preset A", ""), ('B', "Preset B", ""), ('C', "Preset C (Green)", "")], default=CURRENT_DEFAULTS['preset_set'], update=on_update)
    preset_color: EnumProperty(name="Preset Color", items=[(str(i), str(i), "") for i in range(1, 11)], default=CURRENT_DEFAULTS['preset_color'], update=on_update)
    custom_color: FloatVectorProperty(name="Custom Color", subtype='COLOR', default=CURRENT_DEFAULTS['custom_color'], min=0.0, max=1.0, size=4, update=on_update)
    face_alpha: FloatProperty(name="Alpha", default=CURRENT_DEFAULTS['face_alpha'], min=0.0, max=1.0, update=on_update)

    show_main_docs: BoolProperty(default=CURRENT_DEFAULTS['show_main_docs'])
    show_new_docs: BoolProperty(default=CURRENT_DEFAULTS['show_new_docs'])
    show_old_docs: BoolProperty(default=CURRENT_DEFAULTS['show_old_docs'])
    show_social: BoolProperty(default=CURRENT_DEFAULTS['show_social'])

for s_key, set_val in[('preset_color', 'A'), ('preset_color_b', 'B'), ('preset_color_c', 'C')]:
    for i in range(1, 11):
        key = f"{s_key}_{i}"
        PG_B200Props.__annotations__[key] = FloatVectorProperty(subtype='COLOR', default=CURRENT_DEFAULTS[key], min=0.0, max=1.0, size=4, update=make_color_update(set_val, i))

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

class OT_ViewCenterFront(Operator):
    bl_idname = f"{OP_PREFIX}.view_center_front"
    bl_label = "Center 0,0,0 (Front View)"
    bl_description = "原点(0,0,0)を画面中央に配置し、Yマイナス方向からの視点(正面)にします"
    
    def execute(self, context):
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                rv3d = area.spaces.active.region_3d
                if rv3d:
                    rv3d.view_location = (0.0, 0.0, 0.0)
                    rv3d.view_rotation = Euler((math.radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
                    if rv3d.view_distance < 10.0:
                        rv3d.view_distance = 60.0
        return {'FINISHED'}

class OT_CreatePrimitive(Operator):
    bl_idname = f"{OP_PREFIX}.create_primitive"
    bl_label = "実体メッシュを生成 (切り離し)"
    bl_description = "現在のパラメータで実体を生成し、アドオン管理から切り離して指定コレクションに配置します"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME)
        scene = context.scene
        
        prefix = props.object_name_prefix.strip() or "Object"
        final_name = f"{prefix}{scene.b200_object_counter:03d}"
        
        obj = create_primitive_object(context, props, final_name)
        if not obj:
            self.report({'ERROR'}, "形状の生成に失敗しました。")
            return {'CANCELLED'}
        
        scene.b200_object_counter += 1
        
        for c in obj.users_collection:
            c.objects.unlink(obj)
            
        collection_path =[n for n in[props.collection_name.strip(), props.sub_collection_name.strip()] if n]
        if collection_path: 
            place_object_in_hierarchical_collection(obj, collection_path)
        else:
            context.scene.collection.objects.link(obj)
            
        apply_primitive_material(obj, props, is_preview=False)
        
        bpy.ops.object.select_all(action='DESELECT')
        obj.select_set(True)
        context.view_layer.objects.active = obj
        
        self.report({'INFO'}, f"{props.primitive_type} を生成し、アドオンから切り離しました: {obj.name}")
        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 = next((t for t in bpy.data.texts if SOURCE_ID_TAG in t.as_string()), None)
        
        if not target_text:
            self.report({'ERROR'}, "スクリプトのソースが見つかりません。")
            return {'CANCELLED'}

        def format_val(v):
            if isinstance(v, str): return repr(v)
            if isinstance(v, bool): return str(v)
            if isinstance(v, (int, float)): return f"{v:.4f}" if isinstance(v, float) else str(v)
            try:
                return "(" + ", ".join(f"{float(x):.4f}" for x in v) + ")"
            except: pass
            return str(v)

        new_dict = "CURRENT_DEFAULTS = {\n"
        for k in CURRENT_DEFAULTS.keys():
            val = getattr(props, k)
            new_dict += f'    "{k}": {format_val(val)},\n'
        new_dict += "}\n"

        code = target_text.as_string()
        try:
            start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
            pre, post = code.split(start)[0], code.split(end)[1]
            final = f"# Copied: {datetime.datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
            context.window_manager.clipboard = final
            self.report({'INFO'}, "現在のパラメータでコードをコピーしました!")
        except Exception as e:
            self.report({'ERROR'}, f"コピーに失敗: {e}")
            return {'CANCELLED'}
        return {'FINISHED'}

class OT_ResetProperty(Operator):
    bl_idname = f"{OP_PREFIX}.reset_property"
    bl_label = "Reset Property"
    bl_options = {'REGISTER', 'UNDO'}
    prop_name: StringProperty()
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME)
        if self.prop_name in CURRENT_DEFAULTS:
            setattr(props, self.prop_name, CURRENT_DEFAULTS[self.prop_name])
        return {'FINISHED'}

class OT_ResetAllSettings(Operator):
    bl_idname = f"{OP_PREFIX}.reset_all_settings"
    bl_label = "Reset All Settings"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME)
        for k, v in CURRENT_DEFAULTS.items():
            setattr(props, k, v)
        context.scene.b200_object_counter = 1
        self.report({'INFO'}, "All settings and counter have been reset.")
        return {'FINISHED'}

class OT_ResetCounter(Operator):
    bl_idname = f"{OP_PREFIX}.reset_counter"
    bl_label = "Reset Counter"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        context.scene.b200_object_counter = 1
        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):
        def delayed_unregister(): unregister(); return None
        bpy.app.timers.register(delayed_unregister, first_interval=0.1)
        return {'FINISHED'}

# --- View Control Operators ---

class OT_View_GetCurrent(Operator):
    bl_idname = f"{OP_PREFIX}.view_get_current"
    bl_label = "Get Current View & Update"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return {'CANCELLED'}
        r3d = context.space_data.region_3d if context.space_data else None
        if not r3d: return {'CANCELLED'}
            
        target_pos = Vector(r3d.view_location)
        actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
        max_val = max(abs(actual_cam_pos.x), abs(actual_cam_pos.y), abs(actual_cam_pos.z))
        if max_val > props.slider_limit: props.slider_limit = max_val + 50.0 
        props.view_pos = actual_cam_pos
        return {'FINISHED'}

class OT_View_Reset(Operator):
    bl_idname = f"{OP_PREFIX}.view_reset"
    bl_label = VIEW_RESET_BTN_TEXT
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        if props:
            for window in 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':
                                space.region_3d.view_location = (0.0, 0.0, 0.0)
            props.view_pos = VIEW_POS_INIT 
        return {'FINISHED'}

class OT_View_CenterSelected(Operator):
    bl_idname = f"{OP_PREFIX}.view_center_selected"
    bl_label = "Center Selected Object"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        bpy.ops.view3d.view_selected()
        r3d = context.space_data.region_3d if context.space_data else None
        if r3d:
            props = getattr(context.scene, PROPS_NAME, None)
            if props:
                target_pos = Vector(r3d.view_location)
                props.view_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
        return {'FINISHED'}

class OT_View_CopyActualPos(Operator):
    bl_idname = f"{OP_PREFIX}.view_copy_actual_pos"
    bl_label = "Copy Position Only"
    def execute(self, context):
        r3d = context.space_data.region_3d if context.space_data else None
        if not r3d: return {'CANCELLED'}
        target_pos = Vector(r3d.view_location)
        p = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
        context.window_manager.clipboard = f"Actual View Pos: ({p.x:.4f}, {p.y:.4f}, {p.z:.4f})"
        self.report({'INFO'}, "視座位置をコピーしました")
        return {'FINISHED'}

class OT_View_CopyAngles(Operator):
    bl_idname = f"{OP_PREFIX}.view_copy_angles"
    bl_label = "Copy Full Info (Pos & Angles)"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        r3d = context.space_data.region_3d if context.space_data else None
        if not r3d or not props: return {'CANCELLED'}
        target_pos = Vector(r3d.view_location)
        actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
        vec = target_pos - actual_cam_pos
        length = vec.length
        if length < 0.0001: return {'CANCELLED'}
            
        ang_x = math.degrees(math.acos(vec.x / length))
        ang_y = math.degrees(math.acos(vec.y / length))
        ang_z = math.degrees(math.acos(vec.z / length))

        pl_x = math.degrees(math.asin(vec.x / length))
        pl_y = math.degrees(math.asin(vec.y / length))
        pl_z = math.degrees(math.asin(vec.z / length))
        
        info_text = (
            f"--- View Direction Info ---\n"
            f"[ Actual 3D View Status ]\n"
            f"Actual View Pos : ({actual_cam_pos.x:.4f}, {actual_cam_pos.y:.4f}, {actual_cam_pos.z:.4f})\n"
            f"Target Pos      : ({target_pos.x:.4f}, {target_pos.y:.4f}, {target_pos.z:.4f})\n"
            f"Distance        : {length:.4f}\n\n"
            f"[ Direction Angles (軸そのものとの角度 0〜180°) ]\n"
            f"Angle from X Axis : {ang_x:.2f} deg\n"
            f"Angle from Y Axis : {ang_y:.2f} deg\n"
            f"Angle from Z Axis : {ang_z:.2f} deg\n\n"
            f"[ Planar Angles (直感的な傾き・ズレ角 -90〜90°) ]\n"
            f"X (横のズレ角)    : {pl_x:.2f} deg\n"
            f"Y (前後の傾き)    : {pl_y:.2f} deg\n"
            f"Z (仰角・俯角)    : {pl_z:.2f} deg\n"
        )
        context.window_manager.clipboard = info_text
        self.report({'INFO'}, "情報全体をクリップボードにコピーしました")
        return {'FINISHED'}

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

class PG_BasePanel:
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME

    def _draw_prop_with_reset(self, layout, obj, prop_name, text=None):
        row = layout.row(align=True)
        split = row.split(factor=0.85, align=True)
        split.prop(obj, prop_name, text=text if text else prop_name.replace("_", " ").title())
        op = split.operator(OT_ResetProperty.bl_idname, text="", icon='FILE_REFRESH')
        op.prop_name = prop_name

    def _draw_vector_xyz(self, layout, obj, prop_name, label):
        col = layout.column(align=True)
        col.label(text=label)
        for i, axis in enumerate(['X', 'Y', 'Z']):
            row = col.row(align=True)
            split = row.split(factor=0.85, align=True)
            split.prop(obj, prop_name, index=i, text=axis)
            op = split.operator(OT_ResetProperty.bl_idname, text="", icon='FILE_REFRESH')
            op.prop_name = prop_name
            
    def _draw_vector_2d(self, layout, obj, prop_name, label, labels=['U', 'V']):
        col = layout.column(align=True)
        col.label(text=label)
        for i, axis in enumerate(labels):
            row = col.row(align=True)
            split = row.split(factor=0.85, align=True)
            split.prop(obj, prop_name, index=i, text=axis)
            op = split.operator(OT_ResetProperty.bl_idname, text="", icon='FILE_REFRESH')
            op.prop_name = prop_name

class PT_ViewControlPanel(PG_BasePanel, Panel):
    bl_label = "View Control (視座位置)"
    bl_idname = f"{OP_PREFIX}_PT_view_control"
    
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: return

        row = layout.row()
        row.scale_y = 1.2
        row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="状態を保持してコードコピー")
        layout.separator()

        box = layout.box()
        box.label(text="Perspective Viewpoint", icon='VIEW_CAMERA')
        box.prop(props, "slider_limit", text="Range Limit (+/-)")
        
        col = box.column(align=True)
        col.prop(props, "view_pos", text="X", index=0)
        col.prop(props, "view_pos", text="Y", index=1)
        col.prop(props, "view_pos", text="Z", index=2)
        
        box.separator()
        box.operator(OT_View_GetCurrent.bl_idname, icon='RESTRICT_VIEW_OFF')
        box.operator(OT_View_Reset.bl_idname, icon='LOOP_BACK')
        layout.operator(OT_View_CenterSelected.bl_idname, icon='VIEWZOOM')
        
        layout.separator()
        box_info = layout.box()
        box_info.label(text="Actual View Status", icon='INFO')
        
        r3d = context.space_data.region_3d if context.space_data else None
        if r3d:
            target_pos = Vector(r3d.view_location)
            
            actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
            vec = target_pos - actual_cam_pos
            length = vec.length
            
            col_pos = box_info.column(align=True)
            col_pos.label(text="[ Actual Position ]", icon='VIEW_CAMERA')
            col_pos.label(text=f"  X: {actual_cam_pos.x:.4f}")
            col_pos.label(text=f"  Y: {actual_cam_pos.y:.4f}")
            col_pos.label(text=f"  Z: {actual_cam_pos.z:.4f}")
            col_pos.label(text=f"  Distance: {length:.4f}") 
            box_info.operator(OT_View_CopyActualPos.bl_idname, icon='COPYDOWN')
            box_info.separator()
            
            col_ang = box_info.column(align=True)
            if length > 0.0001:
                a_x = math.degrees(math.acos(vec.x / length))
                a_y = math.degrees(math.acos(vec.y / length))
                a_z = math.degrees(math.acos(vec.z / length))
                p_x = math.degrees(math.asin(vec.x / length))
                p_y = math.degrees(math.asin(vec.y / length))
                p_z = math.degrees(math.asin(vec.z / length))
                
                col_ang.label(text="[ Direction Angles (軸との角度) ]", icon='ORIENTATION_GLOBAL')
                col_ang.label(text=f"  X: {a_x:.2f}°")
                col_ang.label(text=f"  Y: {a_y:.2f}°")
                col_ang.label(text=f"  Z: {a_z:.2f}°")
                
                col_ang.separator()
                col_ang.label(text="[ Planar Angles (直感的な傾き) ]", icon='DRIVER_ROTATIONAL_DIFFERENCE')
                col_ang.label(text=f"  X (ズレ角): {p_x:.2f}°")
                col_ang.label(text=f"  Y (ズレ角): {p_y:.2f}°")
                col_ang.label(text=f"  Z (仰俯角): {p_z:.2f}°")
            else:
                col_ang.label(text="  Target is too close")

            box_info.separator()
            box_info.operator(OT_View_CopyAngles.bl_idname, icon='COPYDOWN')
        else:
            box_info.label(text="Please use in 3D View")

class PT_MainPanel(PG_BasePanel, Panel):
    bl_label = "Primitive Generator [Preview]"
    bl_idname = f"{OP_PREFIX}_PT_main"
    bl_options = {'DEFAULT_CLOSED'}
    
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME)
        scene = context.scene

        row = layout.row()
        row.scale_y = 1.2
        row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="状態を保持してコードコピー")
        
        row_view = layout.row()
        row_view.operator(OT_ViewCenterFront.bl_idname, icon='VIEWZOOM', text="0,0,0 を正面(Y-)から見る")
        
        layout.separator()

        layout.prop(props, "primitive_type", text="図形")
        
        row_sol = layout.row()
        row_sol.enabled = (props.primitive_type != 'IMAGE_CYLINDER')
        row_sol.prop(props, "use_solidify", text="面に厚みを加える")
        if props.use_solidify:
            self._draw_prop_with_reset(layout, props, "solidify_thickness", text="厚み")

        layout.separator()

        layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')

        row = layout.row()
        row.scale_y = 1.5
        row.operator(OT_CreatePrimitive.bl_idname, icon='PLUS')

        box_naming = layout.box()
        box_naming.label(text="Naming & Collection")
        self._draw_prop_with_reset(box_naming, props, "collection_name")
        self._draw_prop_with_reset(box_naming, props, "sub_collection_name")
        self._draw_prop_with_reset(box_naming, props, "object_name_prefix")
        
        row_counter = box_naming.row(align=True)
        row_counter.label(text=f"Next Index: {scene.b200_object_counter:03d}")
        row_counter.operator(OT_ResetCounter.bl_idname, text="", icon='FILE_REFRESH')

        layout.separator()

        box_prim = layout.box()
        prim_type = props.primitive_type
        col = box_prim.column(align=True)
        
        if prim_type == 'CUBE': self._draw_prop_with_reset(col, props, "cube_size")
        elif prim_type == 'CUBOID': self._draw_vector_xyz(col, props, "cuboid_dimensions", "Dimensions")
        elif prim_type == 'PLANE': self._draw_prop_with_reset(col, props, "plane_size_x"); self._draw_prop_with_reset(col, props, "plane_size_y")
        elif prim_type == 'CIRCLE': self._draw_prop_with_reset(col, props, "circle_radius"); self._draw_prop_with_reset(col, props, "circle_vertices")
        elif prim_type == 'ELLIPSE': self._draw_prop_with_reset(col, props, "ellipse_radius_x"); self._draw_prop_with_reset(col, props, "ellipse_radius_y"); self._draw_prop_with_reset(col, props, "ellipse_vertices")
        elif prim_type == 'UV_SPHERE': self._draw_prop_with_reset(col, props, "uv_sphere_radius"); self._draw_prop_with_reset(col, props, "uv_sphere_segments"); self._draw_prop_with_reset(col, props, "uv_sphere_rings")
        elif prim_type == 'ICO_SPHERE': self._draw_prop_with_reset(col, props, "ico_sphere_radius"); self._draw_prop_with_reset(col, props, "ico_sphere_subdivisions")
        elif prim_type == 'CYLINDER':
            col.prop(props, "cylinder_mode", text="")
            col.separator(factor=0.5)
            self._draw_prop_with_reset(col, props, "cylinder_radius")
            if props.cylinder_mode == 'CENTER':
                self._draw_prop_with_reset(col, props, "cylinder_depth")
            else:
                col.separator()
                self._draw_vector_xyz(col, props, "cylinder_point_top", "Top Point:")
                col.separator(factor=0.5)
                self._draw_vector_xyz(col, props, "cylinder_point_bottom", "Bottom Point:")
            col.separator(factor=0.5)
            self._draw_prop_with_reset(col, props, "cylinder_vertices")
            row_caps = col.row(align=True)
            row_caps.prop(props, "cylinder_cap_top")
            row_caps.prop(props, "cylinder_cap_bottom")
        elif prim_type == 'FRUSTUM':
            col.prop(props, "frustum_mode", text="")
            col.separator(factor=0.5)
            self._draw_prop_with_reset(col, props, "frustum_radius_top")
            self._draw_prop_with_reset(col, props, "frustum_radius_bottom")
            if props.frustum_mode == 'CENTER':
                self._draw_prop_with_reset(col, props, "frustum_height")
            else:
                col.separator()
                self._draw_vector_xyz(col, props, "frustum_point_top", "Top Point:")
                col.separator(factor=0.5)
                self._draw_vector_xyz(col, props, "frustum_point_bottom", "Bottom Point:")
            col.separator(factor=0.5)
            self._draw_prop_with_reset(col, props, "frustum_vertices")
            row_caps = col.row(align=True); row_caps.prop(props, "frustum_cap_top"); row_caps.prop(props, "frustum_cap_bottom")
        elif prim_type == 'IMAGE_CYLINDER':
            self._draw_prop_with_reset(col, props, "image_cylinder_radius")
            self._draw_prop_with_reset(col, props, "image_cylinder_depth")
            self._draw_prop_with_reset(col, props, "image_cylinder_vertices")
            col.separator()
            box_img = col.box()
            box_img.label(text="内側に貼る画像:")
            box_img.template_ID(props, "image_cylinder_image", open="image.open")
            
            box_img.separator()
            self._draw_vector_2d(box_img, props, "image_cylinder_uv_offset", "UV Offset (位置ずらし):")
            box_img.separator()
            self._draw_vector_2d(box_img, props, "image_cylinder_uv_scale", "UV Scale (繰り返し/縮尺):")
            
            col.separator()
            self._draw_prop_with_reset(col, props, "image_cylinder_alpha_outer")
            self._draw_prop_with_reset(col, props, "image_cylinder_alpha_inner")
        elif prim_type == 'CUBE_FRAME':
            self._draw_prop_with_reset(col, props, "cube_frame_size")
            self._draw_prop_with_reset(col, props, "cube_frame_radius")
            self._draw_prop_with_reset(col, props, "cube_frame_vertices")
        elif prim_type == 'CUBOID_FRAME':
            self._draw_vector_xyz(col, props, "cuboid_frame_dimensions", "Dimensions")
            self._draw_prop_with_reset(col, props, "cuboid_frame_radius")
            self._draw_prop_with_reset(col, props, "cuboid_frame_vertices")
        elif prim_type == 'TORUS':
            self._draw_prop_with_reset(col, props, "torus_major_radius"); self._draw_prop_with_reset(col, props, "torus_minor_radius")
            self._draw_prop_with_reset(col, props, "torus_major_segments"); self._draw_prop_with_reset(col, props, "torus_minor_segments")
        elif prim_type == 'MONKEY':
            self._draw_prop_with_reset(col, props, "monkey_size")
        
        # 配列図形UI
        elif prim_type.startswith('SPHERE_'):
            self._draw_prop_with_reset(col, props, "sphere_spacing")
            if 'SQUARE' in prim_type or 'CUBE' in prim_type:
                self._draw_prop_with_reset(col, props, "sphere_size", text="全体サイズ")
            else:
                self._draw_prop_with_reset(col, props, "sphere_radius_val", text="全体半径")
            
            col.separator()
            box_sph = col.box()
            box_sph.label(text="個々の球体設定:")
            self._draw_prop_with_reset(box_sph, props, "sphere_elem_radius", text="球の半径")
            self._draw_prop_with_reset(box_sph, props, "sphere_segments")
            self._draw_prop_with_reset(box_sph, props, "sphere_rings")
            
        elif prim_type == 'TORUS_CONCENTRIC':
            self._draw_prop_with_reset(col, props, "torus_count", text="同心円の数")
            self._draw_prop_with_reset(col, props, "torus_spacing", text="間隔")
            self._draw_prop_with_reset(col, props, "torus_minor_radius_arr", text="トーラスの太さ")
            self._draw_prop_with_reset(col, props, "torus_major_segments")
            self._draw_prop_with_reset(col, props, "torus_minor_segments")
            
        elif prim_type == 'CYLINDER_GRID':
            box_c = col.box()
            box_c.label(text="Count (本数):")
            row_c = box_c.row(align=True)
            row_c.prop(props, "grid_count_x", text="X")
            row_c.prop(props, "grid_count_y", text="Y")
            row_c.prop(props, "grid_count_z", text="Z")
            
            box_s = col.box()
            box_s.label(text="Spacing (間隔):")
            row_s = box_s.row(align=True)
            row_s.prop(props, "grid_spacing_x", text="X")
            row_s.prop(props, "grid_spacing_y", text="Y")
            row_s.prop(props, "grid_spacing_z", text="Z")
            
            col.separator()
            self._draw_prop_with_reset(col, props, "grid_radius", text="円柱の太さ")
            self._draw_prop_with_reset(col, props, "grid_vertices", text="円柱の頂点数")

class PT_TransformPanel(PG_BasePanel, Panel):
    bl_label = "Transform (位置・回転・スケール)"
    bl_idname = f"{OP_PREFIX}_PT_transform"
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        props = getattr(context.scene, PROPS_NAME)
        layout = self.layout
        
        disable_transform = False
        if props.primitive_type == 'CYLINDER' and props.cylinder_mode != 'CENTER':
            disable_transform = True
        elif props.primitive_type == 'FRUSTUM' and props.frustum_mode != 'CENTER':
            disable_transform = True

        main_col = layout.column()
        main_col.enabled = not disable_transform

        if disable_transform:
            main_col.label(text="※ 端点指定モードでは操作できません", icon='INFO')

        self._draw_vector_xyz(main_col, props, "location", "Location:")
        main_col.separator()
        
        col_scale = main_col.column(align=True)
        row_scale_uni = col_scale.row(align=True)
        row_scale_uni.prop(props, "scale_uniform", text="Uniform Scale")
        if props.scale_uniform:
            self._draw_prop_with_reset(row_scale_uni, props, "scale_factor", text="")
        else:
            self._draw_vector_xyz(col_scale, props, "scale_vector", "Scale:")
            
        main_col.separator()
        col_rot = main_col.column(align=True)
        col_rot.label(text="Rotation (XYZ Euler):")
        self._draw_prop_with_reset(col_rot, props, "additional_rotation_x", text="X")
        self._draw_prop_with_reset(col_rot, props, "additional_rotation_y", text="Y")
        self._draw_prop_with_reset(col_rot, props, "additional_rotation_z", text="Z")

class PT_MaterialPanel(PG_BasePanel, Panel):
    bl_label = "Material & Color"
    bl_idname = f"{OP_PREFIX}_PT_material"
    bl_options = {'DEFAULT_CLOSED'}
    
    def draw(self, context):
        props = getattr(context.scene, PROPS_NAME)
        layout = self.layout
        
        if props.primitive_type == 'IMAGE_CYLINDER':
            layout.label(text="※ 画像パノラマ筒は上部の専用設定が適用されます", icon='INFO')
            return
            
        col_mat = layout.column(align=True)
        col_mat.prop(props, "color_mode", text="Mode")
        
        if props.color_mode == 'PRESET':
            col_mat.prop(props, "preset_set")
            box_presets = col_mat.box()
            box_presets.label(text="番号クリックで適用 / 色枠で編集:")
            grid = box_presets.grid_flow(row_major=True, columns=5, even_columns=True, align=True)
            preset_prefix = f"preset_color_{props.preset_set.lower()}_" if props.preset_set != 'A' else "preset_color_"
            for i in range(1, 11):
                row = grid.row(align=True)
                row.prop_enum(props, "preset_color", str(i), text=str(i))
                row.prop(props, f"{preset_prefix}{i}", text="")
        elif props.color_mode == 'PICKER':
            self._draw_prop_with_reset(col_mat, props, "custom_color", text="")
            
        layout.separator()
        self._draw_prop_with_reset(layout, props, "face_alpha")
        
        layout.separator()
        layout.operator(OT_ResetAllSettings.bl_idname, icon='LOOP_BACK')

class PT_LinksPanel(PG_BasePanel, Panel):
    bl_label = "Links"
    bl_idname = f"{OP_PREFIX}_PT_links"
    bl_options = {'DEFAULT_CLOSED'}

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

        def draw_section(prop_name, link_list, title):
            box = layout.box()
            row = box.row()
            is_expanded = getattr(props, prop_name)
            row.prop(props, prop_name, icon="TRIA_DOWN" if is_expanded else "TRIA_RIGHT", emboss=False, text=title)
            if is_expanded:
                col = box.column(align=True)
                for link in link_list:
                    op = col.operator(OT_OpenUrl.bl_idname, text=link["label"], icon='URL')
                    op.url = link["url"]

        draw_section("show_main_docs", THIS_LINKS, "This Addon")
        draw_section("show_new_docs", NEW_DOC_LINKS, "Documents Index")
        draw_section("show_old_docs", DOC_LINKS, "Old Documents")
        draw_section("show_social", SOCIAL_LINKS, "Social Links")

class PT_RemovePanel(PG_BasePanel, Panel):
    bl_label = "System"
    bl_idname = f"{OP_PREFIX}_PT_remove"
    bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        self.layout.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Unregister Addon")

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

def initial_setup():
    context = bpy.context
    if not hasattr(context, "scene") or not context.scene:
        return 0.1  
    update_preview_geometry(context)
    return None

def open_sidebar():
    if not bpy.context: return None
    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': space.show_region_ui = True 
    return None

classes = (
    PG_B200Props,
    OT_ViewCenterFront, OT_CreatePrimitive, OT_CopyFullScript, OT_ResetProperty, OT_ResetAllSettings, OT_ResetCounter, OT_OpenUrl, OT_RemoveAddon,
    OT_View_GetCurrent, OT_View_Reset, OT_View_CenterSelected, OT_View_CopyActualPos, OT_View_CopyAngles,
    
    PT_ViewControlPanel,
    PT_MainPanel, 
    PT_TransformPanel, 
    PT_MaterialPanel, 
    PT_LinksPanel, 
    PT_RemovePanel
)

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_B200Props))
    bpy.types.Scene.b200_object_counter = IntProperty(name="Object Counter", default=1, min=1)
    
    if not bpy.app.timers.is_registered(initial_setup): 
        bpy.app.timers.register(initial_setup, first_interval=0.1)
    if not bpy.app.timers.is_registered(open_sidebar):
        bpy.app.timers.register(open_sidebar, first_interval=0.1)
    if not bpy.app.timers.is_registered(view_sync_timer):
        bpy.app.timers.register(view_sync_timer)

def unregister():
    if bpy.app.timers.is_registered(initial_setup): 
        bpy.app.timers.unregister(initial_setup)
    if bpy.app.timers.is_registered(view_sync_timer):
        bpy.app.timers.unregister(view_sync_timer)
        
    if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
    if hasattr(bpy.types.Scene, 'b200_object_counter'): delattr(bpy.types.Scene, 'b200_object_counter')
    
    for c in reversed(classes):
        try: bpy.utils.unregister_class(c)
        except RuntimeError: pass

if __name__ == "__main__": 
    try: unregister()
    except Exception: pass
    register()

# ▲▲▲ ここまで ▲▲▲
'''

# ② Viewport Color & Sun (view2026316) のコードを以下に貼り付け
VIEWPORT_SCRIPT_CONTENT = r'''
# ▼▼▼ ここに「3D Viewport Color & Sun」の全コードを貼り付けてください ▼▼▼

# Copied: 20260319 15:00:01
import bpy
import os
import math
from bpy.props import FloatVectorProperty, FloatProperty, EnumProperty, StringProperty, BoolProperty, PointerProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector, Euler
from math import radians
from datetime import datetime

# ★ このスクリプト自身のID (コピー機能で使用)
# ### ZIONAD_SOURCE_ID: VIEWPORT_COLOR_2026_03_16 ###
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: VIEWPORT_COLOR_2026_03_16 ###"

# アドオンのメタデータ
bl_info = {
    "name": "zionad 5520[ 3D Viewport Color & Sun & Perspective ] 20260319",
    "author": "zionadchat",
    "version": (3, 0, 0),
    "blender": (4, 4, 0),
    "category": "   5520[  3D Viewport  ]   ",
    "description": "3Dビューポートの色、太陽、透視投影視座位置をリアルタイム制御します",
    "location": "3Dビュー > サイドバー",
}

# 定数
ADDON_CATEGORY_NAME = bl_info["category"]
PREFIX = "view2026316"

VIEW_RESET_BTN_TEXT = "Reset View (0, -10, 10)"  
VIEW_POS_INIT = (0.0, -10.0, 10.0)  

# ==============================================================================
#  デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# プリセットによる上書きを防ぐため、プリセット項目を先に読み込む順序にしています
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "sun_control_mode": 'ANGLE',
    "grid_preset": 'white',
    "wire_preset": 'orange',
    "camera_preset": 'Cam',
    "background_type": 'LINEAR',
    "header_preset": 'Dark Green',
    "preset": 'Dark Green',
    "render_preset": 'Blue',
    "outliner_preset": 'Outliner 4.4.0',
    "text_editor_preset": 'Text 4.4.0',
    "sun_target_location": (0.0000, 0.0000, 0.0000),
    "sun_rotation": (0.7854, 0.0000, 0.7854),
    "sun_location": (0.0000, 0.0000, 10.0000),
    "sun_strength": 2.5000,
    "custom_grid_scale": 1.0000,
    "grid_color": (1.0000, 1.0000, 1.0000, 1.0000),
    "wire_color": (0.0000, 0.0000, 0.0000),
    "camera_color": (0.4700, 0.5500, 1.0000),
    "header_color": (0.0000, 0.0300, 0.0000, 1.0000),
    "custom_gradient_high": (0.2256, 0.2800, 0.1424),
    "custom_gradient_low": (0.1000, 0.1500, 0.0500),
    "reverse_gradient": False,
    "render_color": (0.1900, 0.6000, 1.0000, 1.0000),
    "render_environment_strength": 1.0000,
    "outliner_header_color": (0.1900, 0.1900, 0.1900, 0.7000),
    "outliner_background_color": (0.1400, 0.1400, 0.1400, 1.0000),
    "text_editor_header_color": (0.1900, 0.1900, 0.1900, 0.7000),
    "text_editor_background_color": (0.1400, 0.1400, 0.1400, 1.0000),
    "view_pos": (0.0000, -10.0000, 10.0000),
}
# <END_DICT>

# パネル定義
COPY_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_copy_panel"
PERSP_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_persp_control"
OVERLAY_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_overlay_panel"
BG_PANEL_IDNAME_1 = f"{PREFIX}_VIEW3D_PT_solid_background_panel"
HEADER_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_header_panel"
RENDER_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_render_panel"
SUN_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_sun_panel"
GRID_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_gridpanel"
WIRE_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_wirepanel"
CAMERA_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_camerapanel"
OUTLINER_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_outliner_panel"
TEXT_EDITOR_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_text_editor_panel"
LINK_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_link_panel"
REMOVE_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_remove"

# パネルラベル
PANEL_LABELS = {
    "COPY": "コードコピー",
    "PERSP": "透視投影 視座位置",
    "OVERLAY": "Overlays",
    "BACKGROUND": "3D Viewport Color",
    "HEADER": "Header Color",
    "RENDER": "Render Color",
    "SUN": "太陽 設定",
    "GRID": "Grid Color",
    "WIRE": "Wire Color",
    "CAMERA": "Camera Color",
    "OUTLINER": "Outliner Color",
    "TEXT_EDITOR": "Text Editor Color",
    "LINK": "リンク",
    "REMOVE": "アドオン削除",
}

# プリセット群
BASE_PRESETS =[
    ("Dark Green", "Dark Green", "Dark Green background", (0.00, 0.28, 0.02), (0.10, 0.15, 0.05)),
    ("purple", "purple", "purple background", (0.49, 0.45, 1.00), (0.74, 0.65, 0.88)),
    ("BlueGreen", "BlueGreen", "BlueGreen background", (0.14, 0.34, 0.83), (0.57, 0.88, 0.63)),
    ("DARK_BLUE", "Dark Blue", "Dark Blue background", (0.07, 0.13, 0.31), (0.05, 0.05, 0.15)),
    ("Viewport 4.4.0", "Viewport 4.4.0", "Viewport 4.4.0 background", (0.24, 0.24, 0.24), (0.19, 0.19, 0.19)),
    ("FOREST_GREEN", "Forest Green", "Forest Green background", (0.50, 0.70, 0.50), (0.10, 0.15, 0.05)),
]

HEADER_PRESETS =[
    ("Dark Green", "Dark Green", "Dark Green header", (0.00, 0.03, 0.00, 1.00)),
    ("purple", "purple", "purple header", (0.00, 0.00, 0.00, 1.00)),
    ("BlueGreen", "BlueGreen", "BlueGreen header", (0.00, 0.00, 0.00, 1.00)),
    ("DARK_BLUE", "Dark Blue", "Dark Blue header", (0.10, 0.10, 0.30, 1.00)),
    ("Viewport 4.4.0", "Viewport 4.4.0", "Viewport 4.4.0 header", (0.19, 0.19, 0.19, 0.70)),
    ("FOREST_GREEN", "Forest Green", "Forest Green header", (0.20, 0.30, 0.10, 1.00)),
]

RENDER_PRESETS =[
    ("Blue", "Blue", "Blue render color", (0.19, 0.60, 1.00, 1.00)),
    ("Render 4.4.0", "Render 4.4.0", "Render 4.4.0 color", (0.05, 0.05, 0.05, 1.00)),
    ("LIGHT_GRAY", "Light Gray", "Light gray render", (0.80, 0.80, 0.80, 1.00)),
]

GRID_PRESETS =[
    ("white", "white", "white grid color", (1.00, 1.00, 1.00, 1.00)),
    ("Grid 4.4.0", "Grid 4.4.0", "Grid 4.4.0 color", (0.33, 0.33, 0.33, 0.50)),
    ("DARK_GRAY", "Dark Gray", "Dark gray grid", (0.10, 0.10, 0.10, 1.00)),
]

WIRE_PRESETS =[
    ("orange", "orange", "orange wire", (0.71, 0.21, 0.05)),
    ("Wire 4.4.0", "Wire 4.4.0", "Wire 4.4.0 color", (0.00, 0.00, 0.00)),
    ("WHITE", "White", "White wire", (1.00, 1.00, 1.00)),
]

CAMERA_PRESETS =[
    ("Cam", "Cam", "Cam camera color", (0.47, 0.55, 1.00)),
    ("Cam 4.4.0", "Cam 4.4.0", "Cam 4.4.0 color", (0.00, 0.00, 0.00)),
    ("YELLOW", "Yellow", "Yellow camera", (1.00, 1.00, 0.00)),
]

OUTLINER_PRESETS =[
    ("Outliner 4.4.0", "Outliner 4.4.0", "Outliner 4.4.0 colors", (0.19, 0.19, 0.19, 0.70), (0.14, 0.14, 0.14, 1.00)),
    ("DARK_TEAL", "Dark Teal", "Dark teal outliner", (0.00, 0.20, 0.20, 1.00), (0.00, 0.10, 0.10, 1.00)),
]

TEXT_EDITOR_PRESETS =[
    ("Text 4.4.0", "Text 4.4.0", "Text Editor 4.4.0 colors", (0.19, 0.19, 0.19, 0.70), (0.14, 0.14, 0.14, 1.00)),
    ("DARK_GREEN", "Dark Green", "Dark green text editor", (0.00, 0.20, 0.00, 1.00), (0.00, 0.10, 0.00, 1.00)),
]

BACKGROUND_TYPES =[
    ('SINGLE_COLOR', "Single Color", "Uniform background color"),
    ('LINEAR', "Linear", "Linear gradient background"),
    ('RADIAL', "Vignette", "Radial gradient simulating a vignette effect"),
]

# ==============================================================================
#  リアルタイム更新用コールバック関数(ビューポート色・太陽)
# ==============================================================================
def format_tuple(t):
    return '(' + ', '.join(f"{x:.3f}" for x in t) + ')'

def update_custom_grid_scale(self, context):
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D':
                area.spaces.active.overlay.grid_scale = self.custom_grid_scale

def update_grid_color(self, context):
    bpy.context.preferences.themes[0].view_3d.grid = self.grid_color

def update_wire_color(self, context):
    bpy.context.preferences.themes[0].view_3d.wire = self.wire_color

def update_camera_color(self, context):
    bpy.context.preferences.themes[0].view_3d.camera = self.camera_color

def update_background_color(self, context):
    gradients = bpy.context.preferences.themes[0].view_3d.space.gradients
    if self.background_type == 'SINGLE_COLOR':
        gradients.background_type = 'LINEAR'
        gradients.high_gradient = gradients.gradient = self.custom_gradient_low if self.reverse_gradient else self.custom_gradient_high
    else:
        gradients.background_type = self.background_type
        gradients.high_gradient = self.custom_gradient_low if self.reverse_gradient else self.custom_gradient_high
        gradients.gradient = self.custom_gradient_high if self.reverse_gradient else self.custom_gradient_low
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D': area.tag_redraw()

def update_header_color(self, context):
    bpy.context.preferences.themes[0].view_3d.space.header = self.header_color
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D': area.tag_redraw()

def update_render_color(self, context):
    bpy.context.preferences.themes[0].image_editor.space.back = self.render_color[:3]
    world = bpy.data.worlds.get('MyWorld') or bpy.data.worlds.new('MyWorld')
    context.scene.world = world
    world.use_nodes = True
    bg_node = world.node_tree.nodes.get('Background') or world.node_tree.nodes.new(type='ShaderNodeBackground')
    bg_node.name = 'Background'
    bg_node.inputs[0].default_value = self.render_color
    bg_node.inputs[1].default_value = self.render_environment_strength
    output_node = world.node_tree.nodes.get('World Output') or world.node_tree.nodes.new(type='ShaderNodeOutputWorld')
    output_node.name = 'World Output'
    world.node_tree.links.new(bg_node.outputs[0], output_node.inputs['Surface'])

def update_outliner_color(self, context):
    space = bpy.context.preferences.themes[0].outliner.space
    space.header = self.outliner_header_color
    space.back = self.outliner_background_color[:3]
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'OUTLINER': area.tag_redraw()

def update_text_editor_color(self, context):
    space = bpy.context.preferences.themes[0].text_editor.space
    space.header = self.text_editor_header_color
    space.back = self.text_editor_background_color[:3]
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'TEXT_EDITOR': area.tag_redraw()

def get_or_create_sun():
    sun_obj = bpy.data.objects.get("Sun")
    if sun_obj is None or sun_obj.type != 'LIGHT' or sun_obj.data.type != 'SUN':
        if sun_obj:
            try: bpy.data.objects.remove(sun_obj, do_unlink=True)
            except: pass
        bpy.ops.object.light_add(type='SUN', align='WORLD', location=(0, 0, 0))
        sun_obj = bpy.context.active_object
        sun_obj.name = "Sun"; sun_obj.data.name = "Sun"
    return sun_obj

def update_sun(self, context):
    sun = get_or_create_sun()
    sun.location = self.sun_location
    if self.sun_control_mode == 'ANGLE':
        sun.rotation_euler = self.sun_rotation
    else:
        target_vec = Vector(self.sun_target_location)
        sun_vec = Vector(self.sun_location)
        if (target_vec - sun_vec).length_squared < 0.0001: return
        direction = target_vec - sun_vec
        sun.rotation_euler = direction.to_track_quat('-Z', 'Y').to_euler()
    sun.data.energy = self.sun_strength

# ==============================================================================
#  リアルタイム更新用コールバック関数(透視投影 視座位置)
# ==============================================================================
_is_updating_view = False

def update_view_position(self, context):
    """スライダーが操作されたときに視点を更新する"""
    global _is_updating_view
    if _is_updating_view: return
    
    props = getattr(context.scene, "persp_view_props", None)
    if not props: return

    limit = props.slider_limit
    v = list(props.view_pos)
    clamped = False
    for i in range(3):
        if v[i] > limit: v[i] = limit; clamped = True
        elif v[i] < -limit: v[i] = -limit; clamped = True
            
    if clamped:
        _is_updating_view = True
        props.view_pos = v
        _is_updating_view = False
    
    _is_updating_view = True
    try:
        cam_pos = Vector(props.view_pos)
        for window in 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':
                            r3d = space.region_3d
                            r3d.view_perspective = 'PERSP'
                            target_pos = Vector(r3d.view_location)
                            rel_pos = cam_pos - target_pos
                            dist = rel_pos.length
                            if dist > 0.001:
                                r3d.view_distance = dist
                                r3d.view_rotation = rel_pos.to_track_quat('Z', 'Y')
    finally:
        _is_updating_view = False

def view_sync_timer():
    """マウス操作での視点移動を検知し、スライダー(UI)を同期するタイマー"""
    global _is_updating_view
    if _is_updating_view: return 0.05
    
    context = bpy.context
    if getattr(context, "scene", None) is None: return 0.05
    props = getattr(context.scene, "persp_view_props", None)
    if not props: return 0.05

    r3d = None
    target_area = None
    for window in 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':
                        r3d = space.region_3d
                        target_area = area
                        break
                if r3d: break
        if r3d: break

    if r3d and target_area:
        target_pos = Vector(r3d.view_location)
        actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
        
        current_pos = Vector(props.view_pos)
        if (current_pos - actual_cam_pos).length > 0.001:
            _is_updating_view = True
            max_val = max(abs(actual_cam_pos.x), abs(actual_cam_pos.y), abs(actual_cam_pos.z))
            if max_val > props.slider_limit:
                props.slider_limit = max_val + 50.0 
                
            props.view_pos = actual_cam_pos
            _is_updating_view = False
            target_area.tag_redraw()
            
    return 0.05

# ------------------------------------------------------------------------
# Property Groups
# ------------------------------------------------------------------------
class SunSettingsProperties(PropertyGroup):
    sun_control_mode: EnumProperty(name="制御モード", items=[('ANGLE', "角度", "太陽の回転を直接指定"), ('TARGET', "ターゲット", "指定位置に太陽を向ける")], default=CURRENT_DEFAULTS["sun_control_mode"], update=update_sun)
    sun_target_location: FloatVectorProperty(name="ターゲット位置", subtype='XYZ', default=CURRENT_DEFAULTS["sun_target_location"], update=update_sun)
    sun_rotation: FloatVectorProperty(name="角度", subtype='EULER', unit='ROTATION', default=CURRENT_DEFAULTS["sun_rotation"], update=update_sun)
    sun_location: FloatVectorProperty(name="位置", subtype='XYZ', default=CURRENT_DEFAULTS["sun_location"], update=update_sun)
    sun_strength: FloatProperty(name="強さ", default=CURRENT_DEFAULTS["sun_strength"], min=0.0, update=update_sun)

class ViewportColorProperties(PropertyGroup):
    custom_grid_scale: FloatProperty(name="Scale", default=CURRENT_DEFAULTS["custom_grid_scale"], min=0.001, update=update_custom_grid_scale)
    grid_color: FloatVectorProperty(name="Grid Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["grid_color"], update=update_grid_color)
    grid_preset: EnumProperty(name="Grid Preset", items=[(p[0], p[1], p[2]) for p in GRID_PRESETS], default=CURRENT_DEFAULTS["grid_preset"], update=lambda self, context: self.update_grid_preset(context))
    wire_color: FloatVectorProperty(name="Wire Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=CURRENT_DEFAULTS["wire_color"], update=update_wire_color)
    wire_preset: EnumProperty(name="Wire Preset", items=[(p[0], p[1], p[2]) for p in WIRE_PRESETS], default=CURRENT_DEFAULTS["wire_preset"], update=lambda self, context: self.update_wire_preset(context))
    camera_color: FloatVectorProperty(name="Camera Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=CURRENT_DEFAULTS["camera_color"], update=update_camera_color)
    camera_preset: EnumProperty(name="Camera Preset", items=[(p[0], p[1], p[2]) for p in CAMERA_PRESETS], default=CURRENT_DEFAULTS["camera_preset"], update=lambda self, context: self.update_camera_preset(context))
    background_type: EnumProperty(name="Background Type", items=BACKGROUND_TYPES, default=CURRENT_DEFAULTS["background_type"], update=update_background_color)
    header_color: FloatVectorProperty(name="Header Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["header_color"], update=update_header_color)
    header_preset: EnumProperty(name="Header Preset", items=[(p[0], p[1], p[2]) for p in HEADER_PRESETS], default=CURRENT_DEFAULTS["header_preset"], update=lambda self, context: self.update_header_preset(context))
    custom_gradient_high: FloatVectorProperty(name="Gradient High Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=CURRENT_DEFAULTS["custom_gradient_high"], update=update_background_color)
    custom_gradient_low: FloatVectorProperty(name="Gradient Low Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=CURRENT_DEFAULTS["custom_gradient_low"], update=update_background_color)
    reverse_gradient: BoolProperty(name="Reverse Gradient", default=CURRENT_DEFAULTS["reverse_gradient"], update=update_background_color)
    preset: EnumProperty(name="Color Preset", items=[(p[0], p[1], p[2]) for p in BASE_PRESETS], default=CURRENT_DEFAULTS["preset"], update=lambda self, context: self.update_preset(context))
    render_color: FloatVectorProperty(name="Render Background Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["render_color"], update=update_render_color)
    render_preset: EnumProperty(name="Render Preset", items=[(p[0], p[1], p[2]) for p in RENDER_PRESETS], default=CURRENT_DEFAULTS["render_preset"], update=lambda self, context: self.update_render_preset(context))
    render_environment_strength: FloatProperty(name="Render Environment Strength", default=CURRENT_DEFAULTS["render_environment_strength"], min=0.0, max=1900.0, update=update_render_color)
    outliner_header_color: FloatVectorProperty(name="Outliner Header Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["outliner_header_color"], update=update_outliner_color)
    outliner_background_color: FloatVectorProperty(name="Outliner Background Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["outliner_background_color"], update=update_outliner_color)
    outliner_preset: EnumProperty(name="Outliner Preset", items=[(p[0], p[1], p[2]) for p in OUTLINER_PRESETS], default=CURRENT_DEFAULTS["outliner_preset"], update=lambda self, context: self.update_outliner_preset(context))
    text_editor_header_color: FloatVectorProperty(name="Text Editor Header Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["text_editor_header_color"], update=update_text_editor_color)
    text_editor_background_color: FloatVectorProperty(name="Text Editor Background Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["text_editor_background_color"], update=update_text_editor_color)
    text_editor_preset: EnumProperty(name="Text Editor Preset", items=[(p[0], p[1], p[2]) for p in TEXT_EDITOR_PRESETS], default=CURRENT_DEFAULTS["text_editor_preset"], update=lambda self, context: self.update_text_editor_preset(context))

    def update_grid_preset(self, context):
        for p in GRID_PRESETS:
            if p[0] == self.grid_preset: self.grid_color = p[3]; break
    def update_wire_preset(self, context):
        for p in WIRE_PRESETS:
            if p[0] == self.wire_preset: self.wire_color = p[3]; break
    def update_camera_preset(self, context):
        for p in CAMERA_PRESETS:
            if p[0] == self.camera_preset: self.camera_color = p[3]; break
    def update_preset(self, context):
        for p in BASE_PRESETS:
            if p[0] == self.preset: self.custom_gradient_high = p[3]; self.custom_gradient_low = p[4]; break
    def update_header_preset(self, context):
        for p in HEADER_PRESETS:
            if p[0] == self.header_preset: self.header_color = p[3]; break
    def update_render_preset(self, context):
        for p in RENDER_PRESETS:
            if p[0] == self.render_preset: self.render_color = p[3]; break
    def update_outliner_preset(self, context):
        for p in OUTLINER_PRESETS:
            if p[0] == self.outliner_preset: self.outliner_header_color = p[3]; self.outliner_background_color = p[4]; break
    def update_text_editor_preset(self, context):
        for p in TEXT_EDITOR_PRESETS:
            if p[0] == self.text_editor_preset: self.text_editor_header_color = p[3]; self.text_editor_background_color = p[4]; break

class PerspViewProperties(PropertyGroup):
    slider_limit: FloatProperty(name="Range Limit", default=300.0, min=10.0, max=10000.0)
    view_pos: FloatVectorProperty(name="View Position", size=3, soft_min=-10000.0, soft_max=10000.0, 
                                  default=CURRENT_DEFAULTS.get('view_pos', VIEW_POS_INIT), 
                                  update=update_view_position)

# ------------------------------------------------------------------------
# Operators
# ------------------------------------------------------------------------
class OT_ViewCenterFront(Operator):
    bl_idname = f"{PREFIX}_wm.view_center_front"
    bl_label = "Center 0,0,0 (Front View)"
    bl_description = "原点(0,0,0)を画面中央に配置し、Yマイナス方向からの視点(正面)にします"
    
    def execute(self, context):
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                rv3d = area.spaces.active.region_3d
                if rv3d:
                    rv3d.view_location = (0.0, 0.0, 0.0)
                    rv3d.view_rotation = Euler((radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
                    if rv3d.view_distance < 10.0:
                        rv3d.view_distance = 60.0
        return {'FINISHED'}

class OT_CopyFullScript(Operator):
    bl_idname = f"{PREFIX}_wm.copy_script"
    bl_label = "Copy Script"
    
    def execute(self, context):
        props = context.scene.viewport_color_props
        sun_props = context.scene.sun_settings_props
        persp_props = context.scene.persp_view_props
        
        code = ""
        file_path = globals().get('__file__')
        if file_path:
            if file_path.endswith('.pyc') or file_path.endswith('.pyo'):
                file_path = file_path[:-1]
            try:
                if os.path.exists(file_path):
                    with open(file_path, 'r', encoding='utf-8') as f:
                        code = f.read()
            except Exception:
                pass
        
        if not code or SOURCE_ID_TAG not in code:
            for t in bpy.data.texts:
                if SOURCE_ID_TAG in t.as_string():
                    code = t.as_string()
                    break

        if not code:
            self.report({'ERROR'}, "スクリプトのソースが見つかりません。")
            return {'CANCELLED'}

        def fmt_vec(v): return "(" + ", ".join(f"{x:.4f}" for x in v) + ")"
        def fmt_str(s): return f"'{s}'"

        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "sun_control_mode": {fmt_str(sun_props.sun_control_mode)},\n'
        new_dict += f'    "grid_preset": {fmt_str(props.grid_preset)},\n'
        new_dict += f'    "wire_preset": {fmt_str(props.wire_preset)},\n'
        new_dict += f'    "camera_preset": {fmt_str(props.camera_preset)},\n'
        new_dict += f'    "background_type": {fmt_str(props.background_type)},\n'
        new_dict += f'    "header_preset": {fmt_str(props.header_preset)},\n'
        new_dict += f'    "preset": {fmt_str(props.preset)},\n'
        new_dict += f'    "render_preset": {fmt_str(props.render_preset)},\n'
        new_dict += f'    "outliner_preset": {fmt_str(props.outliner_preset)},\n'
        new_dict += f'    "text_editor_preset": {fmt_str(props.text_editor_preset)},\n'
        
        new_dict += f'    "sun_target_location": {fmt_vec(sun_props.sun_target_location)},\n'
        new_dict += f'    "sun_rotation": {fmt_vec(sun_props.sun_rotation)},\n'
        new_dict += f'    "sun_location": {fmt_vec(sun_props.sun_location)},\n'
        new_dict += f'    "sun_strength": {sun_props.sun_strength:.4f},\n'
        new_dict += f'    "custom_grid_scale": {props.custom_grid_scale:.4f},\n'
        new_dict += f'    "grid_color": {fmt_vec(props.grid_color)},\n'
        new_dict += f'    "wire_color": {fmt_vec(props.wire_color)},\n'
        new_dict += f'    "camera_color": {fmt_vec(props.camera_color)},\n'
        new_dict += f'    "header_color": {fmt_vec(props.header_color)},\n'
        new_dict += f'    "custom_gradient_high": {fmt_vec(props.custom_gradient_high)},\n'
        new_dict += f'    "custom_gradient_low": {fmt_vec(props.custom_gradient_low)},\n'
        new_dict += f'    "reverse_gradient": {props.reverse_gradient},\n'
        new_dict += f'    "render_color": {fmt_vec(props.render_color)},\n'
        new_dict += f'    "render_environment_strength": {props.render_environment_strength:.4f},\n'
        new_dict += f'    "outliner_header_color": {fmt_vec(props.outliner_header_color)},\n'
        new_dict += f'    "outliner_background_color": {fmt_vec(props.outliner_background_color)},\n'
        new_dict += f'    "text_editor_header_color": {fmt_vec(props.text_editor_header_color)},\n'
        new_dict += f'    "text_editor_background_color": {fmt_vec(props.text_editor_background_color)},\n'
        new_dict += f'    "view_pos": {fmt_vec(persp_props.view_pos)},\n'
        new_dict += "}\n"

        try:
            start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
            parts = code.split(start)
            if len(parts) < 2: return {'CANCELLED'}
            pre = parts[0]
            post = code.split(end)[1]
            
            lines = pre.split('\n')
            if len(lines) > 0 and lines[0].startswith("# Copied:"): lines.pop(0)
            pre = '\n'.join(lines).lstrip('\n')
            
            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'}, "現在の数値でコードをコピーしました!")
        except Exception as e:
            self.report({'ERROR'}, f"Failed to parse code: {str(e)}")
            return {'CANCELLED'}
            
        return {'FINISHED'}

class OVERLAY_OT_set_grid_scale(Operator):
    bl_idname = f"{PREFIX}_overlay.set_grid_scale"
    bl_label = "Set Grid Scale"
    scale_value: FloatProperty()

    def execute(self, context):
        for window in context.window_manager.windows:
            for area in window.screen.areas:
                if area.type == 'VIEW_3D': area.spaces.active.overlay.grid_scale = self.scale_value
        return {'FINISHED'}

class SUN_OT_Create(Operator):
    bl_idname = f"{PREFIX}.create_sun"; bl_label = "太陽を作成"
    def execute(self, context):
        get_or_create_sun()
        self.report({'INFO'}, "太陽を作成しました。"); return {'FINISHED'}

class SUN_OT_Reset(Operator):
    bl_idname = f"{PREFIX}.reset_sun"; bl_label = "太陽の設定を初期値にリセット"
    def execute(self, context):
        props = context.scene.sun_settings_props
        props.sun_control_mode, props.sun_target_location = 'ANGLE', (0.0, 0.0, 0.0)
        props.sun_rotation = (radians(45.0), radians(0.0), radians(45.0))
        props.sun_location, props.sun_strength = (0.0, 0.0, 10.0), 2.5
        update_sun(props, context)
        self.report({'INFO'}, "太陽の設定をリセットしました。"); return {'FINISHED'}

class RemoveAllPanels(Operator):
    bl_idname = f"{PREFIX}_wm.remove_all_panels"; bl_label = PANEL_LABELS["REMOVE"]
    def execute(self, context): unregister(); return {'FINISHED'}

# --- 透視投影用のオペレーター ---
class PERSP_OT_GetCurrentView(Operator):
    bl_idname = f"{PREFIX}_persp.get_current_view"
    bl_label = "Get Current View & Update"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, "persp_view_props", None)
        r3d = context.space_data.region_3d if context.space_data else None
        if not props or not r3d: return {'CANCELLED'}
        target_pos = Vector(r3d.view_location)
        actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
        max_val = max(abs(actual_cam_pos.x), abs(actual_cam_pos.y), abs(actual_cam_pos.z))
        if max_val > props.slider_limit: props.slider_limit = max_val + 50.0 
        props.view_pos = actual_cam_pos
        return {'FINISHED'}

class PERSP_OT_ResetView(Operator):
    bl_idname = f"{PREFIX}_persp.reset_view"
    bl_label = VIEW_RESET_BTN_TEXT
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, "persp_view_props", None)
        if props:
            for window in 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': space.region_3d.view_location = (0.0, 0.0, 0.0)
            props.view_pos = VIEW_POS_INIT 
        return {'FINISHED'}

class PERSP_OT_CenterSelected(Operator):
    bl_idname = f"{PREFIX}_persp.center_selected"
    bl_label = "Center Selected Object"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        bpy.ops.view3d.view_selected()
        r3d = context.space_data.region_3d if context.space_data else None
        props = getattr(context.scene, "persp_view_props", None)
        if r3d and props:
            target_pos = Vector(r3d.view_location)
            props.view_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
        return {'FINISHED'}

class PERSP_OT_CopyActualViewPos(Operator):
    bl_idname = f"{PREFIX}_persp.copy_actual_pos"
    bl_label = "Copy Position Only"
    def execute(self, context):
        r3d = context.space_data.region_3d if context.space_data else None
        if not r3d: return {'CANCELLED'}
        target_pos = Vector(r3d.view_location)
        p = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
        context.window_manager.clipboard = f"Actual View Pos: ({p.x:.4f}, {p.y:.4f}, {p.z:.4f})"
        self.report({'INFO'}, "視座位置をコピーしました")
        return {'FINISHED'}

class PERSP_OT_CopyAngles(Operator):
    bl_idname = f"{PREFIX}_persp.copy_angles"
    bl_label = "Copy Full Info (Pos & Angles)"
    def execute(self, context):
        props = getattr(context.scene, "persp_view_props", None)
        r3d = context.space_data.region_3d if context.space_data else None
        if not r3d or not props: return {'CANCELLED'}
            
        target_pos = Vector(r3d.view_location)
        actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
        vec = target_pos - actual_cam_pos
        length = vec.length
        
        if length < 0.0001: return {'CANCELLED'}
        ang_x = math.degrees(math.acos(vec.x / length))
        ang_y = math.degrees(math.acos(vec.y / length))
        ang_z = math.degrees(math.acos(vec.z / length))
        pl_x = math.degrees(math.asin(vec.x / length))
        pl_y = math.degrees(math.asin(vec.y / length))
        pl_z = math.degrees(math.asin(vec.z / length))
        
        info_text = (
            f"--- View Direction Info ---\n"
            f"[ Actual 3D View Status ]\n"
            f"Actual View Pos : ({actual_cam_pos.x:.4f}, {actual_cam_pos.y:.4f}, {actual_cam_pos.z:.4f})\n"
            f"Target Pos      : ({target_pos.x:.4f}, {target_pos.y:.4f}, {target_pos.z:.4f})\n"
            f"Distance        : {length:.4f}\n\n"
            f"[ Direction Angles (軸そのものとの角度 0〜180°) ]\n"
            f"Angle from X Axis : {ang_x:.2f} deg\n"
            f"Angle from Y Axis : {ang_y:.2f} deg\n"
            f"Angle from Z Axis : {ang_z:.2f} deg\n\n"
            f"[ Planar Angles (直感的な傾き・ズレ角 -90〜90°) ]\n"
            f"X (横のズレ角)    : {pl_x:.2f} deg\n"
            f"Y (前後の傾き)    : {pl_y:.2f} deg\n"
            f"Z (仰角・俯角)    : {pl_z:.2f} deg\n"
        )
        context.window_manager.clipboard = info_text
        self.report({'INFO'}, "情報全体をクリップボードにコピーしました")
        return {'FINISHED'}

# ------------------------------------------------------------------------
# Panels
# ------------------------------------------------------------------------
class BasePanel(Panel):
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME

class VIEW3D_PT_CopyPanel(BasePanel):
    bl_label = PANEL_LABELS["COPY"]; bl_idname = COPY_PANEL_IDNAME; bl_order = 0
    def draw(self, context):
        layout = self.layout
        row = layout.row()
        row.scale_y = 1.2
        row.operator(f"{PREFIX}_wm.copy_script", icon='COPY_ID', text="最新数値付きコードコピー")

        row_view = layout.row()
        row_view.operator(f"{PREFIX}_wm.view_center_front", icon='VIEWZOOM', text="0,0,0 を正面(Y-)から見る")

class VIEW3D_PT_PerspControlPanel(BasePanel):
    bl_label = PANEL_LABELS["PERSP"]; bl_idname = PERSP_PANEL_IDNAME; bl_order = 1
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, "persp_view_props", None)
        if not props: return

        box = layout.box()
        box.label(text="Perspective Viewpoint", icon='VIEW_CAMERA')
        box.prop(props, "slider_limit", text="Range Limit (+/-)")
        
        col = box.column(align=True)
        col.prop(props, "view_pos", text="X", index=0)
        col.prop(props, "view_pos", text="Y", index=1)
        col.prop(props, "view_pos", text="Z", index=2)
        
        box.separator()
        box.operator(f"{PREFIX}_persp.get_current_view", icon='RESTRICT_VIEW_OFF')
        box.operator(f"{PREFIX}_persp.reset_view", icon='LOOP_BACK')
        layout.operator(f"{PREFIX}_persp.center_selected", icon='VIEWZOOM')
        
        layout.separator()
        box_info = layout.box()
        box_info.label(text="Actual View Status", icon='INFO')
        
        r3d = context.space_data.region_3d if context.space_data else None
        if r3d:
            target_pos = Vector(r3d.view_location)
            actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
            vec = target_pos - actual_cam_pos
            length = vec.length
            
            col_pos = box_info.column(align=True)
            col_pos.label(text="[ Actual Position ]", icon='VIEW_CAMERA')
            col_pos.label(text=f"  X: {actual_cam_pos.x:.4f}")
            col_pos.label(text=f"  Y: {actual_cam_pos.y:.4f}")
            col_pos.label(text=f"  Z: {actual_cam_pos.z:.4f}")
            col_pos.label(text=f"  Distance: {length:.4f}") 
            box_info.operator(f"{PREFIX}_persp.copy_actual_pos", icon='COPYDOWN')
            box_info.separator()
            
            col_ang = box_info.column(align=True)
            if length > 0.0001:
                a_x = math.degrees(math.acos(vec.x / length))
                a_y = math.degrees(math.acos(vec.y / length))
                a_z = math.degrees(math.acos(vec.z / length))
                p_x = math.degrees(math.asin(vec.x / length))
                p_y = math.degrees(math.asin(vec.y / length))
                p_z = math.degrees(math.asin(vec.z / length))
                
                col_ang.label(text="[ Direction Angles (軸との角度) ]", icon='ORIENTATION_GLOBAL')
                col_ang.label(text=f"  X: {a_x:.2f}°")
                col_ang.label(text=f"  Y: {a_y:.2f}°")
                col_ang.label(text=f"  Z: {a_z:.2f}°")
                col_ang.separator()
                col_ang.label(text="[ Planar Angles (直感的な傾き) ]", icon='DRIVER_ROTATIONAL_DIFFERENCE')
                col_ang.label(text=f"  X (ズレ角): {p_x:.2f}°")
                col_ang.label(text=f"  Y (ズレ角): {p_y:.2f}°")
                col_ang.label(text=f"  Z (仰俯角): {p_z:.2f}°")
            else:
                col_ang.label(text="  Target is too close")

            box_info.separator()
            box_info.operator(f"{PREFIX}_persp.copy_angles", icon='COPYDOWN')
        else:
            box_info.label(text="Please use in 3D View")

class VIEW3D_PT_OverlayPanel(BasePanel):
    bl_label = PANEL_LABELS["OVERLAY"]; bl_idname = OVERLAY_PANEL_IDNAME; bl_order = 2
    def draw(self, context):
        layout, props = self.layout, context.scene.viewport_color_props
        if context.space_data.type == 'VIEW_3D':
            layout.prop(context.space_data.overlay, "show_floor", text="Floor")
            layout.prop(props, "custom_grid_scale", text="Scale 数値入力")
            row = layout.row(align=True)
            row.operator(f"{PREFIX}_overlay.set_grid_scale", text="入力値").scale_value = props.custom_grid_scale
            row.operator(f"{PREFIX}_overlay.set_grid_scale", text="10.0").scale_value = 10.0
            row.operator(f"{PREFIX}_overlay.set_grid_scale", text="100.0").scale_value = 100.0
        else:
            layout.label(text="3D Viewport is required.")

class VIEW3D_PT_solid_background_panel(BasePanel):
    bl_label = PANEL_LABELS["BACKGROUND"]; bl_idname = BG_PANEL_IDNAME_1; bl_order = 3
    def draw(self, context):
        layout, props = self.layout, context.scene.viewport_color_props
        layout.prop(props, "preset", text="Background Preset")
        layout.prop(props, "background_type", expand=True)
        layout.prop(props, "custom_gradient_high", text="Color High" if props.background_type != 'SINGLE_COLOR' else "Color")
        if props.background_type != 'SINGLE_COLOR': layout.prop(props, "custom_gradient_low")
        layout.prop(props, "reverse_gradient", text="Reverse Gradient")

class VIEW3D_PT_HeaderPanel(BasePanel):
    bl_label = PANEL_LABELS["HEADER"]; bl_idname = HEADER_PANEL_IDNAME; bl_order = 4
    def draw(self, context):
        layout, props = self.layout, context.scene.viewport_color_props
        layout.prop(props, "header_preset", text="Header Preset")
        layout.prop(props, "header_color")

class VIEW3D_PT_RenderPanel(BasePanel):
    bl_label = PANEL_LABELS["RENDER"]; bl_idname = RENDER_PANEL_IDNAME; bl_order = 5
    def draw(self, context):
        layout, props = self.layout, context.scene.viewport_color_props
        layout.prop(props, "render_preset")
        layout.prop(props, "render_color")
        layout.prop(props, "render_environment_strength")

class VIEW3D_PT_SunPanel(BasePanel):
    bl_label = PANEL_LABELS["SUN"]; bl_idname = SUN_PANEL_IDNAME; bl_order = 6
    def draw(self, context):
        layout, props = self.layout, context.scene.sun_settings_props
        row = layout.row(align=True)
        row.operator(f"{PREFIX}.create_sun", text="太陽作成ボタン")
        row.operator(f"{PREFIX}.reset_sun", icon='FILE_REFRESH', text="")
        layout.separator()
        layout.prop(props, "sun_control_mode", expand=True)
        if props.sun_control_mode == 'ANGLE': layout.prop(props, "sun_rotation")
        else: layout.prop(props, "sun_target_location")
        layout.prop(props, "sun_location"); layout.prop(props, "sun_strength")

class VIEW3D_PT_GridPanel(BasePanel):
    bl_label = PANEL_LABELS["GRID"]; bl_idname = GRID_PANEL_IDNAME; bl_order = 7
    def draw(self, context):
        layout, props = self.layout, context.scene.viewport_color_props
        layout.prop(props, "grid_preset")
        layout.prop(props, "grid_color")

class VIEW3D_PT_WirePanel(BasePanel):
    bl_label = PANEL_LABELS["WIRE"]; bl_idname = WIRE_PANEL_IDNAME; bl_order = 8
    def draw(self, context):
        layout, props = self.layout, context.scene.viewport_color_props
        layout.prop(props, "wire_preset")
        layout.prop(props, "wire_color")

class VIEW3D_PT_CameraPanel(BasePanel):
    bl_label = PANEL_LABELS["CAMERA"]; bl_idname = CAMERA_PANEL_IDNAME; bl_order = 9
    def draw(self, context):
        layout, props = self.layout, context.scene.viewport_color_props
        layout.prop(props, "camera_preset")
        layout.prop(props, "camera_color")

class VIEW3D_PT_OutlinerPanel(BasePanel):
    bl_label = PANEL_LABELS["OUTLINER"]; bl_idname = OUTLINER_PANEL_IDNAME; bl_order = 10
    def draw(self, context):
        layout, props = self.layout, context.scene.viewport_color_props
        layout.prop(props, "outliner_preset")
        layout.prop(props, "outliner_header_color")
        layout.prop(props, "outliner_background_color")

class VIEW3D_PT_TextEditorPanel(BasePanel):
    bl_label = PANEL_LABELS["TEXT_EDITOR"]; bl_idname = TEXT_EDITOR_PANEL_IDNAME; bl_order = 11
    def draw(self, context):
        layout, props = self.layout, context.scene.viewport_color_props
        layout.prop(props, "text_editor_preset")
        layout.prop(props, "text_editor_header_color")
        layout.prop(props, "text_editor_background_color")

class VIEW3D_PT_LinkPanel(BasePanel):
    bl_label = PANEL_LABELS["LINK"]; bl_idname = LINK_PANEL_IDNAME; bl_order = 12
    def draw(self, context):
        layout = self.layout
        layout.operator("wm.url_open", text="進化版 画面中央 透視投影視座位置 20260319bb", icon='URL').url = "<https://www.notion.so/20260319bb-327f5dacaf43801e8e37ce489dc1d593>"
        layout.operator("wm.url_open", text="5520 背景色 変更 20260316版", icon='URL').url = "<https://www.notion.so/5520-20260316-314f5dacaf4380da9be4c05551d40710>"

class VIEW3D_PT_RemovePanel(BasePanel):
    bl_label = PANEL_LABELS["REMOVE"]; bl_idname = REMOVE_PANEL_IDNAME; bl_order = 13
    def draw(self, context):
        self.layout.operator(f"{PREFIX}_wm.remove_all_panels", text=PANEL_LABELS["REMOVE"])

# ------------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------------
classes =[
    SunSettingsProperties, ViewportColorProperties, PerspViewProperties,
    OT_CopyFullScript, OT_ViewCenterFront,
    OVERLAY_OT_set_grid_scale,
    SUN_OT_Create, SUN_OT_Reset,
    PERSP_OT_GetCurrentView, PERSP_OT_ResetView, PERSP_OT_CenterSelected, PERSP_OT_CopyActualViewPos, PERSP_OT_CopyAngles,
    RemoveAllPanels, 
    VIEW3D_PT_CopyPanel, VIEW3D_PT_PerspControlPanel,
    VIEW3D_PT_OverlayPanel, VIEW3D_PT_solid_background_panel,
    VIEW3D_PT_HeaderPanel, VIEW3D_PT_RenderPanel, VIEW3D_PT_SunPanel, VIEW3D_PT_GridPanel,
    VIEW3D_PT_WirePanel, VIEW3D_PT_CameraPanel, VIEW3D_PT_OutlinerPanel, VIEW3D_PT_TextEditorPanel,
    VIEW3D_PT_LinkPanel, VIEW3D_PT_RemovePanel
]

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Scene.viewport_color_props = PointerProperty(type=ViewportColorProperties)
    bpy.types.Scene.sun_settings_props = PointerProperty(type=SunSettingsProperties)
    bpy.types.Scene.persp_view_props = PointerProperty(type=PerspViewProperties)
    
    def apply_initial_settings():
        if bpy.context.scene and hasattr(bpy.context.scene, 'viewport_color_props'):
            props = bpy.context.scene.viewport_color_props
            sun_props = bpy.context.scene.sun_settings_props
            persp_props = bpy.context.scene.persp_view_props
            
            for key, val in CURRENT_DEFAULTS.items():
                if hasattr(props, key): setattr(props, key, val)
                elif hasattr(sun_props, key): setattr(sun_props, key, val)
                elif hasattr(persp_props, key): setattr(persp_props, key, val)
            
            update_background_color(props, bpy.context)
            update_header_color(props, bpy.context)
            update_render_color(props, bpy.context)
            update_grid_color(props, bpy.context)
            update_wire_color(props, bpy.context)
            update_camera_color(props, bpy.context)
            update_outliner_color(props, bpy.context)
            update_text_editor_color(props, bpy.context)
            update_sun(sun_props, bpy.context)
    
    bpy.app.timers.register(apply_initial_settings, first_interval=0.1)

    if not bpy.app.timers.is_registered(view_sync_timer):
        bpy.app.timers.register(view_sync_timer)

def unregister():
    if hasattr(bpy.types.Scene, 'viewport_color_props'): del bpy.types.Scene.viewport_color_props
    if hasattr(bpy.types.Scene, 'sun_settings_props'): del bpy.types.Scene.sun_settings_props
    if hasattr(bpy.types.Scene, 'persp_view_props'): del bpy.types.Scene.persp_view_props
    
    for cls in reversed(classes):
        try: bpy.utils.unregister_class(cls)
        except RuntimeError: pass

    if bpy.app.timers.is_registered(view_sync_timer):
        bpy.app.timers.unregister(view_sync_timer)

if __name__ == "__main__":
    register()

# ▲▲▲ ここまで ▲▲▲
'''

# ③ Fixed Camera & World (cam_kotei...) のコードを以下に貼り付け
CAMERA_SCRIPT_CONTENT = r'''
# ▼▼▼ ここに「v100 Fixed Camera & World」の全コードを貼り付けてください ▼▼▼

import bpy
import math
import mathutils
import webbrowser
import os
from bpy.types import Operator, Panel, Scene, PropertyGroup
from bpy.props import StringProperty, PointerProperty, EnumProperty, FloatVectorProperty, FloatProperty, CollectionProperty, BoolProperty, IntProperty
from datetime import datetime

# --- ユニークID生成 ---
START_TIMESTAMP = datetime.now().strftime("%Y%m%d%H%M%S")
PREFIX = f"cam_kotei{START_TIMESTAMP}"

# --- bl_info ---
bl_info = {
    "name": "zionad v100 [Fixed Camera & World]",
    "author": "zionadchat",
    "version": (35, 0, 5), # バージョンアップ
    "blender": (4, 1, 0),
    "location": "View3D > Sidebar > zionad Control",
    "description": "カメラの位置固定、向き(YPR)、レンズ制御に加え、ワールド(HDRI/背景)設定機能を提供します。",
    "category": "   v100[ 固定 Camera ]   ",
}

# ======================================================================
# --- ユーザー設定 / Parameters to Customize ---
# ======================================================================

ADDON_CATEGORY_NAME = bl_info["category"]

# --- HDRI画像ファイルのフルパスリスト ---
# ▼▼▼【変更点】ご指定のHDRIパスをリストの2番目に追加しました ▼▼▼
HDRI_PATHS = [
    r"C:\a111\HDRi_pic\qwantani_afternoon_puresky_4k.exr",
    r"C:\a111\HDRi_pic\rogland_moonlit_night_4k.hdr",
    r"C:\a111\HDRi_pic\rogland_clear_night_4k.hdr",
    r"C:\a111\HDRi_pic\golden_bay_4k.hdr",
]

# --- ワイヤーフレームの色プリセット ---
# 形式: ("ID", "ラベル", "説明", (R, G, B))
WIRE_PRESETS = [
    ("CUSTOM_GREENISH", "Custom Greenish", "Custom greenish wire color", (0.51, 1.0, 0.75)),
    ("WHITE", "White", "White wire", (1.0, 1.0, 1.0)),
    ("RED", "Red", "Red wire", (1.0, 0.0, 0.0)),
    ("GREEN", "Green", "Green wire", (0.0, 1.0, 0.0)),
]

# --- グリッドの色プリセット ---
# 形式: ("ID", "ラベル", "説明", (R, G, B, A))
GRID_PRESETS = [
    ("CUSTOM_REDDISH", "Custom Reddish", "Custom reddish color", (0.545, 0.322, 0.322, 1.0)),
    ("DEEP_GREEN", "Deep Green", "A deep green color", (0.098, 0.314, 0.271, 1.0)),
    ("MINT_GREEN", "Mint Green", "A mint green color", (0.165, 0.557, 0.475, 1.0)),
]

# --- 専用カメラのコレクション名とオブジェクト名 ---
CAMERA_COLLECTION_NAME = "Cam"
DEDICATED_CAMERA_NAME = "Fixed_Cam"

# ======================================================================
# --- 定数定義 / Constants ---
# ======================================================================

SENSOR_WIDTH = 36.0
FOV_PRESETS = [1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]
CAMERA_COLOR_PRESETS = [("CYAN", "Cyan", "水色", (0.0, 1.0, 1.0)), ("Cam 4.4.0", "Cam 4.4.0", "Blenderデフォルト色", (0.0, 0.0, 0.0)), ("YELLOW", "Yellow", "黄色", (1.0, 1.0, 0.0)), ("PURPLE", "Purple", "紫色", (0.5, 0.0, 0.5)),]

# --- リンクパネル用データ ---
ADDON_LINKS = ({"label": "カメラ 固定 Git 管理 20250711", "url":"<https://memo2017.hatenablog.com/entry/2025/07/11/131157>"},)

NEW_DOC_LINKS = [
    {"label": "完成品 目次", "url": "<https://mokuji000zionad.hatenablog.com/entry/2025/05/30/135936>"},
]

DOC_LINKS = [
    {"label": "812 地球儀 経度 緯度でのコントロール 20250302", "url": "<https://sortphotos2025.hatenablog.jp/entry/2025/03/02/211757>"},
    {"label": "アドオン目次 from 20250227", "url": "<https://sortphotos2025.hatenablog.jp/entry/2025/02/27/201251>"},
    {"label": "addon 目次整理 from 20250116", "url": "<https://blenderzionad.hatenablog.com/entry/2025/01/17/002322>"},
]

SOCIAL_LINKS = [
    {"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
    {"label": "Posfie zionad2022", "url": "<https://posfie.com/t/zionad2022>"},
    {"label": "X (Twitter) zionadchat", "url": "<https://x.com/zionadchat>"},
    {"label": "単純トリック 2025 open", "url": "<https://www.notion.so/2025-open-221b3deba7a2809a85a9f5ab5600ab06>"},
]

# --- パネルIDと順序 ---
PANEL_IDS = {
    "SETUP": f"{PREFIX}_PT_setup", "POSITION": f"{PREFIX}_PT_position", "AIMING": f"{PREFIX}_PT_aiming",
    "LENS": f"{PREFIX}_PT_lens", "CAMERA_DISPLAY": f"{PREFIX}_PT_camera_display", "WORLD_CONTROL": f"{PREFIX}_PT_world_control",
    "INFO": f"{PREFIX}_PT_info", "GRID": f"{PREFIX}_PT_grid_panel", "WIRE": f"{PREFIX}_PT_wire_panel",
    "LINKS": f"{PREFIX}_PT_links",
    "LINKS_NEWDOC": f"{PREFIX}_PT_links_newdoc", "LINKS_DOC": f"{PREFIX}_PT_links_doc", "LINKS_SOCIAL": f"{PREFIX}_PT_links_social",
    "REMOVE": f"{PREFIX}_PT_remove",
}
PANEL_ORDER = {PANEL_IDS["SETUP"]: 0, PANEL_IDS["POSITION"]: 1, PANEL_IDS["AIMING"]: 2, PANEL_IDS["LENS"]: 3, PANEL_IDS["CAMERA_DISPLAY"]: 4, PANEL_IDS["WORLD_CONTROL"]: 5, PANEL_IDS["INFO"]: 6, PANEL_IDS["GRID"]: 89, PANEL_IDS["WIRE"]: 90, PANEL_IDS["LINKS"]: 100, PANEL_IDS["REMOVE"]: 200,}

# --- グローバル状態管理 ---
_is_updating_by_addon = False; _update_timer = None
def reset_update_flag(): global _is_updating_by_addon, _update_timer; _is_updating_by_addon = False; _update_timer = None; return None
def schedule_update_flag_reset():
    global _update_timer
    if _update_timer and bpy.app.timers.is_registered(reset_update_flag): bpy.app.timers.unregister(reset_update_flag)
    bpy.app.timers.register(reset_update_flag, first_interval=0.01)

# --- World Tools ヘルパー関数 ---
def find_node(nodes, node_type, name):
    if node_type == 'OUTPUT_WORLD': return next((n for n in nodes if n.type == 'OUTPUT_WORLD'), None)
    return nodes.get(name)
def find_or_create_node(nodes, node_type, name, location_offset=(0, 0)):
    node = find_node(nodes, node_type, name)
    if node: return node
    new_node = nodes.new(type=node_type); new_node.name = name; new_node.label = name.replace("_", " ")
    output_node = find_node(nodes, 'OUTPUT_WORLD', '');
    if output_node: new_node.location = output_node.location + mathutils.Vector(location_offset)
    return new_node
def get_world_nodes(context, create=True):
    world = context.scene.world
    if not world and create: world = bpy.data.worlds.new("World"); context.scene.world = world
    if not world: return None, None, None
    if create: world.use_nodes = True
    if not world.use_nodes: return world, None, None
    return world, world.node_tree.nodes, world.node_tree.links
def load_hdri_from_path(filepath, context):
    _, nodes, _ = get_world_nodes(context)
    if not nodes: return False
    env_node = find_or_create_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
    if os.path.exists(filepath):
        try: env_node.image = bpy.data.images.load(filepath, check_existing=True); return True
        except RuntimeError as e: print(f"Error loading image: {e}"); return False
    print(f"File not found: {filepath}"); return False
def update_viewport(context):
    for area in context.screen.areas:
        if area.type == 'VIEW_3D':
            for space in area.spaces:
                if space.type == 'VIEW_3D': space.shading.type = 'RENDERED'; return
def update_background_mode(self, context):
    mode = context.scene.zionad_swt_props.background_mode; world, nodes, links = get_world_nodes(context)
    if not nodes: return
    output_node = find_or_create_node(nodes, 'OUTPUT_WORLD', 'World_Output')
    background_node = find_or_create_node(nodes, 'ShaderNodeBackground', 'Background', (-250, 0))
    sky_node = find_or_create_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture', (-550, 0))
    env_node = find_or_create_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture', (-550, 0))
    mapping_node = find_or_create_node(nodes, 'ShaderNodeMapping', 'Mapping', (-800, 0))
    tex_coord_node = find_or_create_node(nodes, 'ShaderNodeTexCoord', 'Texture_Coordinate', (-1050, 0))
    if background_node.inputs['Color'].is_linked: links.remove(background_node.inputs['Color'].links[0])
    if output_node.inputs['Surface'].is_linked: links.remove(output_node.inputs['Surface'].links[0])
    links.new(background_node.outputs['Background'], output_node.inputs['Surface'])
    if mode == 'SKY': links.new(sky_node.outputs['Color'], background_node.inputs['Color'])
    elif mode == 'HDRI':
        if not mapping_node.inputs['Vector'].is_linked: links.new(tex_coord_node.outputs['Generated'], mapping_node.inputs['Vector'])
        if not env_node.inputs['Vector'].is_linked: links.new(mapping_node.outputs['Vector'], env_node.inputs['Vector'])
        links.new(env_node.outputs['Color'], background_node.inputs['Color'])
        props = context.scene.zionad_swt_props
        if 0 <= props.hdri_list_index < len(HDRI_PATHS): load_hdri_from_path(HDRI_PATHS[props.hdri_list_index], context)
    update_viewport(context)

# --- プロパティグループ ---
class ThemeGridProperties(PropertyGroup):
    grid_color: FloatVectorProperty(name="Grid Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.545, 0.322, 0.322, 1.0))
    grid_preset: EnumProperty(name="Grid Preset", items=[(p[0], p[1], p[2]) for p in GRID_PRESETS], update=lambda self, context: SFC_OT_GridApplyColor.update_preset(self, context))
class ThemeWireProperties(PropertyGroup):
    wire_color: FloatVectorProperty(name="Wire Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=(0.51, 1.0, 0.75))
    wire_preset: EnumProperty(name="Wire Preset", items=[(p[0], p[1], p[2]) for p in WIRE_PRESETS], update=lambda self, context: SFC_OT_WireApplyColor.update_preset(self, context))
class TargetProperty(PropertyGroup): name: StringProperty()
class SurfaceCameraProperties(PropertyGroup):
    camera_obj: PointerProperty(name="操作カメラ", type=bpy.types.Object, poll=lambda self, obj: obj.type == 'CAMERA', update=lambda s,c: update_surface_camera(s,c))
    fixed_location: FloatVectorProperty(name="固定位置", default=(0.0, -10.0, 0.0), subtype='XYZ', update=lambda s,c: update_surface_camera(s,c))
    target_location: FloatVectorProperty(name="固定注視点", default=(0, 0, 0), subtype='XYZ', update=lambda s,c: update_surface_camera(s,c))
    offset_yaw: FloatProperty(name="Yaw", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c)); offset_pitch: FloatProperty(name="Pitch", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c)); offset_roll: FloatProperty(name="Roll", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
    is_updating_settings: BoolProperty(default=False, options={'HIDDEN'})
    lens_focal_length: FloatProperty(name="焦点距離 (mm)", default=50.0, min=1.0, max=1000.0, unit='LENGTH', update=lambda s,c: update_surface_camera(s,c))
    clip_start: FloatProperty(name="クリップ開始", default=0.1, min=0.001, update=lambda s,c: update_surface_camera(s,c)); clip_end: FloatProperty(name="クリップ終了", default=1000.0, min=1.0, update=lambda s,c: update_surface_camera(s,c))
    info_precision: EnumProperty(name="桁数", items=[('1', '1', ''), ('2', '2', ''), ('3', '3', '')], default='1', update=lambda s,c: update_info_panel_text(s,c))
    info_focal_length: StringProperty(name="焦点距離"); info_horizontal_fov: StringProperty(name="水平視野角"); info_camera_location: StringProperty(name="カメラ位置"); info_target_location: StringProperty(name="注視点位置"); info_distance_to_target: StringProperty(name="注視点までの距離"); info_clip_setting: StringProperty(name="クリップ範囲"); info_viewable_width: StringProperty(name="注視点での横幅")
    camera_color: FloatVectorProperty(name="カメラカラー", subtype='COLOR', size=3, min=0.0, max=1.0, default=(0.0, 1.0, 1.0))
    camera_preset: EnumProperty(name="カメラプリセット", items=[(p[0], p[1], p[2]) for p in CAMERA_COLOR_PRESETS], default="CYAN", update=lambda self, context: SFC_OT_ApplyCameraColor.update_preset(self, context))
class ZIONAD_SWT_Properties(PropertyGroup):
    background_mode: EnumProperty(name="Background Mode", items=[('HDRI', "HDRI", ""), ('SKY', "Sky", "")], default='HDRI', update=update_background_mode)
    hdri_list_index: IntProperty(name="Active HDRI Index", default=0, update=update_background_mode)

# --- カメラ コアロジック ---
def calculate_horizontal_fov(focal_length, sensor_width=SENSOR_WIDTH):
    try: return 2 * math.atan(sensor_width / (2 * focal_length)) * (180 / math.pi)
    except (ZeroDivisionError, ValueError): return 0.0
def calculate_focal_length(fov_degrees, sensor_width=SENSOR_WIDTH):
    try: return sensor_width / (2 * math.tan(math.radians(fov_degrees) / 2))
    except (ZeroDivisionError, ValueError): return 50.0
def get_target_location(props):
    return mathutils.Vector(props.target_location)
def update_object_transform(obj, props):
    location = mathutils.Vector(props.fixed_location)
    target_location = get_target_location(props); direction = target_location - location
    if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
    base_track_quat = direction.to_track_quat('-Z', 'Y')
    offset_euler = mathutils.Euler((props.offset_pitch, props.offset_yaw, props.offset_roll), 'XYZ')
    final_quat = base_track_quat @ offset_euler.to_quaternion()
    obj.location = location; obj.rotation_euler = final_quat.to_euler('XYZ')
def update_surface_camera(self, context):
    global _is_updating_by_addon
    if _is_updating_by_addon: return
    _is_updating_by_addon = True
    try:
        props, camera_obj = context.scene.surface_camera_properties, context.scene.surface_camera_properties.camera_obj
        if props.is_updating_settings or not camera_obj: update_info_panel_text(props, context); return
        cam_data = camera_obj.data
        if cam_data: cam_data.sensor_fit, cam_data.lens_unit, cam_data.lens, cam_data.clip_start, cam_data.clip_end = 'HORIZONTAL', 'MILLIMETERS', props.lens_focal_length, props.clip_start, props.clip_end
        update_object_transform(camera_obj, props); update_info_panel_text(props, context)
    finally: schedule_update_flag_reset()
def update_info_panel_text(props, context):
    if not hasattr(context, 'scene') or not props: return
    precision, fmt = int(props.info_precision), f".{props.info_precision}f"
    camera_location, target_location = mathutils.Vector(props.fixed_location), get_target_location(props)
    props.info_camera_location = f"({camera_location.x:{fmt}}, {camera_location.y:{fmt}}, {camera_location.z:{fmt}})"; current_fov = calculate_horizontal_fov(props.lens_focal_length); props.info_horizontal_fov = f"{current_fov:{fmt}} °"; props.info_focal_length = f"{props.lens_focal_length:{fmt}} mm"
    props.info_target_location = f"({target_location.x:{fmt}}, {target_location.y:{fmt}}, {target_location.z:{fmt}})"; distance = (target_location - camera_location).length; props.info_distance_to_target = f"{distance:{fmt}}"
    if distance > 0 and current_fov > 0: props.info_viewable_width = f"{2 * distance * math.tan(math.radians(current_fov) / 2):{fmt}}"
    else: props.info_viewable_width = "N/A"
    props.info_clip_setting = f"{props.clip_start:{fmt}} - {props.clip_end:{fmt}}"
def sync_ui_from_manual_transform(props, obj, context):
    global _is_updating_by_addon
    if _is_updating_by_addon: return
    _is_updating_by_addon = True
    try:
        props.fixed_location = obj.location
        target_location = get_target_location(props); direction = target_location - obj.location
        if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
        base_track_quat, final_quat = direction.to_track_quat('-Z', 'Y'), obj.matrix_world.to_quaternion()
        offset_quat, offset_euler = base_track_quat.inverted() @ final_quat, offset_quat.to_euler('XYZ')
        props.offset_pitch, props.offset_yaw, props.offset_roll = offset_euler.x, offset_euler.y, offset_euler.z
    finally: _is_updating_by_addon = False
    update_info_panel_text(props, context)
@bpy.app.handlers.persistent
def on_depsgraph_update(scene, depsgraph):
    if _is_updating_by_addon: return
    context = bpy.context
    if not (hasattr(context, 'scene') and context.scene): return
    sfc_props = context.scene.surface_camera_properties
    for update in depsgraph.updates:
        if not update.is_updated_transform: continue
        obj_id = update.id.original
        if sfc_props.camera_obj and obj_id == sfc_props.camera_obj: sync_ui_from_manual_transform(sfc_props, sfc_props.camera_obj, context); return

# --- オペレーター ---
class SFC_OT_ApplyCameraColor(Operator):
    bl_idname = f"{PREFIX}.apply_camera_color"; bl_label = "カメラカラー適用"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context): context.preferences.themes[0].view_3d.camera = context.scene.surface_camera_properties.camera_color; return {'FINISHED'}
    @staticmethod
    def update_preset(self, context): props = context.scene.surface_camera_properties; props.camera_color = next((p[3] for p in CAMERA_COLOR_PRESETS if p[0] == props.camera_preset), props.camera_color); getattr(bpy.ops, f"{PREFIX}.apply_camera_color")()
class SFC_OT_GridApplyColor(Operator):
    bl_idname = f"{PREFIX}.apply_grid_color"; bl_label = "Apply Grid Color"
    def execute(self, context): props = context.scene.theme_grid_properties; theme = bpy.context.preferences.themes[0]; theme.view_3d.grid = props.grid_color; return {'FINISHED'}
    @staticmethod
    def update_preset(self, context):
        props = context.scene.theme_grid_properties
        props.grid_color = next((p[3] for p in GRID_PRESETS if p[0] == props.grid_preset), props.grid_color)
        getattr(bpy.ops, f"{PREFIX}.apply_grid_color")()
class SFC_OT_GridCopyColor(Operator):
    bl_idname = f"{PREFIX}.copy_grid_color"; bl_label = "Copy Grid Color"
    def execute(self, context): theme = bpy.context.preferences.themes[0]; color_tuple = tuple(round(c, 3) for c in theme.view_3d.grid); context.window_manager.clipboard = f'("CUSTOM", "Custom", "Custom grid color", {color_tuple}),'; self.report({'INFO'}, f"グリッドの色をコピーしました: {context.window_manager.clipboard}"); return {'FINISHED'}
class SFC_OT_CreateDedicatedCamera(Operator):
    bl_idname = f"{PREFIX}.create_dedicated_camera"; bl_label = "専用カメラ作成"
    def execute(self, context):
        if DEDICATED_CAMERA_NAME not in bpy.data.objects:
            cam_data = bpy.data.cameras.new(name=DEDICATED_CAMERA_NAME); cam_obj = bpy.data.objects.new(DEDICATED_CAMERA_NAME, cam_data)
            cam_collection = bpy.data.collections.get(CAMERA_COLLECTION_NAME) or bpy.data.collections.new(CAMERA_COLLECTION_NAME)
            if CAMERA_COLLECTION_NAME not in context.scene.collection.children: context.scene.collection.children.link(cam_collection)
            cam_collection.objects.link(cam_obj)
            if cam_obj.name in context.scene.collection.objects: context.scene.collection.objects.unlink(cam_obj)
        else: cam_obj = bpy.data.objects[DEDICATED_CAMERA_NAME]
        props = context.scene.surface_camera_properties; props.camera_obj = cam_obj; props.is_updating_settings = True
        for key in props.bl_rna.properties.keys():
            if key not in ['camera_obj', 'bl_rna', 'is_updating_settings'] and not props.bl_rna.properties[key].is_readonly: props.property_unset(key)
        props.is_updating_settings = False; update_surface_camera(props, context); self.report({'INFO'}, f"カメラ '{DEDICATED_CAMERA_NAME}' を作成/選択し、初期化しました。"); return {'FINISHED'}
class SFC_OT_SyncWithCamera(Operator):
    bl_idname = f"{PREFIX}.sync_with_camera"; bl_label = "UIを同期"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props, cam_obj = context.scene.surface_camera_properties, context.scene.surface_camera_properties.camera_obj
        if not cam_obj or cam_obj.type != 'CAMERA': self.report({'WARNING'}, "有効なカメラが選択されていません。"); return {'CANCELLED'}
        context.scene.camera = cam_obj; cam_data = cam_obj.data; props.is_updating_settings = True
        props.lens_focal_length, props.clip_start, props.clip_end = cam_data.lens, cam_data.clip_start, cam_data.clip_end
        props.is_updating_settings = False; sync_ui_from_manual_transform(props, cam_obj, context); self.report({'INFO'}, f"カメラ '{cam_obj.name}' の設定をUIに読み込みました。"); return {'FINISHED'}
class SFC_OT_UnlinkObject(Operator):
    bl_idname = f"{PREFIX}.unlink_object"; bl_label = "解除"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props, update_func, obj_prop = context.scene.surface_camera_properties, update_surface_camera, 'camera_obj'
        if getattr(props, obj_prop): self.report({'INFO'}, f"'{getattr(props, obj_prop).name}' との関連付けを解除しました。"); setattr(props, obj_prop, None)
        props.is_updating_settings = True
        for key in props.bl_rna.properties.keys():
            if key not in ['bl_rna', 'is_updating_settings', 'camera_obj'] and not props.bl_rna.properties[key].is_readonly: props.property_unset(key)
        props.is_updating_settings = False; update_func(props, context); return {'FINISHED'}
class SFC_OT_ResetProperty(Operator):
    bl_idname = f"{PREFIX}.reset_property"; bl_label = "プロパティリセット"; targets: CollectionProperty(type=TargetProperty); prop_group_name: StringProperty()
    def execute(self, context):
        props, update_func = context.scene.surface_camera_properties, update_surface_camera
        prop_groups = {"location": ["fixed_location"],"ypr": ["offset_yaw", "offset_pitch", "offset_roll"],"aim": ["target_location"],"clip": ["clip_start", "clip_end", "lens_focal_length"],}
        target_names, props_to_reset = {t.name for t in self.targets}, set()
        if "all" in target_names:
            for group_props in prop_groups.values(): props_to_reset.update(group_props)
        else:
            for name in target_names: props_to_reset.update(prop_groups.get(name, []))
        props.is_updating_settings = True
        for prop_name in props_to_reset:
            if hasattr(props, prop_name): props.property_unset(prop_name)
        props.is_updating_settings = False; update_func(props, context); return {'FINISHED'}
class SFC_OT_SetFOV(Operator):
    bl_idname = f"{PREFIX}.set_fov"; bl_label = "FOV設定"; fov: FloatProperty(default=0.0)
    def execute(self, context): props = context.scene.surface_camera_properties; props.lens_focal_length = calculate_focal_length(self.fov); return {'FINISHED'}
class SFC_OT_CopyAllInfo(Operator):
    bl_idname = f"{PREFIX}.copy_all_info"; bl_label = "全情報コピー"
    def execute(self, context):
        props=context.scene.surface_camera_properties; context.window_manager.clipboard = (f"カメラ情報 ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})\n----------------------------------------\n" f"焦点距離: {props.info_focal_length}\n水平視野角: {props.info_horizontal_fov}\nカメラ位置: {props.info_camera_location}\n" f"注視点: {props.info_target_location}\n注視点までの距離: {props.info_distance_to_target}\n注視点での横幅: {props.info_viewable_width}\n" f"クリップ範囲: {props.info_clip_setting}\n----------------------------------------"); self.report({'INFO'}, "全情報をクリップボードにコピーしました。"); return {'FINISHED'}
class SFC_OT_OpenURL(Operator):
    bl_idname = f"{PREFIX}.open_url"; bl_label = "URLを開く"; url: StringProperty(default="")
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class SFC_OT_RemoveAddon(Operator):
    bl_idname = f"{PREFIX}.remove_addon"; bl_label = "アドオン解除"
    def execute(self, context): module_name = __name__.split('.')[0]; bpy.ops.preferences.addon_disable(module=module_name); self.report({'INFO'}, f"アドオン '{bl_info.get('name')}' を無効化・解除しました。"); unregister(); return {'FINISHED'}
class SFC_OT_WireApplyColor(Operator):
    bl_idname = f"{PREFIX}.apply_wire_color"; bl_label = "Apply Wire Color"
    def execute(self, context): props=context.scene.theme_wire_properties; theme=bpy.context.preferences.themes[0]; theme.view_3d.wire=props.wire_color; theme.view_3d.object_active=props.wire_color; return {'FINISHED'}
    @staticmethod
    def update_preset(self, context):
        props = context.scene.theme_wire_properties
        props.wire_color = next((p[3] for p in WIRE_PRESETS if p[0] == props.wire_preset), props.wire_color)
        getattr(bpy.ops, f"{PREFIX}.apply_wire_color")()
class SFC_OT_WireCopyColor(Operator):
    bl_idname = f"{PREFIX}.copy_wire_color"; bl_label = "Copy Wire Color"
    def execute(self, context): theme=bpy.context.preferences.themes[0]; color_tuple=tuple(round(c, 2) for c in theme.view_3d.wire); context.window_manager.clipboard=f'("CUSTOM", "Custom", "Custom wire color", {color_tuple}),'; self.report({'INFO'}, f"ワイアの色をコピーしました: {context.window_manager.clipboard}"); return {'FINISHED'}
class SFC_OT_SetFixedLocationFromView(Operator):
    bl_idname = f"{PREFIX}.set_fixed_location_from_view"; bl_label = "現在のカメラ位置をセット"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props, cam_obj = context.scene.surface_camera_properties, context.scene.surface_camera_properties.camera_obj
        if not cam_obj: self.report({'WARNING'}, "操作対象のカメラが選択されていません。"); return {'CANCELLED'}
        props.fixed_location = cam_obj.location; self.report({'INFO'}, f"固定位置を {tuple(round(c, 2) for c in cam_obj.location)} に設定しました。"); return {'FINISHED'}
class ZIONAD_SWT_OT_LoadHdriFromList(Operator):
    bl_idname = f"{PREFIX}.load_hdri_from_list"; bl_label = "Load HDRI from List"; bl_options = {'REGISTER', 'UNDO'}; hdri_index: IntProperty()
    def execute(self, context):
        props = context.scene.zionad_swt_props
        if 0 <= self.hdri_index < len(HDRI_PATHS):
            props.hdri_list_index = self.hdri_index; props.background_mode = 'HDRI'; load_hdri_from_path(HDRI_PATHS[self.hdri_index], context); update_background_mode(props, context)
            self.report({'INFO'}, f"Loaded: {os.path.basename(HDRI_PATHS[self.hdri_index])}")
        else: self.report({'ERROR'}, "Invalid HDRI index")
        return {'FINISHED'}
class ZIONAD_SWT_OT_ResetTransform(Operator):
    bl_idname = f"{PREFIX}.reset_transform"; bl_label = "Reset Transform Value"; bl_options = {'REGISTER', 'UNDO'}; property_to_reset: StringProperty()
    def execute(self, context):
        _, nodes, _ = get_world_nodes(context)
        if not nodes: return {'CANCELLED'}
        mapping_node = find_node(nodes, 'ShaderNodeMapping', 'Mapping')
        if not mapping_node: return {'CANCELLED'}
        if self.property_to_reset == 'Location': mapping_node.inputs['Location'].default_value = (0, 0, 0)
        elif self.property_to_reset == 'Rotation': mapping_node.inputs['Rotation'].default_value = (0, 0, 0)
        elif self.property_to_reset == 'Scale': mapping_node.inputs['Scale'].default_value = (1, 1, 1)
        return {'FINISHED'}

# --- UIパネル ---
class SFC_PT_CameraSetupPanel(Panel):
    bl_label = "1. カメラ設定"; bl_idname = PANEL_IDS["SETUP"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["SETUP"]]
    def draw(self, context):
        layout = self.layout; props = context.scene.surface_camera_properties; box = layout.box(); col = box.column(); col.prop(props, "camera_obj", text="カメラ")
        if props.camera_obj: row = col.row(align=True); row.operator(f"{PREFIX}.sync_with_camera", icon='UV_SYNC_SELECT'); row.operator(f"{PREFIX}.unlink_object", icon='X')
        else: col.label(text="カメラを選択してください", icon='ERROR'); col.operator(f"{PREFIX}.create_dedicated_camera", text=f"'{DEDICATED_CAMERA_NAME}' を作成/選択", icon='ADD')
        col.separator(); box.prop(props, "camera_preset", text="色プリセット"); box.prop(props, "camera_color", text="カラー"); box.operator(f"{PREFIX}.apply_camera_color", text="ビューポート色を適用")
class SFC_PT_PositionPanel(Panel):
    bl_label = "2. カメラ位置 (固定)"; bl_idname = PANEL_IDS["POSITION"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["POSITION"]]
    def draw(self, context):
        layout = self.layout; props = context.scene.surface_camera_properties; box = layout.box(); col = box.column(align=True); row = col.row(align=True)
        row.label(text="固定位置"); op = row.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK'); op.targets.add().name = "location"; op.prop_group_name = "camera"
        col.prop(props, "fixed_location", text=""); col.operator(f"{PREFIX}.set_fixed_location_from_view", icon='OBJECT_ORIGIN')
class SFC_PT_AimingPanel(Panel):
    bl_label = "3. カメラ視線制御"; bl_idname = PANEL_IDS["AIMING"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["AIMING"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout, props = self.layout, context.scene.surface_camera_properties
        box_aim = layout.box(); col_aim = box_aim.column(align=True)
        row_aim = col_aim.row(align=True); row_aim.label(text="注視点")
        op_aim = row_aim.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK'); op_aim.targets.add().name = "aim"; op_aim.prop_group_name = "camera"
        col_aim.prop(props, "target_location", text="")
        box_offset = layout.box(); col_offset = box_offset.column(align=True); row_offset = col_offset.row(align=True); row_offset.label(text="視線オフセット (YPR)")
        op_offset = row_offset.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK'); op_offset.targets.add().name = "ypr"; op_offset.prop_group_name = "camera"
        col_offset.prop(props, "offset_yaw"); col_offset.prop(props, "offset_pitch"); col_offset.prop(props, "offset_roll")
class SFC_PT_LensPanel(Panel):
    bl_label = "4. レンズ設定"; bl_idname = PANEL_IDS["LENS"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["LENS"]]
    def draw(self, context):
        layout, props = self.layout, context.scene.surface_camera_properties; box = layout.box(); col = box.column(align=True); row = col.row(align=True)
        row.label(text="レンズとクリップ"); op = row.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK'); op.targets.add().name = "clip"; op.prop_group_name = "camera"
        col.prop(props, "lens_focal_length"); row = col.row(align=True); row.label(text="水平視野角:"); row.label(text=props.info_horizontal_fov); col.label(text="FOVプリセット:")
        row = col.row(align=True); col1, col2 = row.column(align=True), row.column(align=True)
        for i, fov in enumerate(FOV_PRESETS): op = (col1 if i % 2 == 0 else col2).operator(f"{PREFIX}.set_fov", text=f"{fov}°"); op.fov = fov
        col.separator(); row = col.row(align=True); row.prop(props, "clip_start"); row.prop(props, "clip_end")
class SFC_PT_CameraDisplayPanel(Panel):
    bl_label = "Camera Display & Render"; bl_idname = PANEL_IDS["CAMERA_DISPLAY"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["CAMERA_DISPLAY"]]
    def draw(self, context):
        layout, scene, cam = self.layout, context.scene, context.scene.camera
        
        box_render = layout.box()
        box_render.label(text="Render Engine", icon='SCENE')
        box_render.prop(scene.render, "engine", expand=True)
        layout.separator()

        if not cam or not isinstance(cam.data, bpy.types.Camera):
            layout.box().label(text="シーンにアクティブなカメラがありません", icon='ERROR')
            return

        cam_data = cam.data
        overlay = context.space_data.overlay if context.space_data and hasattr(context.space_data, 'overlay') else None
        
        layout.label(text="Active Camera: " + cam.name, icon='CAMERA_DATA')
        
        box_passepartout = layout.box()
        box_passepartout.label(text="Passepartout", icon='MOD_MASK')
        col_passepartout = box_passepartout.column(align=True)
        col_passepartout.prop(cam_data, "show_passepartout", text="Enable")
        row_passepartout = col_passepartout.row()
        row_passepartout.enabled = cam_data.show_passepartout
        row_passepartout.prop(cam_data, "passepartout_alpha", text="Opacity")
        layout.separator()

        box_display = layout.box()
        box_display.label(text="Viewport Display", icon='OVERLAY')
        
        if not overlay:
            box_display.label(text="3D Viewport only", icon='INFO')
            return
            
        box_display.prop(overlay, "show_overlays", text="Viewport Overlays")
        
        col_overlay_options = box_display.column(); col_overlay_options.enabled = overlay.show_overlays
        col_overlay_options.prop(overlay, "show_extras", text="Extras")
        
        col_details = col_overlay_options.column(); col_details.enabled = overlay.show_extras
        col_details.prop(overlay, "show_text", text="Text Info")
        col_details.prop(cam_data, "show_name", text="Name")
        col_details.prop(cam_data, "show_limits", text="Limits")
class ZIONAD_SWT_PT_WorldControlPanel(Panel):
    bl_label = "World Control"; bl_idname = PANEL_IDS["WORLD_CONTROL"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["WORLD_CONTROL"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout, scene, props = self.layout, context.scene, context.scene.zionad_swt_props; world, nodes, _ = get_world_nodes(context, create=False)
        if not world or not world.use_nodes or not nodes:
            col = layout.column(align=True)
            if not world: col.label(text="No World in Scene", icon='ERROR'); col.operator("world.new", text="Create New World")
            else: col.label(text="Enable Nodes in World"); col.prop(world, "use_nodes", text="Use Nodes")
            return
        box_mode = layout.box(); box_mode.label(text="Background Mode", icon='WORLD'); box_mode.prop(props, "background_mode", expand=True); layout.separator()
        if props.background_mode == 'HDRI':
            box_env = layout.box(); box_env.label(text="Environment Texture (HDRI)", icon='IMAGE_DATA'); col_list = box_env.column(align=True); col_list.label(text="HDRI Presets:")
            for i, path in enumerate(HDRI_PATHS): op = col_list.operator(f"{PREFIX}.load_hdri_from_list", text=os.path.basename(path), depress=(props.hdri_list_index == i)); op.hdri_index = i
            box_env.separator(); env_node = find_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
            if env_node:
                box_env.template_ID(env_node, "image", open="image.open", text="Select HDRI"); mapping_node = find_node(nodes, 'ShaderNodeMapping', 'Mapping')
                if mapping_node:
                    box_transform = box_env.box(); box_transform.label(text="Transform", icon='OBJECT_DATA'); col = box_transform.column(align=True)
                    for prop_name in ['Location', 'Rotation', 'Scale']:
                        row = col.row(align=True); split = row.split(factor=0.8, align=True); split.prop(mapping_node.inputs[prop_name], "default_value", text=prop_name)
                        op = split.operator(f"{PREFIX}.reset_transform", text="", icon='FILE_REFRESH'); op.property_to_reset = prop_name
        elif props.background_mode == 'SKY':
            box_sky = layout.box(); box_sky.label(text="Sky Texture", icon='WORLD_DATA'); sky_node = find_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture')
            if sky_node:
                col_sky = box_sky.column(align=True); col_sky.prop(sky_node, "sky_type", text="Sky Type")
                if sky_node.sky_type == 'NISHITA':
                    if hasattr(sky_node, 'sun_elevation'): col_sky.prop(sky_node, "sun_elevation", text="Sun Elevation")
                    if hasattr(sky_node, 'sun_rotation'): col_sky.prop(sky_node, "sun_rotation", text="Sun Rotation")
                    if hasattr(sky_node, 'altitude'): col_sky.prop(sky_node, "altitude", text="Altitude")
                    if hasattr(sky_node, 'air_density'): col_sky.prop(sky_node, "air_density", text="Air Density")
                    if hasattr(sky_node, 'dust_density'): col_sky.prop(sky_node, "dust_density", text="Dust Density")
                    if hasattr(sky_node, 'ozone_density'): col_sky.prop(sky_node, "ozone_density", text="Ozone Density")
                elif sky_node.sky_type in {'PREETHAM', 'HOSEK_WILKIE'}:
                    if hasattr(sky_node, 'turbidity'): col_sky.prop(sky_node, "turbidity", text="Turbidity")
                    if hasattr(sky_node, 'ground_albedo'): col_sky.prop(sky_node, "ground_albedo", text="Ground Albedo")
class SFC_PT_InfoPanel(Panel):
    bl_label = "カメラ情報"; bl_idname = PANEL_IDS["INFO"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["INFO"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): layout, props = self.layout, context.scene.surface_camera_properties; col = layout.column(align=True); row = col.row(align=True); row.label(text="焦点距離:"); row.label(text=props.info_focal_length); row = col.row(align=True); row.label(text="水平視野角:"); row.label(text=props.info_horizontal_fov); col.separator(); row = col.row(align=True); row.label(text="カメラ位置:"); row.label(text=props.info_camera_location); row = col.row(align=True); row.label(text="注視点:"); row.label(text=props.info_target_location); row = col.row(align=True); row.label(text="注視点までの距離:"); row.label(text=props.info_distance_to_target); row = col.row(align=True); row.label(text="注視点での横幅:"); row.label(text=props.info_viewable_width); col.separator(); row = col.row(align=True); row.label(text="クリップ範囲:"); row.label(text=props.info_clip_setting); col.separator(); col.prop(props, "info_precision", text="表示桁数"); col.operator(f"{PREFIX}.copy_all_info", text="全情報をコピー", icon='COPY_ID')
class SFC_PT_GridPanel(Panel):
    bl_label = "Grid Color"; bl_idname = PANEL_IDS["GRID"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["GRID"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): layout, props, theme = self.layout, context.scene.theme_grid_properties, bpy.context.preferences.themes[0]; layout.label(text=f"Current: {tuple(round(c, 3) for c in theme.view_3d.grid)}"); layout.operator(f"{PREFIX}.copy_grid_color", text="Copy Grid Color"); layout.separator(); layout.prop(props, "grid_preset"); layout.prop(props, "grid_color"); layout.operator(f"{PREFIX}.apply_grid_color", text="Apply Grid Color")
class SFC_PT_WirePanel(Panel):
    bl_label = "Wire Color"; bl_idname = PANEL_IDS["WIRE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["WIRE"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): layout, props, theme = self.layout, context.scene.theme_wire_properties, bpy.context.preferences.themes[0]; layout.label(text=f"Current: {tuple(round(c, 3) for c in theme.view_3d.wire)}"); layout.operator(f"{PREFIX}.copy_wire_color", text="Copy Wire Color"); layout.separator(); layout.prop(props, "wire_preset"); layout.prop(props, "wire_color"); layout.operator(f"{PREFIX}.apply_wire_color", text="Apply Wire Color")

# --- リンクパネル ---
class SFC_PT_LinksPanel(Panel):
    bl_label = "リンク"; bl_idname = PANEL_IDS["LINKS"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["LINKS"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout = self.layout
        for link in ADDON_LINKS:
            op = layout.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
            op.url = link["url"]
class SFC_PT_NewDocsLinksPanel(Panel):
    bl_label = "アドオン管理"; bl_idname = PANEL_IDS["LINKS_NEWDOC"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
    def draw(self, context):
        layout = self.layout
        if not NEW_DOC_LINKS:
            layout.label(text="No links available.", icon='INFO')
        for link in NEW_DOC_LINKS:
            op = layout.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
            op.url = link["url"]
class SFC_PT_DocsLinksPanel(Panel):
    bl_label = "関連ドキュメント"; bl_idname = PANEL_IDS["LINKS_DOC"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
    def draw(self, context):
        layout = self.layout
        for link in DOC_LINKS:
            op = layout.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
            op.url = link["url"]
class SFC_PT_SocialLinksPanel(Panel):
    bl_label = "ソーシャルリンク"; bl_idname = PANEL_IDS["LINKS_SOCIAL"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
    def draw(self, context):
        layout = self.layout
        for link in SOCIAL_LINKS:
            op = layout.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
            op.url = link["url"]

class SFC_PT_RemovePanel(Panel):
    bl_label = "アドオン削除"; bl_idname = PANEL_IDS["REMOVE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["REMOVE"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): self.layout.operator(f"{PREFIX}.remove_addon", text="このアドオンを解除", icon='CANCEL')

# --- World Tools 初期化 ---
def initial_setup():
    if bpy.context.scene.world and bpy.context.scene.world.use_nodes:
        props = bpy.context.scene.zionad_swt_props; nodes = bpy.context.scene.world.node_tree.nodes
        background_node = find_node(nodes, 'ShaderNodeBackground', 'Background')
        if background_node and background_node.inputs['Color'].is_linked:
            source_node = background_node.inputs['Color'].links[0].from_node
            if source_node.type == 'TEX_SKY': props.background_mode = 'SKY'
            else: props.background_mode = 'HDRI';
        update_background_mode(props, bpy.context)
    return None

# --- 登録/解除 ---
classes = (
    ThemeGridProperties, ThemeWireProperties, TargetProperty, SurfaceCameraProperties, ZIONAD_SWT_Properties,
    SFC_OT_GridApplyColor, SFC_OT_GridCopyColor, SFC_OT_WireApplyColor, SFC_OT_WireCopyColor, SFC_OT_ApplyCameraColor,
    SFC_OT_CreateDedicatedCamera, SFC_OT_SyncWithCamera, SFC_OT_UnlinkObject, SFC_OT_ResetProperty, SFC_OT_SetFOV,
    SFC_OT_CopyAllInfo, SFC_OT_OpenURL, SFC_OT_RemoveAddon, SFC_OT_SetFixedLocationFromView,
    ZIONAD_SWT_OT_LoadHdriFromList, ZIONAD_SWT_OT_ResetTransform,
    SFC_PT_CameraSetupPanel, SFC_PT_PositionPanel, SFC_PT_AimingPanel, SFC_PT_LensPanel, SFC_PT_CameraDisplayPanel,
    ZIONAD_SWT_PT_WorldControlPanel, SFC_PT_InfoPanel, SFC_PT_GridPanel, SFC_PT_WirePanel,
    SFC_PT_LinksPanel, SFC_PT_NewDocsLinksPanel, SFC_PT_DocsLinksPanel, SFC_PT_SocialLinksPanel,
    SFC_PT_RemovePanel,
)
_registered_classes = []
def register():
    global _registered_classes; _registered_classes.clear()
    for cls in classes:
        try: bpy.utils.register_class(cls); _registered_classes.append(cls)
        except Exception as e: print(f"Error registering class {cls.__name__}: {e}"); unregister(); raise
    bpy.types.Scene.surface_camera_properties = PointerProperty(type=SurfaceCameraProperties)
    bpy.types.Scene.theme_grid_properties = PointerProperty(type=ThemeGridProperties)
    bpy.types.Scene.theme_wire_properties = PointerProperty(type=ThemeWireProperties)
    bpy.types.Scene.zionad_swt_props = PointerProperty(type=ZIONAD_SWT_Properties)
    if on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update)
    if not bpy.app.timers.is_registered(initial_setup): bpy.app.timers.register(initial_setup, first_interval=0.1)
def unregister():
    global _registered_classes
    if on_depsgraph_update in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update)
    if _update_timer and bpy.app.timers.is_registered(reset_update_flag): bpy.app.timers.unregister(reset_update_flag)
    if bpy.app.timers.is_registered(initial_setup): bpy.app.timers.unregister(initial_setup)
    for prop_name in ['surface_camera_properties', 'theme_grid_properties', 'theme_wire_properties', 'zionad_swt_props']:
        if hasattr(bpy.types.Scene, prop_name):
            try: delattr(bpy.types.Scene, prop_name)
            except (AttributeError, RuntimeError): pass
    for cls in reversed(classes):
        if hasattr(bpy.utils, 'unregister_class') and cls in _registered_classes:
            try: bpy.utils.unregister_class(cls)
            except RuntimeError: pass
    _registered_classes.clear()

if __name__ == "__main__":
    try: unregister()
    except Exception: pass
    register()

# ▲▲▲ ここまで ▲▲▲
'''

# ==============================================================================
#  Square Torus システムロジック
# ==============================================================================

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', '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>"
            if tag_start not in code or tag_end not in code:
                self.report({'ERROR'}, "DICT tags missing! Script might be corrupted.")
                return {'CANCELLED'}
            pre_code, rest = code.split(tag_start, 1)
            _, post_code = rest.split(tag_end, 1)
            final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
            if SOURCE_ID_TAG not in final_code:
                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, p.torus_rot, p.torus_plane = (0,0,0), (0,0,0), 'XY'
        p.square_size, p.corner_radius, p.minor_radius = 10.0, 0.0, 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'}

# ==============================================================================
#  追加スクリプト書き出しオペレーター
# ==============================================================================
class OT_AddZukkeiScript(Operator):
    bl_idname = f"{OP_PREFIX}.add_zukkei_script"
    bl_label = "Load Zukkei Script"
    bl_description = "図形&配列ジェネレータースクリプトをテキストエディターに読み込みます"
    def execute(self, context):
        text_name = "B5200_Zukkei_Array_View_20260319.py"
        if text_name in bpy.data.texts: text_data = bpy.data.texts[text_name]; text_data.clear()
        else: text_data = bpy.data.texts.new(name=text_name)
        text_data.write(ZUKKEI_SCRIPT_CONTENT)
        found_editor = False
        for area in context.screen.areas:
            if area.type == 'TEXT_EDITOR': area.spaces.active.text = text_data; found_editor = True; break
        if found_editor: self.report({'INFO'}, f"テキストエディターに '{text_name}' を読み込みました!")
        else: self.report({'INFO'}, f"'{text_name}' を作成しました。Text Editor を開いて確認してください。")
        return {'FINISHED'}

class OT_AddViewportScript(Operator):
    bl_idname = f"{OP_PREFIX}.add_viewport_script"
    bl_label = "Load Viewport & Sun Script"
    bl_description = "3D Viewport Color & Sun スクリプトをテキストエディターに読み込みます"
    def execute(self, context):
        text_name = "Viewport_Color_Sun_20260316.py"
        if text_name in bpy.data.texts: text_data = bpy.data.texts[text_name]; text_data.clear()
        else: text_data = bpy.data.texts.new(name=text_name)
        text_data.write(VIEWPORT_SCRIPT_CONTENT)
        found_editor = False
        for area in context.screen.areas:
            if area.type == 'TEXT_EDITOR': area.spaces.active.text = text_data; found_editor = True; break
        if found_editor: self.report({'INFO'}, f"テキストエディターに '{text_name}' を読み込みました!")
        else: self.report({'INFO'}, f"'{text_name}' を作成しました。")
        return {'FINISHED'}

class OT_AddCameraScript(Operator):
    bl_idname = f"{OP_PREFIX}.add_camera_script"
    bl_label = "Load Fixed Camera Script"
    bl_description = "Fixed Camera & World スクリプトをテキストエディターに読み込みます"
    def execute(self, context):
        text_name = "Fixed_Camera_World_2026.py"
        if text_name in bpy.data.texts: text_data = bpy.data.texts[text_name]; text_data.clear()
        else: text_data = bpy.data.texts.new(name=text_name)
        text_data.write(CAMERA_SCRIPT_CONTENT)
        found_editor = False
        for area in context.screen.areas:
            if area.type == 'TEXT_EDITOR': area.spaces.active.text = text_data; found_editor = True; break
        if found_editor: self.report({'INFO'}, f"テキストエディターに '{text_name}' を読み込みました!")
        else: self.report({'INFO'}, f"'{text_name}' を作成しました。")
        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")

class PT_ScriptPanel(Panel):
    bl_label = "Additional Scripts"
    bl_idname = f"{PREFIX}_PT_script"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        layout.label(text="B5200 図形ジェネレーターを追加:")
        layout.operator(OT_AddZukkeiScript.bl_idname, icon='TEXT', text="Load Zukkei Script")
        layout.separator()
        layout.label(text="5520 Viewport & Sun を追加:")
        layout.operator(OT_AddViewportScript.bl_idname, icon='TEXT', text="Load Viewport Script")
        layout.separator()
        layout.label(text="v100 Fixed Camera を追加:")
        layout.operator(OT_AddCameraScript.bl_idname, icon='TEXT', text="Load Camera Script")

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

classes = (
    PG_TorusProps, 
    OT_CreateTorus, 
    OT_CopyFullScript, 
    OT_Reset, 
    OT_OpenUrl, 
    OT_RemoveAddon, 
    OT_AddZukkeiScript,
    OT_AddViewportScript,
    OT_AddCameraScript,
    PT_MainPanel, 
    PT_LinksPanel, 
    PT_RemovePanel, 
    PT_ScriptPanel
)

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()
3つのアドオンを追加して貼る
bl_info = {
    "name": "zionad 520[ Sq-Torus ] SquareTorus20260324",
    "author": "zionadchat",
    "version": (7, 0, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": "Topology-Perfect Square Torus Generator & Script Loader",
    "category": "3D View",
}

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_V7_FINAL ###"

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>

# ==============================================================================
#  【 内包する追加スクリプトの文字列定義 】
#   ※以下の指定された場所に、対象のスクリプトをペーストしてください。
# ==============================================================================

# ① 図形ジェネレーター (b5200_zukkei...) のコードを以下に貼り付け
ZUKKEI_SCRIPT_CONTENT = r'''
# ▼▼▼ ここに「図形&配列ジェネレーター」の全コードを貼り付けてください ▼▼▼

# ▲▲▲ ここまで ▲▲▲
'''

# ② Viewport Color & Sun (view2026316) のコードを以下に貼り付け
VIEWPORT_SCRIPT_CONTENT = r'''
# ▼▼▼ ここに「3D Viewport Color & Sun」の全コードを貼り付けてください ▼▼▼

# ▲▲▲ ここまで ▲▲▲
'''

# ③ Fixed Camera & World (cam_kotei...) のコードを以下に貼り付け
CAMERA_SCRIPT_CONTENT = r'''
# ▼▼▼ ここに「v100 Fixed Camera & World」の全コードを貼り付けてください ▼▼▼

# ▲▲▲ ここまで ▲▲▲
'''

# ==============================================================================
#  Square Torus システムロジック
# ==============================================================================

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', '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>"
            if tag_start not in code or tag_end not in code:
                self.report({'ERROR'}, "DICT tags missing! Script might be corrupted.")
                return {'CANCELLED'}
            pre_code, rest = code.split(tag_start, 1)
            _, post_code = rest.split(tag_end, 1)
            final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
            if SOURCE_ID_TAG not in final_code:
                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, p.torus_rot, p.torus_plane = (0,0,0), (0,0,0), 'XY'
        p.square_size, p.corner_radius, p.minor_radius = 10.0, 0.0, 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'}

# ==============================================================================
#  追加スクリプト書き出しオペレーター
# ==============================================================================
class OT_AddZukkeiScript(Operator):
    bl_idname = f"{OP_PREFIX}.add_zukkei_script"
    bl_label = "Load Zukkei Script"
    bl_description = "図形&配列ジェネレータースクリプトをテキストエディターに読み込みます"
    def execute(self, context):
        text_name = "B5200_Zukkei_Array_View_20260319.py"
        if text_name in bpy.data.texts: text_data = bpy.data.texts[text_name]; text_data.clear()
        else: text_data = bpy.data.texts.new(name=text_name)
        text_data.write(ZUKKEI_SCRIPT_CONTENT)
        found_editor = False
        for area in context.screen.areas:
            if area.type == 'TEXT_EDITOR': area.spaces.active.text = text_data; found_editor = True; break
        if found_editor: self.report({'INFO'}, f"テキストエディターに '{text_name}' を読み込みました!")
        else: self.report({'INFO'}, f"'{text_name}' を作成しました。Text Editor を開いて確認してください。")
        return {'FINISHED'}

class OT_AddViewportScript(Operator):
    bl_idname = f"{OP_PREFIX}.add_viewport_script"
    bl_label = "Load Viewport & Sun Script"
    bl_description = "3D Viewport Color & Sun スクリプトをテキストエディターに読み込みます"
    def execute(self, context):
        text_name = "Viewport_Color_Sun_20260316.py"
        if text_name in bpy.data.texts: text_data = bpy.data.texts[text_name]; text_data.clear()
        else: text_data = bpy.data.texts.new(name=text_name)
        text_data.write(VIEWPORT_SCRIPT_CONTENT)
        found_editor = False
        for area in context.screen.areas:
            if area.type == 'TEXT_EDITOR': area.spaces.active.text = text_data; found_editor = True; break
        if found_editor: self.report({'INFO'}, f"テキストエディターに '{text_name}' を読み込みました!")
        else: self.report({'INFO'}, f"'{text_name}' を作成しました。")
        return {'FINISHED'}

class OT_AddCameraScript(Operator):
    bl_idname = f"{OP_PREFIX}.add_camera_script"
    bl_label = "Load Fixed Camera Script"
    bl_description = "Fixed Camera & World スクリプトをテキストエディターに読み込みます"
    def execute(self, context):
        text_name = "Fixed_Camera_World_2026.py"
        if text_name in bpy.data.texts: text_data = bpy.data.texts[text_name]; text_data.clear()
        else: text_data = bpy.data.texts.new(name=text_name)
        text_data.write(CAMERA_SCRIPT_CONTENT)
        found_editor = False
        for area in context.screen.areas:
            if area.type == 'TEXT_EDITOR': area.spaces.active.text = text_data; found_editor = True; break
        if found_editor: self.report({'INFO'}, f"テキストエディターに '{text_name}' を読み込みました!")
        else: self.report({'INFO'}, f"'{text_name}' を作成しました。")
        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")

class PT_ScriptPanel(Panel):
    bl_label = "Additional Scripts"
    bl_idname = f"{PREFIX}_PT_script"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        layout.label(text="B5200 図形ジェネレーターを追加:")
        layout.operator(OT_AddZukkeiScript.bl_idname, icon='TEXT', text="Load Zukkei Script")
        layout.separator()
        layout.label(text="5520 Viewport & Sun を追加:")
        layout.operator(OT_AddViewportScript.bl_idname, icon='TEXT', text="Load Viewport Script")
        layout.separator()
        layout.label(text="v100 Fixed Camera を追加:")
        layout.operator(OT_AddCameraScript.bl_idname, icon='TEXT', text="Load Camera Script")

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

classes = (
    PG_TorusProps, 
    OT_CreateTorus, 
    OT_CopyFullScript, 
    OT_Reset, 
    OT_OpenUrl, 
    OT_RemoveAddon, 
    OT_AddZukkeiScript,
    OT_AddViewportScript,
    OT_AddCameraScript,
    PT_MainPanel, 
    PT_LinksPanel, 
    PT_RemovePanel, 
    PT_ScriptPanel
)

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