ダミー人形
import bpy
from bpy.types import Operator, Panel
from bpy.props import IntProperty

# =========================================================
# アドオン宣言
# =========================================================
bl_info = {
    "name": "Addon Code Container",
    "author": "AI",
    "version": (4, 1, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > Code Container",
    "description": "ソースコード内に貼り付けた複数のアドオンコードを管理するコンテナ",
    "category": "Development",
}

PREFIX_VAL = "zionad_container"
ADDON_TITLE = "Code Container"

# =========================================================
# 【手動設定 & コード貼り付けエリア】
# =========================================================

# ---------------------------------
# 1番登録アドオン
TITLE_1 = "1番のアドオン名をここに入力"
CODE_1 = '''

'''
# ---------------------------------

# ---------------------------------
# 2番登録アドオン
TITLE_2 = ""
CODE_2 = '''

'''
# ---------------------------------

# ---------------------------------
# 3番登録アドオン
TITLE_3 = ""
CODE_3 = '''

'''
# ---------------------------------

# ---------------------------------
# 4番登録アドオン
TITLE_4 = ""
CODE_4 = '''

'''
# ---------------------------------

# ---------------------------------
# 5番登録アドオン
TITLE_5 = ""
CODE_5 = '''

'''
# ---------------------------------

# ---------------------------------
# 6番登録アドオン
TITLE_6 = ""
CODE_6 = '''

'''
# ---------------------------------

# ---------------------------------
# 7番登録アドオン
TITLE_7 = ""
CODE_7 = '''

'''
# ---------------------------------

# ---------------------------------
# 8番登録アドオン
TITLE_8 = ""
CODE_8 = '''

'''
# ---------------------------------

# ---------------------------------
# 9番登録アドオン
TITLE_9 = ""
CODE_9 = '''

'''
# ---------------------------------

# ---------------------------------
# 10番登録アドオン
TITLE_10 = "ダミー人形"
CODE_10 = '''
bl_info = {
    "name": "zionad_Dummy Sightline Gen",
    "author": "Your Name",
    "version": (1, 4, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "Grid格子設定の追加、現在の設定を初期設定コードとしてコピーする機能の追加",
    "category": "3D View",
}

# 【作成日】 2024年5月22日 17:00

import bpy
import bmesh
import math
import mathutils
import webbrowser

# ===================================
# 【初期設定・定数定義】 (1行ずつ定義)
# ===================================
ADDON_TITLE = "Dummy Sightline Gen"
PREFIX = "dummy_sight"
COLLECTION_MAIN = "Dummy_Circle_Sightline_Group"
OBJ_NAME_ROOT = "Dummy_Root"
OBJ_NAME_DUMMY = "Dummy"
OBJ_NAME_TORUS = "Torus"
OBJ_NAME_SIGHTLINE = "Sightline_Arrow"

# 寸法初期値 (画像ベース)
INIT_TORUS_RADIUS = 10.00
INIT_SIGHTLINE_LENGTH = 10.00

# 太さ初期値 (画像ベース)
INIT_DUMMY_THICKNESS = 0.40
INIT_TORUS_THICKNESS = 0.30
INIT_SIGHTLINE_THICKNESS = 0.40

# カラー初期値 (R, G, B, Alpha) - 画像の色味に近似
INIT_COLOR_DUMMY = (0.05, 0.92, 0.06, 1.00)
INIT_COLOR_TORUS = (0.91, 0.86, 0.36, 1.00)
INIT_COLOR_SIGHT = (0.12, 0.00, 0.06, 1.00)
INIT_SIGHTLINE_EMISSION = 5.0

LINK_NAME_1 = "ダミー人形 20260713"
LINK_URL_1 = "<https://note.com/zionadmillion/n/ndebba8a8d691>"

# ===================================
# 【ビュー情報取得 関数】
# ===================================
def get_rv3d(context):
    for a in context.window.screen.areas:
        if a.type == 'VIEW_3D':
            return a.spaces.active.region_3d
    return None

def get_view_info_data(context):
    rv3d = None
    area = None
    space = None
    for a in context.window.screen.areas:
        if a.type == 'VIEW_3D':
            rv3d = a.spaces.active.region_3d
            area = a
            space = a.spaces.active
            break
            
    if not rv3d or not area or not space: return None
        
    mode = rv3d.view_perspective
    target_pos = rv3d.view_location
    view_dist = rv3d.view_distance
    cam_pos = rv3d.view_matrix.inverted().translation
    view_dir = rv3d.view_rotation @ mathutils.Vector((0.0, 0.0, -1.0))
    
    aspect = area.height / area.width if area.width > 0 else 1.0
    sensor = 32.0 
    
    if mode == 'PERSP':
        proj_text = "透視投影"
        is_persp = True
    elif mode == 'ORTHO':
        proj_text = "平行投影"
        is_persp = False
    elif mode == 'CAMERA':
        proj_text = "カメラビュー"
        is_persp = True
    else:
        proj_text = "不明"
        is_persp = False
    
    if is_persp:
        fov_h = 2.0 * math.atan(sensor / (2.0 * space.lens))
        w = 2.0 * view_dist * math.tan(fov_h / 2.0)
        h = w * aspect
    else: 
        w = view_dist * (sensor / space.lens)
        h = w * aspect
        
    return {
        'proj_text': proj_text,
        'cam_pos': cam_pos,
        'view_dir': view_dir,
        'target_pos': target_pos,
        'dist': view_dist,
        'w': w,
        'h': h,
        'is_persp': is_persp
    }

# ===================================
# 【メッシュ生成 ヘルパー関数】
# ===================================
def add_cylinder_between(bm, p1, p2, radius):
    p1 = mathutils.Vector(p1)
    p2 = mathutils.Vector(p2)
    v = p2 - p1
    dist = v.length
    if dist < 0.0001: return
    rot = v.to_track_quat('Z', 'Y').to_matrix().to_4x4()
    loc = mathutils.Matrix.Translation((p1 + p2) / 2.0)
    bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=radius, radius2=radius, depth=dist, matrix=loc @ rot)

def rebuild_dummy_mesh(bm, scale, dummy_thickness):
    R = INIT_TORUS_RADIUS * scale
    Z_c = 1.0 * scale
    pelvis = (0, 0, Z_c)
    neck = (0, 0, Z_c + R * 0.4)
    head = (0, 0, Z_c + R * 0.55)
    r_hand = (R, 0, Z_c + R * 0.25)
    l_hand = (-R, 0, Z_c + R * 0.25)
    r_foot = (R * math.sin(math.radians(40)), 0, Z_c - R * math.cos(math.radians(40)))
    l_foot = (-R * math.sin(math.radians(40)), 0, Z_c - R * math.cos(math.radians(40)))
    
    mat_head = mathutils.Matrix.Translation(head)
    head_size = dummy_thickness * scale * 2.0
    bmesh.ops.create_uvsphere(bm, u_segments=16, v_segments=8, radius=head_size, matrix=mat_head)
    
    thickness = dummy_thickness * scale
    add_cylinder_between(bm, pelvis, neck, thickness * 1.5)
    add_cylinder_between(bm, neck, r_hand, thickness)
    add_cylinder_between(bm, neck, l_hand, thickness)
    add_cylinder_between(bm, pelvis, r_foot, thickness)
    add_cylinder_between(bm, pelvis, l_foot, thickness)

def rebuild_torus_mesh(bm, scale, torus_thickness):
    r_major = INIT_TORUS_RADIUS * scale
    r_minor = torus_thickness * scale
    segments_major = 48
    segments_minor = 12
    verts = []
    
    for i in range(segments_major):
        u = i * 2.0 * math.pi / segments_major
        cos_u = math.cos(u)
        sin_u = math.sin(u)
        row_verts = []
        for j in range(segments_minor):
            v = j * 2.0 * math.pi / segments_minor
            cos_v = math.cos(v)
            sin_v = math.sin(v)
            x = (r_major + r_minor * cos_v) * cos_u
            y = (r_major + r_minor * cos_v) * sin_u
            z = r_minor * sin_v
            
            px = x
            py = -z
            pz = y + (1.0 * scale)
            row_verts.append(bm.verts.new((px, py, pz)))
        verts.append(row_verts)
        
    bm.verts.ensure_lookup_table()
    for i in range(segments_major):
        i_next = (i + 1) % segments_major
        for j in range(segments_minor):
            j_next = (j + 1) % segments_minor
            bm.faces.new((verts[i][j], verts[i_next][j], verts[i_next][j_next], verts[i][j_next]))

def rebuild_sightline_mesh(bm, scale, length, sight_thickness):
    R = INIT_TORUS_RADIUS * scale
    Z_c = 1.0 * scale
    head = mathutils.Vector((0, 0.15 * scale, Z_c + R * 0.55))
    
    thickness = sight_thickness * scale
    cone_length = thickness * 15.0
    
    rot = mathutils.Matrix.Rotation(-math.pi/2, 4, 'X')
    
    loc_cyl = mathutils.Matrix.Translation(head + mathutils.Vector((0, length / 2.0, 0)))
    bmesh.ops.create_cone(bm, cap_ends=True, segments=8, radius1=thickness, radius2=thickness, depth=length, matrix=loc_cyl @ rot)
    
    loc_tip = mathutils.Matrix.Translation(head + mathutils.Vector((0, length + cone_length / 2.0, 0)))
    bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=thickness * 3.0, radius2=0.0, depth=cone_length, matrix=loc_tip @ rot)

# ===================================
# 【リアルタイム更新 処理】
# ===================================
def update_models(self, context):
    if self.is_detached: return
    obj_dummy = bpy.data.objects.get(OBJ_NAME_DUMMY)
    obj_torus = bpy.data.objects.get(OBJ_NAME_TORUS)
    obj_sight = bpy.data.objects.get(OBJ_NAME_SIGHTLINE)
    scale = self.torus_radius / INIT_TORUS_RADIUS
    
    if obj_dummy and obj_dummy.data:
        bm = bmesh.new(); rebuild_dummy_mesh(bm, scale, self.dummy_thickness); bm.to_mesh(obj_dummy.data); bm.free()
    if obj_torus and obj_torus.data:
        bm = bmesh.new(); rebuild_torus_mesh(bm, scale, self.torus_thickness); bm.to_mesh(obj_torus.data); bm.free()
    if obj_sight and obj_sight.data:
        bm = bmesh.new(); rebuild_sightline_mesh(bm, scale, self.sight_length, self.sight_thickness); bm.to_mesh(obj_sight.data); bm.free()
    context.view_layer.update()

def update_materials(self, context):
    if self.is_detached: return
    def set_mat_color(mat_name, color_val, is_emission=False):
        mat = bpy.data.materials.get(mat_name)
        if mat and mat.use_nodes:
            principled = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None)
            if principled:
                principled.inputs['Base Color'].default_value = color_val
                if 'Alpha' in principled.inputs:
                    principled.inputs['Alpha'].default_value = color_val[3]
                if is_emission:
                    if 'Emission Color' in principled.inputs:
                        principled.inputs['Emission Color'].default_value = (color_val[0], color_val[1], color_val[2], 1.0)
                        principled.inputs['Emission Strength'].default_value = INIT_SIGHTLINE_EMISSION
    
    set_mat_color(f"{PREFIX}_Mat_Dummy", self.color_dummy)
    set_mat_color(f"{PREFIX}_Mat_Torus", self.color_torus)
    set_mat_color(f"{PREFIX}_Mat_Sight", self.color_sight, is_emission=True)

# ===================================
# 【プロパティ定義】
# ===================================
class DummySight_Props(bpy.types.PropertyGroup):
    torus_radius: bpy.props.FloatProperty(name="基準サイズ", default=INIT_TORUS_RADIUS, min=0.1, update=update_models)
    sight_length: bpy.props.FloatProperty(name="視線の長さ", default=INIT_SIGHTLINE_LENGTH, min=0.1, update=update_models)
    
    dummy_thickness: bpy.props.FloatProperty(name="ダミーの太さ", default=INIT_DUMMY_THICKNESS, min=0.01, update=update_models)
    torus_thickness: bpy.props.FloatProperty(name="トーラスの太さ", default=INIT_TORUS_THICKNESS, min=0.01, update=update_models)
    sight_thickness: bpy.props.FloatProperty(name="視線の太さ", default=INIT_SIGHTLINE_THICKNESS, min=0.001, update=update_models)
    
    is_detached: bpy.props.BoolProperty(name="切り離しフラグ", default=False)
    
    color_dummy: bpy.props.FloatVectorProperty(name="ダミーの色", subtype='COLOR', size=4, default=INIT_COLOR_DUMMY, min=0.0, max=1.0, update=update_materials)
    color_torus: bpy.props.FloatVectorProperty(name="トーラスの色", subtype='COLOR', size=4, default=INIT_COLOR_TORUS, min=0.0, max=1.0, update=update_materials)
    color_sight: bpy.props.FloatVectorProperty(name="視線の色", subtype='COLOR', size=4, default=INIT_COLOR_SIGHT, min=0.0, max=1.0, update=update_materials)
    
    custom_grid_scale: bpy.props.FloatProperty(name="Grid指定値", default=10.0, min=0.001)

# ===================================
# 【オペレーター群】
# ===================================
class OT_reset_view(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX}_reset_view"
    bl_label = "選択オブジェクトを画面中央にする"
    def execute(self, context):
        rv3d = get_rv3d(context)
        if not rv3d: return {'CANCELLED'}
        target = context.active_object
        rv3d.view_location = target.matrix_world.to_translation() if target else rv3d.view_location.copy()
        rv3d.view_distance = max(target.dimensions.length * 1.5, 10.0) if target else 30.0
        context.view_layer.update()
        return {'FINISHED'}

class OT_update_info(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX}_update_info"
    bl_label = "更新"
    def execute(self, context):
        for area in context.screen.areas:
            if area.type == 'VIEW_3D': area.tag_redraw()
        return {'FINISHED'}

class OT_copy_info(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX}_copy_info"
    bl_label = "情報をコピー"
    def execute(self, context):
        info = get_view_info_data(context)
        if not info: return {'CANCELLED'}
        
        lines = [
            f"投影モード: {info['proj_text']}",
            f"架空の視座位置 X: {info['cam_pos'].x:.1f}, Y: {info['cam_pos'].y:.1f}, Z: {info['cam_pos'].z:.1f}",
            f"画面中央への視線方向 X: {info['view_dir'].x:.1f}, Y: {info['view_dir'].y:.1f}, Z: {info['view_dir'].z:.1f}",
            f"画面中央位置 X: {info['target_pos'].x:.1f}, Y: {info['target_pos'].y:.1f}, Z: {info['target_pos'].z:.1f}",
            f"画面水平の大きさ: {info['w']:.1f}",
            f"画面上下の大きさ: {info['h']:.1f}"
        ]
        if info['is_persp']:
            lines.append(f"視座からの距離: {info['dist']:.1f}")

        context.window_manager.clipboard = "\n".join(lines)
        self.report({'INFO'}, "ビュー情報をクリップボードにコピーしました")
        return {'FINISHED'}

class OT_set_grid_scale(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX}_set_grid_scale"
    bl_label = "Grid間隔を設定"
    scale_val: bpy.props.FloatProperty(name="Scale")
    use_custom: bpy.props.BoolProperty(name="カスタム値", default=False)
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        val = props.custom_grid_scale if self.use_custom else self.scale_val
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        space.overlay.grid_scale = val
        self.report({'INFO'}, f"Grid間隔を {val} に設定しました")
        return {'FINISHED'}

class OT_create_set(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_create_set"
    bl_label = "セットを作成"
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        props.is_detached = False
        
        col = bpy.data.collections.get(COLLECTION_MAIN)
        if not col:
            col = bpy.data.collections.new(COLLECTION_MAIN)
            context.scene.collection.children.link(col)
            
        for name in [OBJ_NAME_ROOT, OBJ_NAME_DUMMY, OBJ_NAME_TORUS, OBJ_NAME_SIGHTLINE]:
            obj = bpy.data.objects.get(name)
            if obj: bpy.data.objects.remove(obj, do_unlink=True)
            
        root = bpy.data.objects.new(OBJ_NAME_ROOT, None)
        root.empty_display_type = 'PLAIN_AXES'
        col.objects.link(root)
                
        def create_obj(name, mat_name):
            mesh = bpy.data.meshes.new(name)
            obj = bpy.data.objects.new(name, mesh)
            col.objects.link(obj)
            obj.parent = root
            
            mat = bpy.data.materials.get(mat_name)
            if not mat:
                mat = bpy.data.materials.new(mat_name)
                mat.use_nodes = True
                mat.blend_method = 'BLEND'
            if not obj.data.materials:
                obj.data.materials.append(mat)
            return obj
            
        create_obj(OBJ_NAME_DUMMY, f"{PREFIX}_Mat_Dummy")
        create_obj(OBJ_NAME_TORUS, f"{PREFIX}_Mat_Torus")
        create_obj(OBJ_NAME_SIGHTLINE, f"{PREFIX}_Mat_Sight")
        
        update_models(props, context)
        update_materials(props, context)
        
        self.report({'INFO'}, "ダミー視線セットを作成しました")
        return {'FINISHED'}

class OT_detach(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_detach"
    bl_label = "アドオンから切り離し (一体化)"
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        col = bpy.data.collections.get(COLLECTION_MAIN)
        if not col or props.is_detached: return {'CANCELLED'}
            
        import time
        timestamp = int(time.time())
        
        mesh_objs = [obj for obj in col.objects if obj.type == 'MESH']
        root = next((obj for obj in col.objects if obj.name.startswith(OBJ_NAME_ROOT)), None)
        
        for obj in mesh_objs:
            mat_world = obj.matrix_world.copy()
            obj.parent = None
            obj.matrix_world = mat_world
            if obj.data: obj.data = obj.data.copy()
            for slot in obj.material_slots:
                if slot.material: slot.material = slot.material.copy()
        
        if root: bpy.data.objects.remove(root, do_unlink=True)
            
        if mesh_objs:
            bpy.ops.object.select_all(action='DESELECT')
            for obj in mesh_objs: obj.select_set(True)
            context.view_layer.objects.active = mesh_objs[0]
            bpy.ops.object.join()
            
            joined_obj = context.active_object
            joined_obj.name = f"Dummy_Merged_{timestamp}"
                    
        col.name = f"{COLLECTION_MAIN}_{timestamp}"
        props.is_detached = True
        
        self.report({'INFO'}, "独立化し、1つのメッシュに結合しました")
        return {'FINISHED'}

class OT_reset_props(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_reset_props"
    bl_label = "初期値に戻す"
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        props.torus_radius = INIT_TORUS_RADIUS
        props.sight_length = INIT_SIGHTLINE_LENGTH
        props.dummy_thickness = INIT_DUMMY_THICKNESS
        props.torus_thickness = INIT_TORUS_THICKNESS
        props.sight_thickness = INIT_SIGHTLINE_THICKNESS
        
        props.color_dummy = INIT_COLOR_DUMMY
        props.color_torus = INIT_COLOR_TORUS
        props.color_sight = INIT_COLOR_SIGHT
        
        root = bpy.data.objects.get(OBJ_NAME_ROOT)
        if root:
            root.location = (0,0,0)
            root.rotation_euler = (0,0,0)
            root.scale = (1,1,1)
        return {'FINISHED'}

class OT_copy_settings(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_copy_settings"
    bl_label = "現在の設定を初期設定としてコピー"
    bl_description = "現在の数値をPythonコードの初期設定形式でクリップボードにコピーします"
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        
        lines = [
            "# ===================================",
            "# 【初期設定・定数定義】 (1行ずつ定義)",
            "# ===================================",
            f'ADDON_TITLE = "{ADDON_TITLE}"',
            f'PREFIX = "{PREFIX}"',
            f'COLLECTION_MAIN = "{COLLECTION_MAIN}"',
            f'OBJ_NAME_ROOT = "{OBJ_NAME_ROOT}"',
            f'OBJ_NAME_DUMMY = "{OBJ_NAME_DUMMY}"',
            f'OBJ_NAME_TORUS = "{OBJ_NAME_TORUS}"',
            f'OBJ_NAME_SIGHTLINE = "{OBJ_NAME_SIGHTLINE}"',
            "",
            "# 寸法初期値 (画像ベース)",
            f"INIT_TORUS_RADIUS = {props.torus_radius:.2f}",
            f"INIT_SIGHTLINE_LENGTH = {props.sight_length:.2f}",
            "",
            "# 太さ初期値 (画像ベース)",
            f"INIT_DUMMY_THICKNESS = {props.dummy_thickness:.2f}",
            f"INIT_TORUS_THICKNESS = {props.torus_thickness:.2f}",
            f"INIT_SIGHTLINE_THICKNESS = {props.sight_thickness:.2f}",
            "",
            "# カラー初期値 (R, G, B, Alpha) - 画像の色味に近似",
            f"INIT_COLOR_DUMMY = ({props.color_dummy[0]:.2f}, {props.color_dummy[1]:.2f}, {props.color_dummy[2]:.2f}, {props.color_dummy[3]:.2f})",
            f"INIT_COLOR_TORUS = ({props.color_torus[0]:.2f}, {props.color_torus[1]:.2f}, {props.color_torus[2]:.2f}, {props.color_torus[3]:.2f})",
            f"INIT_COLOR_SIGHT = ({props.color_sight[0]:.2f}, {props.color_sight[1]:.2f}, {props.color_sight[2]:.2f}, {props.color_sight[3]:.2f})",
            f"INIT_SIGHTLINE_EMISSION = {INIT_SIGHTLINE_EMISSION:.1f}",
            "",
            f'LINK_NAME_1 = "{LINK_NAME_1}"',
            f'LINK_URL_1 = "{LINK_URL_1}"'
        ]
        
        context.window_manager.clipboard = "\n".join(lines)
        self.report({'INFO'}, "現在の設定をクリップボードにコピーしました")
        return {'FINISHED'}

class OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX}_open_url"
    bl_label = "リンクを開く"
    url: bpy.props.StringProperty()
    def execute(self, context):
        if self.url: webbrowser.open(self.url)
        return {'FINISHED'}

class OT_remove_addon(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX}_remove_addon"
    bl_label = "アドオン削除"
    def execute(self, context): unregister(); return {'FINISHED'}

# ===================================
# 【UIパネル】
# ===================================
class PT_view_info(bpy.types.Panel):
    bl_idname = f"{PREFIX.upper()}_PT_view_info"
    bl_label = "1. ビュー情報・画面操作"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_TITLE
    
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX}_props")
        info = get_view_info_data(context)
        
        layout.operator(f"view3d.{PREFIX}_reset_view", text="選択オブジェクトを画面中央にする", icon='ZOOM_ALL')
        layout.separator()
        
        box = layout.box()
        row = box.row(align=True)
        row.label(text="ビュー情報:")
        row.operator(f"view3d.{PREFIX}_update_info", icon='FILE_REFRESH')
        
        if info:
            col = box.column(align=True)
            col.label(text=f"投影モード: {info['proj_text']}")
            col.label(text=f"架空の視座位置 X: {info['cam_pos'].x:.1f}, Y: {info['cam_pos'].y:.1f}, Z: {info['cam_pos'].z:.1f}")
            col.label(text=f"画面中央への視線方向 X: {info['view_dir'].x:.1f}, Y: {info['view_dir'].y:.1f}, Z: {info['view_dir'].z:.1f}")
            col.label(text=f"画面中央位置 X: {info['target_pos'].x:.1f}, Y: {info['target_pos'].y:.1f}, Z: {info['target_pos'].z:.1f}")
            col.label(text=f"画面水平の大きさ: {info['w']:.1f}")
            col.label(text=f"画面上下の大きさ: {info['h']:.1f}")
            if info['is_persp']:
                col.label(text=f"視座からの距離: {info['dist']:.1f}")
            
            layout.separator()
            layout.operator(f"view3d.{PREFIX}_copy_info", text="情報をコピー (1行ずつペースト)", icon='COPYDOWN')
        else:
            box.label(text="3Dビューが見つかりません")
            
        layout.separator(factor=1.5)
        
        # Grid格子 設定 UI
        box_grid = layout.box()
        box_grid.label(text="Grid格子 設定", icon='GRID')
        
        row_preset = box_grid.row(align=True)
        row_preset.operator(f"view3d.{PREFIX}_set_grid_scale", text="10").scale_val = 10.0
        row_preset.operator(f"view3d.{PREFIX}_set_grid_scale", text="100").scale_val = 100.0
        
        row_custom = box_grid.row(align=True)
        row_custom.prop(props, "custom_grid_scale", text="")
        row_custom.operator(f"view3d.{PREFIX}_set_grid_scale", text="指定値にする").use_custom = True

class PT_generate(bpy.types.Panel):
    bl_idname = f"{PREFIX.upper()}_PT_generate"
    bl_label = "2. セット生成・制御"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_TITLE
    
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX}_props")
        
        row = layout.row(align=True)
        row.scale_y = 1.2
        row.operator(f"object.{PREFIX}_create_set", text="セットを作成", icon='MESH_MONKEY')
        row.operator(f"object.{PREFIX}_detach", text="アドオンから切り離し", icon='UNLINKED')
        
        layout.separator()
        
        # --- 形状・太さ・カラー設定 UI ---
        col_main = layout.column(align=True)
        col_main.enabled = not props.is_detached
        
        box_size = col_main.box()
        col_s = box_size.column(align=True)
        col_s.prop(props, "torus_radius")
        col_s.prop(props, "sight_length")
        
        col_main.separator()
        
        box_thick = col_main.box()
        col_t = box_thick.column(align=True)
        col_t.prop(props, "dummy_thickness")
        col_t.prop(props, "torus_thickness")
        col_t.prop(props, "sight_thickness")
        
        col_main.separator()
        
        col_color = col_main.column(align=True)
        col_color.label(text="ダミーの色:")
        col_color.prop(props, "color_dummy", text="")
        
        col_color.label(text="トーラスの色:")
        col_color.prop(props, "color_torus", text="")
        
        col_color.label(text="視線の色:")
        col_color.prop(props, "color_sight", text="")
        
        layout.separator()
        
        root = bpy.data.objects.get(OBJ_NAME_ROOT)
        if root and not props.is_detached:
            box_tf = layout.box()
            box_tf.label(text="設定値 (セット全体):", icon='ORIENTATION_GLOBAL')
            col_tf = box_tf.column(align=True)
            col_tf.prop(root, "location", text="位置 xyz")
            col_tf.prop(root, "rotation_euler", text="回転 xyz")
            col_tf.prop(root, "scale", text="サイズ xyz")
            
        if props.is_detached:
            layout.label(text="※切り離し済みのため操作不可", icon='INFO')
            
        layout.separator()
        
        # 初期化・設定コピー ボタン
        col_reset = layout.column(align=True)
        col_reset.operator(f"object.{PREFIX}_copy_settings", text="現在の設定を初期設定としてコピー", icon='COPYDOWN')
        col_reset.separator()
        col_reset.operator(f"object.{PREFIX}_reset_props", text="初期値に戻す", icon='LOOP_BACK')

class PT_system(bpy.types.Panel):
    bl_idname = f"{PREFIX.upper()}_PT_system"
    bl_label = "3. システム・リンク"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_TITLE
    
    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label(text="外部リンク", icon='URL')
        op = box.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_1, icon='HELP')
        op.url = LINK_URL_1
        layout.separator()
        layout.operator(f"wm.{PREFIX}_remove_addon", text="アドオン削除 (登録解除)", icon='CANCEL')

# ===================================
# 【登録・削除処理】
# ===================================
classes = [
    DummySight_Props,
    OT_reset_view,
    OT_update_info,
    OT_copy_info,
    OT_set_grid_scale,
    OT_create_set,
    OT_detach,
    OT_reset_props,
    OT_copy_settings,
    OT_open_url,
    OT_remove_addon,
    PT_view_info,
    PT_generate,
    PT_system
]

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, f"{PREFIX}_props", bpy.props.PointerProperty(type=DummySight_Props))

def unregister():
    if hasattr(bpy.types.Scene, f"{PREFIX}_props"): delattr(bpy.types.Scene, f"{PREFIX}_props")
    for c in reversed(classes):
        try: bpy.utils.unregister_class(c)
        except: pass

if __name__ == "__main__":
    try: unregister()
    except: pass
    register()
'''
# ---------------------------------

# =========================================================
# 内部処理 (リスト化)
# =========================================================
RAW_CODES = [
    (TITLE_1, CODE_1), (TITLE_2, CODE_2), (TITLE_3, CODE_3),
    (TITLE_4, CODE_4), (TITLE_5, CODE_5), (TITLE_6, CODE_6),
    (TITLE_7, CODE_7), (TITLE_8, CODE_8), (TITLE_9, CODE_9),
    (TITLE_10, CODE_10)
]

STORED_CODES = []

# コードが入力されているものだけをリストに登録
for i, (title_str, code_str) in enumerate(RAW_CODES):
    if code_str and len(code_str.strip()) > 0:
        addon_title = title_str if title_str.strip() else f"Unnamed_Addon_Slot_{i+1}"
        STORED_CODES.append({
            "name": addon_title,
            "code": code_str,
            "slot": i + 1
        })

# =========================================================
# オペレーター (テキストデータ化 & エディタ表示)
# =========================================================
class ZIONAD_OT_OutputToTextEditor(Operator):
    bl_idname = f"{PREFIX_VAL}.output_to_text_editor"
    bl_label = "テキストエディタで開く"
    bl_description = "このコードをテキストデータとして生成し、テキストエディタで開きます"
    
    code_index: IntProperty()
    
    def execute(self, context):
        if self.code_index < 0 or self.code_index >= len(STORED_CODES):
            self.report({'ERROR'}, "指定されたコードが見つかりません。")
            return {'CANCELLED'}
            
        item = STORED_CODES[self.code_index]
        
        # タイトルに使えない文字(空白等)をアンダースコアにしてファイル名にする
        safe_name = "".join([c if c.isalnum() else "_" for c in item["name"]])
        text_name = f"{safe_name}.py"
        code_text = item["code"]
        
        if text_name in bpy.data.texts:
            text_data = bpy.data.texts[text_name]
        else:
            text_data = bpy.data.texts.new(name=text_name)
            text_data.use_fake_user = True
            text_data.write(code_text.strip())
            
        editor_found = False
        for win in context.window_manager.windows:
            for area in win.screen.areas:
                if area.type == 'TEXT_EDITOR':
                    area.spaces.active.text = text_data
                    editor_found = True
                    break
            if editor_found:
                break
                
        if editor_found:
            self.report({'INFO'}, f"'{text_data.name}' をテキストエディタに表示しました。")
        else:
            self.report({'INFO'}, f"'{text_data.name}' を生成しました。画面をText Editorに切り替えて確認してください。")
            
        return {'FINISHED'}

class ZIONAD_OT_RemoveAddon(Operator):
    bl_idname = f"{PREFIX_VAL}.remove_addon"
    bl_label = "Unregister Addon"
    bl_description = "このコンテナアドオンの登録を解除し、UIから消去します"
    
    def execute(self, context):
        bpy.app.timers.register(unregister, first_interval=0.01)
        return {'FINISHED'}

# =========================================================
# UI パネル
# =========================================================
class ZIONAD_PT_ContainerMain(Panel):
    bl_label = "アドオン・コード・コンテナ"
    bl_idname = "ZIONAD_PT_container_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_TITLE

    def draw(self, context):
        layout = self.layout
        
        layout.label(text="内蔵されているコード一覧:")
        box = layout.box()
        
        if not STORED_CODES:
            box.label(text="※ コードが登録されていません", icon='INFO')
            box.label(text="ソースコードを編集して追加してください")
        else:
            for i, item in enumerate(STORED_CODES):
                row = box.row(align=True)
                row.label(text=f"[{item['slot']}] {item['name']}", icon='FILE_SCRIPT')
                op = row.operator(ZIONAD_OT_OutputToTextEditor.bl_idname, text="開く", icon='TEXT')
                op.code_index = i

class ZIONAD_PT_ContainerUninstall(Panel):
    bl_label = "アドオン削除"
    bl_idname = "ZIONAD_PT_container_uninstall"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_TITLE
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label(text="このコンテナアドオンを消去します", icon='ERROR')
        box.operator(ZIONAD_OT_RemoveAddon.bl_idname, icon='CANCEL')

# =========================================================
# 登録・解除
# =========================================================
classes = (
    ZIONAD_OT_OutputToTextEditor,
    ZIONAD_OT_RemoveAddon,
    ZIONAD_PT_ContainerMain,
    ZIONAD_PT_ContainerUninstall,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)

def unregister():
    for cls in reversed(classes):
        try:
            bpy.utils.unregister_class(cls)
        except:
            pass

if __name__ == "__main__":
    register()

import bpy
import re
import base64
import os
from bpy.types import Operator, Panel, PropertyGroup, UIList
from bpy.props import StringProperty, PointerProperty, CollectionProperty, IntProperty

# =========================================================
# 【タイトル・接頭辞の定義】
# =========================================================
ADDON_TITLE = "Code Matrix Container"
PREFIX = "code_matrix_container"

# =========================================================
# アドオン宣言
# =========================================================
bl_info = {
    "name": ADDON_TITLE,
    "author": "AI",
    "version": (3, 3, 4),
    "blender": (5, 0, 0),
    "location": f"View3D > Sidebar > {ADDON_TITLE}",
    "description": "自己複製機能を持つアドオンコード管理コンテナ",
    "category": "Development",
}

# --- EMBEDDED DATA START ---
# ここに内蔵コードが自動的に保存・展開されます (Base64辞書形式)
EMBEDDED_CODES = {}
# --- EMBEDDED DATA END ---

# =========================================================
# プロパティ・データ構造
# =========================================================
class ZIONAD_CodeItem(PropertyGroup):
    name: StringProperty(name="アドオン別名", default="New Addon")
    text_ptr: PointerProperty(name="Text Data", type=bpy.types.Text)

class ZIONAD_ContainerProperties(PropertyGroup):
    items: CollectionProperty(type=ZIONAD_CodeItem)
    active_index: IntProperty(default=0)

# =========================================================
# 自己複製・出力オペレーター
# =========================================================
class ZIONAD_OT_ExportSelf(Operator):
    bl_idname = f"{PREFIX}.export_self"
    bl_label = "コンテナ自身のコードを出力"
    bl_description = "コンテナ自身のコードを、現在の一時リストを内蔵させた状態でテキストエディタに出力します"
    
    def execute(self, context):
        source_code = ""
        
        # 1. 自身のソースコードを取得 (Generatedファイルは絶対に除外する)
        if __file__ in bpy.data.texts and not __file__.endswith("_Generated.py"):
            source_code = bpy.data.texts[__file__].as_string()
            
        elif os.path.exists(__file__):
            try:
                with open(__file__, 'r', encoding='utf-8') as f:
                    source_code = f.read()
            except Exception:
                pass
                
        # テキストエディタからの取得
        if not source_code:
            for text in bpy.data.texts:
                if "Generated" in text.name:  # 壊れた残骸を無視する安全装置
                    continue
                content = text.as_string()
                if "ZIONAD_ContainerProperties" in content and "ADDON_TITLE =" in content:
                    source_code = content
                    break
                    
        if not source_code and context.space_data and context.space_data.type == 'TEXT_EDITOR':
            active_text = context.space_data.text
            if active_text and "Generated" not in active_text.name:
                content = active_text.as_string()
                if "ZIONAD_ContainerProperties" in content:
                    source_code = content

        if not source_code:
            self.report({'ERROR'}, "自身のソースコードを取得できませんでした。")
            return {'CANCELLED'}
        
        # 2. 内蔵コードをBase64化して辞書データ文字列を作成
        props = context.scene.zionad_container_props
        embedded_dict = {}
        for item in props.items:
            if item.text_ptr:
                raw_text = item.text_ptr.as_string()
                encoded = base64.b64encode(raw_text.encode('utf-8')).decode('utf-8')
                embedded_dict[item.name] = encoded
                
        dict_str = "{\n"
        for k, v in embedded_dict.items():
            dict_str += f'    "{k}": "{v}",\n'
        dict_str += "}"
        
        # 3. マーカー位置を検索して安全に置換 
        # (chr(35)は '#' のこと。コード内で直接マーカー文字列を作らないための工夫)
        start_marker = chr(35) + " --- EMBEDDED DATA START ---"
        end_marker = chr(35) + " --- EMBEDDED DATA END ---"
        
        start_idx = source_code.find(start_marker)
        end_idx = source_code.find(end_marker)
        
        if start_idx == -1 or end_idx == -1:
            self.report({'ERROR'}, "マーカーが見つかりません。")
            return {'CANCELLED'}
            
        try:
            start_idx += len(start_marker)
            
            new_source = (
                source_code[:start_idx]
                + "\n# ここに内蔵コードが自動的に保存・展開されます (Base64辞書形式)\nEMBEDDED_CODES = "
                + dict_str
                + "\n"
                + source_code[end_idx:]
            )
        except Exception as e:
            self.report({'ERROR'}, f"コードの再構築に失敗しました: {e}")
            return {'CANCELLED'}
        
        # 4. 新しいテキストデータとして出力
        out_name = f"{ADDON_TITLE}_Generated.py"
        out_text = bpy.data.texts.get(out_name)
        if not out_text:
            out_text = bpy.data.texts.new(out_name)
        out_text.clear()
        out_text.write(new_source)
        
        # 5. テキストエディタに表示
        for win in context.window_manager.windows:
            for area in win.screen.areas:
                if area.type == 'TEXT_EDITOR':
                    area.spaces.active.text = out_text
                    break
                    
        self.report({'INFO'}, f"コンテナ自身を出力しました: '{out_name}'")
        return {'FINISHED'}

# =========================================================
# 内蔵コード個別出力オペレーター
# =========================================================
class ZIONAD_OT_ExportEmbedded(Operator):
    bl_idname = f"{PREFIX}.export_embedded"
    bl_label = "内蔵コードを抽出"
    bl_description = "アドオン内に静的に定義されている指定の内蔵コードをテキストエディタにデコード出力します"
    
    code_key: StringProperty()
    
    def execute(self, context):
        if self.code_key not in EMBEDDED_CODES:
            self.report({'ERROR'}, f"指定されたキーが見つかりません: {self.code_key}")
            return {'CANCELLED'}
        
        b64_text = EMBEDDED_CODES[self.code_key]
        try:
            decoded = base64.b64decode(b64_text).decode('utf-8')
        except Exception as e:
            self.report({'ERROR'}, f"デコードに失敗しました: {e}")
            return {'CANCELLED'}
            
        out_name = f"{self.code_key}.py"
        out_text = bpy.data.texts.get(out_name)
        if not out_text:
            out_text = bpy.data.texts.new(out_name)
        out_text.clear()
        out_text.write(decoded)
        
        # テキストエディタに表示
        for win in context.window_manager.windows:
            for area in win.screen.areas:
                if area.type == 'TEXT_EDITOR':
                    area.spaces.active.text = out_text
                    break
                    
        self.report({'INFO'}, f"内蔵コード '{out_name}' を抽出しました。")
        return {'FINISHED'}

# =========================================================
# 各種オペレーター
# =========================================================
class ZIONAD_OT_AddFromClipboard(Operator):
    bl_idname = f"{PREFIX}.add_from_clipboard"
    bl_label = "クリップボードから追加"
    
    def execute(self, context):
        text = context.window_manager.clipboard
        if not text or "bl_info" not in text:
            self.report({'WARNING'}, "クリップボードに有効なアドオンコードがありません。")
            return {'CANCELLED'}
        
        match = re.search(r'["\']name["\']\s*:\s*["\']([^"\']+)["\']', text)
        base_name = match.group(1) if match else "新規ペーストコード"
        
        text_name = base_name
        count = 1
        while f"{text_name}.py" in bpy.data.texts:
            count += 1
            text_name = f"{base_name} ({count})"
            
        text_data = bpy.data.texts.new(name=f"{text_name}.py")
        text_data.use_fake_user = True
        text_data.write(text)
        
        props = context.scene.zionad_container_props
        item = props.items.add()
        item.name = text_name
        item.text_ptr = text_data
        props.active_index = len(props.items) - 1
        
        return {'FINISHED'}

class ZIONAD_OT_OutputToTextEditor(Operator):
    bl_idname = f"{PREFIX}.output_to_text_editor"
    bl_label = "テキストエディタで開く"
    
    def execute(self, context):
        props = context.scene.zionad_container_props
        if not props.items: return {'CANCELLED'}
        props.active_index = max(0, min(props.active_index, len(props.items) - 1))
        item = props.items[props.active_index]
        if not item.text_ptr: return {'CANCELLED'}
        
        for win in context.window_manager.windows:
            for area in win.screen.areas:
                if area.type == 'TEXT_EDITOR':
                    area.spaces.active.text = item.text_ptr
                    break
        return {'FINISHED'}

class ZIONAD_OT_RemoveCode(Operator):
    bl_idname = f"{PREFIX}.remove_code"
    bl_label = "コードを削除"
    
    def execute(self, context):
        props = context.scene.zionad_container_props
        if not props.items: return {'CANCELLED'}
        props.active_index = max(0, min(props.active_index, len(props.items) - 1))
        item = props.items[props.active_index]
        if item.text_ptr:
            bpy.data.texts.remove(item.text_ptr)
        props.items.remove(props.active_index)
        if props.items:
            props.active_index = max(0, min(props.active_index, len(props.items) - 1))
        return {'FINISHED'}

class ZIONAD_OT_RemoveAddon(Operator):
    bl_idname = f"{PREFIX}.remove_addon"
    bl_label = "アドオンの登録解除"
    
    def execute(self, context):
        bpy.app.timers.register(unregister, first_interval=0.01)
        return {'FINISHED'}

# =========================================================
# UIリスト・パネル類
# =========================================================
class ZIONAD_UL_CodeList(UIList):
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
        layout.label(text=item.name, icon='FILE_SCRIPT' if item.text_ptr else 'ERROR')

# 【コンテナメインパネル】
class ZIONAD_PT_ContainerMain(Panel):
    bl_label = ADDON_TITLE
    bl_idname = f"{PREFIX}_PT_container_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_TITLE

    def draw(self, context):
        layout = self.layout
        props = context.scene.zionad_container_props
        
        # 1. コンテナの出力
        box_out = layout.box()
        box_out.label(text="コンテナ出力:", icon='EXPORT')
        col_out = box_out.column(align=True)
        col_out.scale_y = 1.3
        col_out.operator(ZIONAD_OT_ExportSelf.bl_idname, icon='TEXT')
        box_out.label(text="※ 空の状態でも正常に機能する複製コードが生成されます", icon='INFO')
        
        layout.separator()

        # 2. 一時コンテナ内のコード管理リスト
        box_container = layout.box()
        box_container.label(text="一時コンテナ内リスト:", icon='FILE_SCRIPT')
        box_container.operator(ZIONAD_OT_AddFromClipboard.bl_idname, icon='PASTEDOWN')
        
        row_list = box_container.row()
        row_list.template_list("ZIONAD_UL_CodeList", "", props, "items", props, "active_index", rows=5)
        row_list.operator(ZIONAD_OT_RemoveCode.bl_idname, text="", icon='REMOVE')
        
        if len(props.items) > 0:
            safe_index = max(0, min(props.active_index, len(props.items) - 1))
            item = props.items[safe_index]
            box_container.prop(item, "name", text="管理名")
            col_view = box_container.column(align=True)
            col_view.scale_y = 1.1
            col_view.operator(ZIONAD_OT_OutputToTextEditor.bl_idname, icon='TEXT')

        layout.separator()

        # 3. 静的に埋め込まれているアドオンコード一覧
        box_embedded = layout.box()
        box_embedded.label(text="静的内蔵コード一覧:", icon='BOOKMARKS')
        if EMBEDDED_CODES:
            for key in EMBEDDED_CODES.keys():
                row = box_embedded.row(align=True)
                row.label(text=key, icon='FILE_BLEND')
                op = row.operator(ZIONAD_OT_ExportEmbedded.bl_idname, text="テキスト出力", icon='EXPORT')
                op.code_key = key
        else:
            box_embedded.label(text="内蔵されているコードはありません", icon='INFO')

# 【アドオン削除パネル】
class ZIONAD_PT_Uninstall(Panel):
    bl_label = "アドオン削除"
    bl_idname = f"{PREFIX}_PT_uninstall"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_TITLE
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label(text="アドオンの登録解除:", icon='ERROR')
        box.operator(ZIONAD_OT_RemoveAddon.bl_idname, icon='CANCEL')

# =========================================================
# 内蔵コードの自動復元 (起動時展開用)
# =========================================================
def restore_embedded_codes():
    if not hasattr(bpy.types.Scene, 'zionad_container_props'):
        return 0.5 # Props生成待ちリトライ
        
    props = bpy.context.scene.zionad_container_props
    for title, b64_text in EMBEDDED_CODES.items():
        if any(item.name == title for item in props.items):
            continue
        try:
            decoded = base64.b64decode(b64_text).decode('utf-8')
            text_data = bpy.data.texts.get(f"{title}.py")
            if not text_data:
                text_data = bpy.data.texts.new(name=f"{title}.py")
                text_data.write(decoded)
                
            item = props.items.add()
            item.name = title
            item.text_ptr = text_data
        except Exception:
            pass
    return None

# =========================================================
# 登録・解除
# =========================================================
classes = (
    ZIONAD_CodeItem,
    ZIONAD_ContainerProperties,
    ZIONAD_OT_ExportSelf,
    ZIONAD_OT_ExportEmbedded,
    ZIONAD_OT_AddFromClipboard,
    ZIONAD_OT_OutputToTextEditor,
    ZIONAD_OT_RemoveCode,
    ZIONAD_OT_RemoveAddon,
    ZIONAD_UL_CodeList,
    ZIONAD_PT_ContainerMain,
    ZIONAD_PT_Uninstall,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Scene.zionad_container_props = PointerProperty(type=ZIONAD_ContainerProperties)
    
    # 内蔵コードがある場合、起動後に自動展開する
    bpy.app.timers.register(restore_embedded_codes, first_interval=0.5)

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

if __name__ == "__main__":
    register()