Sidebar (Nパネル)", "description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション", "category": "Object", } import bpy import webbrowser import math import bmesh import mathutils # ========================================================================= # 【基本設定】 # ========================================================================= TAB_NAME = "Cam 20260706" PREFIX_NAME = "Can001_addon6" INIT_COLOR = (0.0, 1.0, 0.5) IS_UPDATING = False # ========================================================================= # 【マトリクス・座標変換ヘルパー】 # ========================================================================= def get_matrix(loc, rot): euler = mathutils.Euler(rot, 'XYZ') mat_rot = euler.to_matrix().to_4x4() mat_loc = mathutils.Matrix.Translation(loc) return mat_loc @ mat_rot def decompose"> Sidebar (Nパネル)", "description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション", "category": "Object", } import bpy import webbrowser import math import bmesh import mathutils # ========================================================================= # 【基本設定】 # ========================================================================= TAB_NAME = "Cam 20260706" PREFIX_NAME = "Can001_addon6" INIT_COLOR = (0.0, 1.0, 0.5) IS_UPDATING = False # ========================================================================= # 【マトリクス・座標変換ヘルパー】 # ========================================================================= def get_matrix(loc, rot): euler = mathutils.Euler(rot, 'XYZ') mat_rot = euler.to_matrix().to_4x4() mat_loc = mathutils.Matrix.Translation(loc) return mat_loc @ mat_rot def decompose"> Sidebar (Nパネル)", "description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション", "category": "Object", } import bpy import webbrowser import math import bmesh import mathutils # ========================================================================= # 【基本設定】 # ========================================================================= TAB_NAME = "Cam 20260706" PREFIX_NAME = "Can001_addon6" INIT_COLOR = (0.0, 1.0, 0.5) IS_UPDATING = False # ========================================================================= # 【マトリクス・座標変換ヘルパー】 # ========================================================================= def get_matrix(loc, rot): euler = mathutils.Euler(rot, 'XYZ') mat_rot = euler.to_matrix().to_4x4() mat_loc = mathutils.Matrix.Translation(loc) return mat_loc @ mat_rot def decompose">

rapture_20260707002856.png

テスト

bl_info = {
    "name": "Polygon Torus Generator Pro (Realtime & Detach)",
    "author": "Your Name",
    "version": (7, 1),
    "blender": (4, 2, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション",
    "category": "Object",
}

import bpy
import webbrowser
import math
import bmesh
import mathutils

# =========================================================================
# 【基本設定】
# =========================================================================

TAB_NAME    = "Cam 20260706"
PREFIX_NAME = "Can001_addon6"

INIT_COLOR = (0.0, 1.0, 0.5)
IS_UPDATING = False

# =========================================================================
# 【マトリクス・座標変換ヘルパー】
# =========================================================================
def get_matrix(loc, rot):
    euler = mathutils.Euler(rot, 'XYZ')
    mat_rot = euler.to_matrix().to_4x4()
    mat_loc = mathutils.Matrix.Translation(loc)
    return mat_loc @ mat_rot

def decompose_matrix(matrix):
    loc = matrix.to_translation()
    rot = matrix.to_euler('XYZ')
    return loc, rot

def get_world_verts(verts_local, loc, rot):
    mat = get_matrix(loc, rot)
    verts_world = []
    for v in verts_local:
        v_4d = mathutils.Vector((v[0], v[1], v[2], 1.0))
        v_world = (mat @ v_4d).to_3d()
        verts_world.append(tuple(v_world))
    return verts_world

# =========================================================================
# 【純粋数学メッシュ更新:既存オブジェクトの再描画用ヘルパー】
# =========================================================================
def get_torus_mesh_pure(n, r, thick):
    radius = r * math.sqrt(2) if n == 4 else r
    mesh = bpy.data.meshes.new("TorusMesh")
    bm = bmesh.new()
    
    major_segments = n
    minor_segments = 12
    
    verts = []
    for i in range(major_segments):
        theta = 2.0 * math.pi * i / major_segments
        cos_theta = math.cos(theta)
        sin_theta = math.sin(theta)
        
        ring = []
        for j in range(minor_segments):
            phi = 2.0 * math.pi * j / minor_segments
            cos_phi = math.cos(phi)
            sin_phi = math.sin(phi)
            
            x = (radius + thick * cos_phi) * cos_theta
            y = (radius + thick * cos_phi) * sin_theta
            z = thick * sin_phi
            
            ring.append(bm.verts.new((x, y, z)))
        verts.append(ring)
        
    for i in range(major_segments):
        next_i = (i + 1) % major_segments
        for j in range(minor_segments):
            next_j = (j + 1) % minor_segments
            
            v1 = verts[i][j]
            v2 = verts[next_i][j]
            v3 = verts[next_i][next_j]
            v4 = verts[i][next_j]
            
            try:
                bm.faces.new((v1, v2, v3, v4))
            except ValueError:
                pass 
                
    bm.to_mesh(mesh)
    bm.free()
    return mesh

def get_sphere_mesh_pure(r):
    mesh = bpy.data.meshes.new("SphereMesh")
    bm = bmesh.new()
    bmesh.ops.create_icosphere(bm, subdivisions=3, radius=r)
    bm.to_mesh(mesh)
    bm.free()
    return mesh

# =========================================================================
# 【ヘルパー関数:マテリアル・メッシュ再生成】
# =========================================================================
def get_or_create_mat(name, color, alpha=1.0):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name)
        mat.use_nodes = True
        
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    if bsdf:
        bsdf.inputs["Base Color"].default_value = (*color, 1.0)
        bsdf.inputs["Alpha"].default_value = alpha
        
    mat.diffuse_color = (*color, alpha)
        
    try: mat.surface_render_method = 'BLENDED' if alpha < 1.0 else 'DITHERED'
    except: pass
    try: mat.blend_method = 'BLEND' if alpha < 1.0 else 'OPAQUE'
    except: pass
    try: mat.shadow_method = 'NONE'
    except: pass
    
    return mat

def update_skirt_material(mat, color, alpha):
    if not mat.use_nodes:
        mat.use_nodes = True
        
    try: mat.blend_method = 'BLEND'
    except: pass
    try: mat.surface_render_method = 'BLENDED'
    except: pass
    try: mat.show_transparent_back = True
    except: pass
    try: mat.use_transparency_overlap = True
    except: pass
    try: mat.shadow_method = 'NONE'
    except: pass
    try: mat.use_backface_culling = False
    except: pass
    try: mat.use_backface_culling_shadow = False
    except: pass
    
    tree = mat.node_tree
    tree.nodes.clear()
    
    bsdf = tree.nodes.new('ShaderNodeBsdfPrincipled')
    out  = tree.nodes.new('ShaderNodeOutputMaterial')
    
    bsdf.inputs["Base Color"].default_value = (*color, 1.0)
    bsdf.inputs["Alpha"].default_value = alpha
    
    tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
    mat.diffuse_color = (*color, alpha)

def get_polygon_verts_local(n, r):
    verts = []
    radius = r * math.sqrt(2) if n == 4 else r
    phase = math.pi / 4 if n == 4 else 0.0
    for i in range(n):
        angle = phase + 2 * math.pi * i / n
        verts.append((radius * math.cos(angle), 0.0, radius * math.sin(angle)))
    return verts

def create_convex_mesh(verts, mesh_name):
    mesh = bpy.data.meshes.new(mesh_name)
    if len(verts) < 3: return mesh
    bm = bmesh.new()
    for v in verts:
        bm.verts.new(v)
    try: bmesh.ops.convex_hull(bm, input=bm.verts)
    except: pass
    bm.to_mesh(mesh)
    bm.free()
    return mesh

def create_cone_skirt_mesh(p_loc, va, mesh_name):
    mesh = bpy.data.meshes.new(mesh_name)
    if len(va) < 3: return mesh
    bm = bmesh.new()
    v_p = bm.verts.new(p_loc)
    v_as = [bm.verts.new(v) for v in va]
    for k in range(len(v_as)):
        v1 = v_as[k]
        v2 = v_as[(k + 1) % len(v_as)]
        try: bm.faces.new((v_p, v2, v1))
        except ValueError: pass
    bm.to_mesh(mesh)
    bm.free()
    return mesh

def create_frustum_skirt_mesh(p_loc, vc, normal_a, a_loc, mesh_name):
    mesh = bpy.data.meshes.new(mesh_name)
    if len(vc) < 3: return mesh
    bm = bmesh.new()
    p = mathutils.Vector(p_loc)
    a_l = mathutils.Vector(a_loc)
    
    va_projected = []
    for v in vc:
        c_k = mathutils.Vector(v)
        dir_v = c_k - p
        denom = dir_v.dot(normal_a)
        if abs(denom) > 1e-6:
            t = (a_l - p).dot(normal_a) / denom
            a_k = p + t * dir_v
        else:
            a_k = c_k
        va_projected.append(tuple(a_k))
        
    v_cs = [bm.verts.new(v) for v in vc]
    v_aps = [bm.verts.new(v) for v in va_projected]
    
    for k in range(len(vc)):
        next_k = (k + 1) % len(vc)
        try: bm.faces.new((v_aps[k], v_cs[k], v_cs[next_k], v_aps[next_k]))
        except ValueError: pass
            
    bm.to_mesh(mesh)
    bm.free()
    return mesh

# =========================================================================
# 【数学・幾何学ロジック (シミュレーション計算)】
# =========================================================================
def get_projected_verts_mode1(verts_a, verts_b, y_c):
    pts = []
    for pa in verts_a:
        for pb in verts_b:
            dy = pb[1] - pa[1]
            if abs(dy) < 0.0001: continue
            t = (y_c - pa[1]) / dy
            x = pa[0] + t * (pb[0] - pa[0])
            z = pa[2] + t * (pb[2] - pa[2])
            pts.append((x, y_c, z))
    return pts

def get_projected_verts_mode2(p_loc, verts_a, verts_b, y_c):
    px, py, pz = p_loc
    def project_polygon(verts):
        poly2d = []
        for vx, vy, vz in verts:
            if abs(vy - py) < 1e-6: return [] 
            t = (y_c - py) / (vy - py)
            if t < 0: return [] 
            x = px + t * (vx - px)
            z = pz + t * (vz - pz)
            poly2d.append((x, z))
        return poly2d

    def make_ccw(poly):
        if len(poly) < 3: return poly
        area = sum(poly[i][0]*poly[(i+1)%len(poly)][1] - poly[(i+1)%len(poly)][0]*poly[i][1] for i in range(len(poly)))
        return poly[::-1] if area < 0 else poly

    def sutherland_hodgman(subject, clip):
        def inside(p, cp1, cp2):
            return (cp2[0] - cp1[0]) * (p[1] - cp1[1]) - (cp2[1] - cp1[1]) * (p[0] - cp1[0]) >= -1e-8
        def compute_intersection(cp1, cp2, s, e):
            dc, dp = [cp1[0] - cp2[0], cp1[1] - cp2[1]], [s[0] - e[0], s[1] - e[1]]
            n1, n2 = cp1[0] * cp2[1] - cp1[1] * cp2[0], s[0] * e[1] - s[1] * e[0]
            det = dc[0] * dp[1] - dc[1] * dp[0]
            if abs(det) < 1e-8: return s
            n3 = 1.0 / det
            return ((n1*dp[0] - n2*dc[0]) * n3, (n1*dp[1] - n2*dc[1]) * n3)

        out_poly = subject
        if not out_poly or not clip: return []
        cp1 = clip[-1]
        for cp2 in clip:
            in_poly = out_poly
            out_poly = []
            if not in_poly: break
            s = in_poly[-1]
            for e in in_poly:
                if inside(e, cp1, cp2):
                    if not inside(s, cp1, cp2):
                        out_poly.append(compute_intersection(cp1, cp2, s, e))
                    out_poly.append(e)
                elif inside(s, cp1, cp2):
                    out_poly.append(compute_intersection(cp1, cp2, s, e))
                s = e
            cp1 = cp2
        return out_poly

    poly_a = project_polygon(verts_a)
    poly_b = project_polygon(verts_b)
    if not poly_a or not poly_b: return []
    intersect_poly2d = sutherland_hodgman(make_ccw(poly_a), make_ccw(poly_b))
    return [(x, y_c, z) for (x, z) in intersect_poly2d]

# =========================================================================
# 【リアルタイム更新関数 (投影シミュレーション用)】
# =========================================================================
def update_proj(self, context):
    global IS_UPDATING
    if IS_UPDATING: return
    
    props = context.scene.poly_torus_props
    obj_a = bpy.data.objects.get("Proj_Plane_A")
    obj_b = bpy.data.objects.get("Proj_Plane_B")
    obj_c = bpy.data.objects.get("Proj_Plane_C")
    obj_p = bpy.data.objects.get("Proj_Point_P")
    obj_skirt_a = bpy.data.objects.get("Proj_Skirt_A")
    obj_skirt_c = bpy.data.objects.get("Proj_Skirt_C")
    
    if not (obj_a and obj_b and obj_c and obj_p and obj_skirt_a and obj_skirt_c): return
    
    IS_UPDATING = True
    try:
        n_a, r_a, thick_a = props.proj_n_a, props.proj_r_a, props.proj_thick_a
        n_b, r_b, thick_b = props.proj_n_b, props.proj_r_b, props.proj_thick_b
        y_c = props.proj_y_c
        
        def update_torus(obj, n, r, thick, loc, rot, color):
            mesh = get_torus_mesh_pure(n, r, thick)
            old_m, old_mat = obj.data, obj.active_material
            obj.data = mesh
            if old_mat: 
                obj.data.materials.append(old_mat)
                if old_mat.use_nodes:
                    bsdf = old_mat.node_tree.nodes.get("Principled BSDF")
                    if bsdf: bsdf.inputs["Base Color"].default_value = (*color, 1.0)
                old_mat.diffuse_color = (*color, 1.0)
            
            mat_base = mathutils.Euler((math.pi / 2, math.pi / 4 if n == 4 else 0.0, 0.0)).to_matrix().to_4x4()
            mat_user = get_matrix(loc, rot)
            mat_final = mat_user @ mat_base
            
            obj.location = mat_final.to_translation()
            obj.rotation_euler = mat_final.to_euler('XYZ')
            if old_m: bpy.data.meshes.remove(old_m)

        update_torus(obj_a, n_a, r_a, thick_a, props.proj_a_loc, props.proj_a_rot, props.proj_color_a)
        update_torus(obj_b, n_b, r_b, thick_b, props.proj_b_loc, props.proj_b_rot, props.proj_color_b)

        if props.proj_mode == 'MODE2':
            obj_p.hide_viewport = False
            mesh_p = get_sphere_mesh_pure(props.proj_p_radius)
            old_m_p = obj_p.data
            mat_p = obj_p.active_material
            obj_p.data = mesh_p
            if mat_p:
                obj_p.data.materials.append(mat_p)
                if mat_p.use_nodes:
                    bsdf = mat_p.node_tree.nodes.get("Principled BSDF")
                    if bsdf:
                        bsdf.inputs["Base Color"].default_value = (*props.proj_color_p, 1.0)
                        bsdf.inputs["Alpha"].default_value = props.proj_alpha_p
                mat_p.diffuse_color = (*props.proj_color_p, props.proj_alpha_p)
                
                try: mat_p.surface_render_method = 'BLENDED' if props.proj_alpha_p < 1.0 else 'DITHERED'
                except: pass
                try: mat_p.blend_method = 'BLEND' if props.proj_alpha_p < 1.0 else 'OPAQUE'
                except: pass
                
            if old_m_p: bpy.data.meshes.remove(old_m_p)
            
            mat_p_user = get_matrix(props.proj_p_loc, props.proj_p_rot)
            obj_p.location = mat_p_user.to_translation()
            obj_p.rotation_euler = mat_p_user.to_euler('XYZ')
        else:
            obj_p.hide_viewport = True

        va = get_world_verts(get_polygon_verts_local(n_a, r_a), props.proj_a_loc, props.proj_a_rot)
        vb = get_world_verts(get_polygon_verts_local(n_b, r_b), props.proj_b_loc, props.proj_b_rot)
        
        if props.proj_mode == 'MODE1':
            vc = get_projected_verts_mode1(va, vb, y_c)
            color_c = props.proj_color_c_m1
        else:
            vc = get_projected_verts_mode2(props.proj_p_loc, va, vb, y_c)
            color_c = props.proj_color_c_m2
        
        mesh_c = create_convex_mesh(vc, "Mesh_C")
        old_mc, old_mat_c = obj_c.data, obj_c.active_material
        obj_c.data = mesh_c
        
        obj_c.hide_viewport = not props.proj_show_c
        obj_c.hide_render = not props.proj_show_c
        
        if old_mat_c: obj_c.data.materials.append(old_mat_c)
        obj_c.location = (0, 0, 0)
        obj_c.rotation_euler = (0, 0, 0)
        if old_mc: bpy.data.meshes.remove(old_mc)
        
        if old_mat_c and old_mat_c.use_nodes:
            bsdf = old_mat_c.node_tree.nodes.get("Principled BSDF")
            if bsdf: bsdf.inputs["Base Color"].default_value = (*color_c, 1.0)
            old_mat_c.diffuse_color = (*color_c, 0.8)

        if props.proj_mode == 'MODE2' and props.proj_show_skirt:
            obj_skirt_a.hide_viewport = False
            obj_skirt_c.hide_viewport = False
            
            mat_a_rot = mathutils.Euler(props.proj_a_rot, 'XYZ').to_matrix()
            normal_a = mat_a_rot @ mathutils.Vector((0.0, 1.0, 0.0))
            normal_a.normalize()
            
            mesh_skirt_a = create_cone_skirt_mesh(props.proj_p_loc, va, "Mesh_Skirt_A")
            old_m_skirt_a = obj_skirt_a.data
            mat_skirt_a = obj_skirt_a.active_material
            obj_skirt_a.data = mesh_skirt_a
            if mat_skirt_a:
                obj_skirt_a.data.materials.append(mat_skirt_a)
                update_skirt_material(mat_skirt_a, props.proj_color_skirt_a, props.proj_alpha_skirt_a)
            if old_m_skirt_a: bpy.data.meshes.remove(old_m_skirt_a)
            obj_skirt_a.location = (0, 0, 0)
            obj_skirt_a.rotation_euler = (0, 0, 0)
            
            mesh_skirt_c = create_frustum_skirt_mesh(props.proj_p_loc, vc, normal_a, props.proj_a_loc, "Mesh_Skirt_C")
            old_m_skirt_c = obj_skirt_c.data
            mat_skirt_c = obj_skirt_c.active_material
            obj_skirt_c.data = mesh_skirt_c
            if mat_skirt_c:
                obj_skirt_c.data.materials.append(mat_skirt_c)
                update_skirt_material(mat_skirt_c, props.proj_color_skirt_c, props.proj_alpha_skirt_c)
            if old_m_skirt_c: bpy.data.meshes.remove(old_m_skirt_c)
            obj_skirt_c.location = (0, 0, 0)
            obj_skirt_c.rotation_euler = (0, 0, 0)
        else:
            obj_skirt_a.hide_viewport = True
            obj_skirt_c.hide_viewport = True

    finally:
        IS_UPDATING = False

# =========================================================================
# 【連動制御用アップデート関数】
# =========================================================================
def update_proj_link_p_a(self, context):
    props = context.scene.poly_torus_props
    if props.proj_link_p_a:
        mat_p = get_matrix(props.proj_p_loc, props.proj_p_rot)
        mat_a = get_matrix(props.proj_a_loc, props.proj_a_rot)
        mat_rel = mat_p.inverted() @ mat_a
        props.proj_p_a_rel_matrix = [val for row in mat_rel for val in row]

def update_proj_p_transform(self, context):
    global IS_UPDATING
    if IS_UPDATING: return
    props = context.scene.poly_torus_props
    if props.proj_link_p_a:
        IS_UPDATING = True
        try:
            mat_rel = mathutils.Matrix([props.proj_p_a_rel_matrix[i:i+4] for i in range(0, 16, 4)])
            mat_p = get_matrix(props.proj_p_loc, props.proj_p_rot)
            mat_a_new = mat_p @ mat_rel
            loc_a, rot_a = decompose_matrix(mat_a_new)
            props.proj_a_loc = loc_a
            props.proj_a_rot = rot_a
        finally:
            IS_UPDATING = False
    update_proj(self, context)

def update_proj_a_transform(self, context):
    global IS_UPDATING
    if IS_UPDATING: return
    props = context.scene.poly_torus_props
    if props.proj_link_p_a:
        mat_p = get_matrix(props.proj_p_loc, props.proj_p_rot)
        mat_a = get_matrix(props.proj_a_loc, props.proj_a_rot)
        mat_rel = mat_p.inverted() @ mat_a
        props.proj_p_a_rel_matrix = [val for row in mat_rel for val in row]
    update_proj(self, context)

# =========================================================================
# 【カメラ軌道制御用アップデート関数】
# =========================================================================
def update_cam_circle(self, context):
    cam = context.scene.camera
    if not cam: return
    curve_obj = None
    for const in cam.constraints:
        if const.type == 'FOLLOW_PATH':
            curve_obj = const.target
            break
    if curve_obj and "CamTrack_Circle" in curve_obj.name:
        curve_obj.location = self.cam_circle_center
        curve_obj.scale = (self.cam_circle_radius, self.cam_circle_radius, self.cam_circle_radius)
        curve_obj.rotation_euler = self.cam_circle_rotation # 傾きの反映

def update_cam_line(self, context):
    cam = context.scene.camera
    if not cam: return
    curve_obj = None
    for const in cam.constraints:
        if const.type == 'FOLLOW_PATH':
            curve_obj = const.target
            break
    if curve_obj and "CamTrack_Line" in curve_obj.name:
        curve_obj.location = (0, 0, 0)
        curve_obj.scale = (1, 1, 1)
        curve_obj.rotation_euler = (0, 0, 0)
        if len(curve_obj.data.splines) > 0:
            spline = curve_obj.data.splines[0]
            if spline.type == 'POLY' and len(spline.points) >= 2:
                spline.points[0].co = (self.cam_line_start[0], self.cam_line_start[1], self.cam_line_start[2], 1.0)
                spline.points[1].co = (self.cam_line_end[0], self.cam_line_end[1], self.cam_line_end[2], 1.0)

def update_cam_target(self, context):
    """注視点(ターゲットEmpty)の位置や追従設定を更新する"""
    cam = context.scene.camera
    if not cam: return
    
    target_obj = None
    for const in cam.constraints:
        if const.type == 'TRACK_TO':
            target_obj = const.target
            break
            
    if not target_obj: return
    
    # 既存の Copy Location 制約を一旦すべてクリア
    for c in target_obj.constraints:
        if c.type == 'COPY_LOCATION':
            target_obj.constraints.remove(c)
            
    mode = self.cam_target_mode
    
    if mode == 'OBJECT':
        if self.cam_target_obj1:
            c1 = target_obj.constraints.new(type='COPY_LOCATION')
            c1.target = self.cam_target_obj1
    elif mode == 'POINT':
        # どのオブジェクトにも追従せず、指定座標に移動
        target_obj.location = self.cam_target_loc
    elif mode == 'MIDPOINT':
        if self.cam_target_obj1:
            c1 = target_obj.constraints.new(type='COPY_LOCATION')
            c1.target = self.cam_target_obj1
            c1.influence = 1.0
        if self.cam_target_obj2:
            c2 = target_obj.constraints.new(type='COPY_LOCATION')
            c2.target = self.cam_target_obj2
            # 2つ目の影響度を0.5にすることで、1つ目と2つ目のちょうど中間になる
            c2.influence = 0.5

def cam_fov_get(self):
    cam = bpy.context.scene.camera
    if cam and cam.type == 'CAMERA':
        return math.degrees(cam.data.angle)
    return 50.0

def cam_fov_set(self, value):
    cam = bpy.context.scene.camera
    if cam and cam.type == 'CAMERA':
        cam.data.lens_unit = 'FOV'
        try:
            cam.data.angle = math.radians(value)
        except:
            pass

# =========================================================================
# 【ビュー(画面)の回転制御用コールバック】
# =========================================================================
def get_rv3d(context):
    if context.area and context.area.type == 'VIEW_3D':
        return context.area.spaces.active.region_3d
    for a in context.window.screen.areas:
        if a.type == 'VIEW_3D':
            return a.spaces.active.region_3d
    return None

def view_rotation_euler_get(self):
    rv3d = get_rv3d(bpy.context)
    if rv3d:
        return rv3d.view_rotation.to_euler('XYZ')
    return (0.0, 0.0, 0.0)

def view_rotation_euler_set(self, value):
    rv3d = get_rv3d(bpy.context)
    if rv3d:
        target_obj = bpy.context.active_object
        if target_obj:
            rv3d.view_location = target_obj.matrix_world.to_translation()
        euler = mathutils.Euler((value[0], value[1], value[2]), 'XYZ')
        rv3d.view_rotation = euler.to_quaternion()

# =========================================================================
# 【プロパティ定義】
# =========================================================================
class PolyTorusProperties(bpy.types.PropertyGroup):
    proj_mode: bpy.props.EnumProperty(
        name="シミュレーションの種類",
        items=[
            ('MODE1', "モード1 (広域・青)", "枠Aと枠Bを繋ぐすべての直線の範囲"),
            ('MODE2', "モード2 (点光源・ピンク)", "指定点PからA・Bを通り抜ける視界・範囲")
        ],
        default='MODE2',
        update=update_proj
    )
    
    proj_p_loc: bpy.props.FloatVectorProperty(name="P 位置", default=(0.0, -50.0, 0.0), update=update_proj_p_transform)
    proj_p_rot: bpy.props.FloatVectorProperty(name="P 回転", subtype='EULER', default=(0.0, 0.0, 0.0), update=update_proj_p_transform)
    proj_p_radius: bpy.props.FloatProperty(name="球の半径", default=1.0, min=0.1, update=update_proj)
    proj_color_p: bpy.props.FloatVectorProperty(name="P 色", subtype='COLOR', default=(1.0, 0.0, 0.0), min=0.0, max=1.0, update=update_proj)
    proj_alpha_p: bpy.props.FloatProperty(name="透明度", default=1.0, min=0.0, max=1.0, update=update_proj)

    proj_link_p_a: bpy.props.BoolProperty(name="球体Pと平面Aを完全連動 (位置・回転)", default=False, update=update_proj_link_p_a)
    proj_p_a_rel_matrix: bpy.props.FloatVectorProperty(name="相対トランスフォーム行列", size=16, default=[0.0]*16)

    proj_show_skirt: bpy.props.BoolProperty(name="光の道筋 (スカート) を表示", default=True, update=update_proj)
    proj_color_skirt_a: bpy.props.FloatVectorProperty(name="P-A色", subtype='COLOR', default=(1.0, 0.4, 0.1), min=0.0, max=1.0, update=update_proj)
    proj_alpha_skirt_a: bpy.props.FloatProperty(name="P-A透明度", default=0.4, min=0.0, max=1.0, update=update_proj)
    proj_color_skirt_c: bpy.props.FloatVectorProperty(name="A-C色", subtype='COLOR', default=(1.0, 0.8, 0.2), min=0.0, max=1.0, update=update_proj)
    proj_alpha_skirt_c: bpy.props.FloatProperty(name="A-C透明度", default=0.5, min=0.0, max=1.0, update=update_proj)

    proj_n_a: bpy.props.IntProperty(name="A 角数 (4=正)", default=4, min=3, max=256, update=update_proj)
    proj_r_a: bpy.props.FloatProperty(name="A サイズ", default=10.0, min=0.1, update=update_proj)
    proj_thick_a: bpy.props.FloatProperty(name="A 枠の太さ", default=0.2, min=0.01, update=update_proj)
    proj_a_loc: bpy.props.FloatVectorProperty(name="A 位置", default=(0.0, -5.0, 0.0), update=update_proj_a_transform)
    proj_a_rot: bpy.props.FloatVectorProperty(name="A 回転", subtype='EULER', default=(0.0, 0.0, 0.0), update=update_proj_a_transform)
    proj_color_a: bpy.props.FloatVectorProperty(name="A 色", subtype='COLOR', default=(0.0, 0.5, 1.0), min=0.0, max=1.0, update=update_proj)
    
    proj_n_b: bpy.props.IntProperty(name="B 角数 (4=正)", default=4, min=3, max=256, update=update_proj)
    proj_r_b: bpy.props.FloatProperty(name="B サイズ", default=10.0, min=0.1, update=update_proj)
    proj_thick_b: bpy.props.FloatProperty(name="B 枠の太さ", default=0.2, min=0.01, update=update_proj)
    proj_b_loc: bpy.props.FloatVectorProperty(name="B 位置", default=(0.0, 0.0, 0.0), update=update_proj)
    proj_b_rot: bpy.props.FloatVectorProperty(name="B 回転", subtype='EULER', default=(0.0, 0.0, 0.0), update=update_proj)
    proj_color_b: bpy.props.FloatVectorProperty(name="B 色", subtype='COLOR', default=(0.0, 1.0, 0.5), min=0.0, max=1.0, update=update_proj)
    
    proj_y_c: bpy.props.FloatProperty(name="C (投影先) Y座標", default=10.0, update=update_proj)
    proj_show_c: bpy.props.BoolProperty(name="平面C (壁・到達範囲) を表示する", default=True, update=update_proj)
    proj_color_c_m1: bpy.props.FloatVectorProperty(name="C 色(モード1)", subtype='COLOR', default=(0.0, 0.5, 1.0), min=0.0, max=1.0, update=update_proj)
    proj_color_c_m2: bpy.props.FloatVectorProperty(name="C 色(モード2)", subtype='COLOR', default=(1.0, 0.2, 0.8), min=0.0, max=1.0, update=update_proj)

    obj_rot_axis: bpy.props.EnumProperty(
        name="回転軸",
        items=[
            ('X', "X軸 (X)", "X軸を回転"),
            ('Y', "Y軸 (Y)", "Y軸を回転"),
            ('Z', "Z軸 (Z)", "Z軸を回転")
        ],
        default='Z'
    )
    
    view_rot_axis: bpy.props.EnumProperty(
        name="画面の回転軸",
        items=[
            ('X', "X軸 (X)", "ワールドX軸周りに回転"),
            ('Y', "Y軸 (Y)", "ワールドY軸周りに回転"),
            ('Z', "Z軸 (Z)", "ワールドZ軸周りに回転")
        ],
        default='Z'
    )
    
    view_rotation_euler: bpy.props.FloatVectorProperty(
        name="画面回転",
        subtype='EULER',
        unit='ROTATION',
        size=3,
        get=view_rotation_euler_get,
        set=view_rotation_euler_set
    )
    
    cam_circle_center: bpy.props.FloatVectorProperty(name="円の中心", default=(0,0,5), update=update_cam_circle)
    cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=15.0, min=0.1, update=update_cam_circle)
    cam_circle_rotation: bpy.props.FloatVectorProperty(name="円の傾き", subtype='EULER', default=(0,0,0), update=update_cam_circle)
    
    cam_line_start: bpy.props.FloatVectorProperty(name="始点", default=(-15,-15,5), update=update_cam_line)
    cam_line_end: bpy.props.FloatVectorProperty(name="終点", default=(15,-15,5), update=update_cam_line)
    
    cam_fov: bpy.props.FloatProperty(
        name="水平視野角 (度)",
        min=1.0,
        max=359.0,
        default=50.0,
        get=cam_fov_get,
        set=cam_fov_set
    )

    # --- [NEW] 注視点コントロール用プロパティ ---
    cam_target_mode: bpy.props.EnumProperty(
        name="注視点の指定方法",
        items=[
            ('OBJECT', "単一オブジェクト", "1つのオブジェクトを追従"),
            ('POINT', "指定座標 (手動)", "任意の座標を注視"),
            ('MIDPOINT', "2オブジェクトの中間", "2つのオブジェクトの中間を注視")
        ],
        default='OBJECT',
        update=update_cam_target
    )
    cam_target_obj1: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット1", update=update_cam_target)
    cam_target_obj2: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット2", update=update_cam_target)
    cam_target_loc: bpy.props.FloatVectorProperty(name="注視点 座標", default=(0,0,0), update=update_cam_target)

# =========================================================================
# 【オペレーター】
# =========================================================================
class POLY_OT_create_camera_rig(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_NAME.lower()}_create_camera_rig"
    bl_label = "カメラ軌道セットアップ"
    bl_options = {'REGISTER', 'UNDO'}
    
    rig_type: bpy.props.StringProperty(default='CIRCLE')
    
    def execute(self, context):
        target = context.active_object
        
        # ターゲット用のEmptyを生成 (既存のものを再利用せず新規で作成し紐付ける)
        bpy.ops.object.empty_add(type='PLAIN_AXES', location=(0,0,0))
        target_empty = context.active_object
        target_empty.name = "CameraTarget"
        
        # 基準となる位置 (選択オブジェクトがあればその位置、なければ原点)
        loc = target.matrix_world.to_translation() if target else mathutils.Vector((0,0,0))
        target_empty.location = loc
        
        curve_obj = None
        
        if self.rig_type == 'CIRCLE':
            bpy.ops.curve.primitive_nurbs_circle_add(radius=1.0, location=(0,0,0))
            curve_obj = context.active_object
            curve_obj.name = "CamTrack_Circle"
            
        elif self.rig_type == 'LINE':
            curve_data = bpy.data.curves.new('CamTrack_Line', type='CURVE')
            curve_data.dimensions = '3D'
            spline = curve_data.splines.new('POLY')
            spline.points.add(1)
            curve_obj = bpy.data.objects.new("CamTrack_Line", curve_data)
            context.collection.objects.link(curve_obj)
            
        else: # CURVE
            bpy.ops.curve.primitive_bezier_curve_add(radius=15.0, location=loc)
            curve_obj = context.active_object
            curve_obj.location.y -= 10.0
            curve_obj.location.z += 5.0
            curve_obj.scale = (2.0, 2.0, 2.0)
            curve_obj.name = "CamTrack_Curve"
            
        bpy.ops.object.camera_add(location=(0,0,0))
        cam_obj = context.active_object
        cam_obj.name = f"TrackingCamera_{self.rig_type}"
        cam_obj.rotation_mode = 'XYZ'
        
        const_path = cam_obj.constraints.new(type='FOLLOW_PATH')
        const_path.target = curve_obj
        const_path.use_curve_follow = False
        const_path.use_fixed_location = True
        const_path.offset_factor = 0.0
        
        const_track = cam_obj.constraints.new(type='TRACK_TO')
        const_track.target = target_empty
        const_track.track_axis = 'TRACK_NEGATIVE_Z'
        const_track.up_axis = 'UP_Y'
        
        context.scene.camera = cam_obj
        cam_obj.data.dof.use_dof = True
        cam_obj.data.dof.focus_object = target_empty
        cam_obj.data.dof.aperture_fstop = 1.8
        
        # --- プロパティの初期化 ---
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        
        # ターゲット設定の初期化
        props.cam_target_mode = 'OBJECT'
        if target and target.type not in ['CAMERA', 'LIGHT'] and target != target_empty:
            props.cam_target_obj1 = target
        else:
            props.cam_target_obj1 = None
        
        update_cam_target(props, context)
        
        # 軌道設定の初期化
        if self.rig_type == 'CIRCLE':
            props.cam_circle_center = (loc.x, loc.y, loc.z + 5.0)
            props.cam_circle_radius = 15.0
            props.cam_circle_rotation = (0, 0, 0)
        elif self.rig_type == 'LINE':
            props.cam_line_start = (loc.x - 15.0, loc.y - 15.0, loc.z + 5.0)
            props.cam_line_end = (loc.x + 15.0, loc.y - 15.0, loc.z + 5.0)
            
        for a in context.window.screen.areas:
            if a.type == 'VIEW_3D':
                a.spaces.active.region_3d.view_perspective = 'CAMERA'
                break
                
        self.report({'INFO'}, f"{self.rig_type}軌道のカメラセットアップが完了しました。")
        return {'FINISHED'}

class POLY_OT_reset_view_and_p(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_NAME.lower()}_reset_view_and_p"
    bl_label = "選択したオブジェクトを中心にPを配置 & ビュー初期化"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        global IS_UPDATING
        props = context.scene.poly_torus_props
        
        target_area = None
        target_region = None
        for a in context.window.screen.areas:
            if a.type == 'VIEW_3D':
                target_area = a
                for r in a.regions:
                    if r.type == 'WINDOW':
                        target_region = r
                        break
                break
                
        if target_area is None or target_region is None: return {'CANCELLED'}
        rv3d = target_area.spaces.active.region_3d
            
        target_obj = context.active_object
        if target_obj:
            center_loc = target_obj.matrix_world.to_translation()
            radius = target_obj.dimensions.length
            new_distance = max(radius * 1.5, 10.0)
        else:
            center_loc = rv3d.view_location.copy()
            new_distance = 30.0
            
        IS_UPDATING = True
        try:
            props.proj_p_loc = center_loc
            if props.proj_link_p_a:
                mat_rel = mathutils.Matrix([props.proj_p_a_rel_matrix[i:i+4] for i in range(0, 16, 4)])
                mat_p = get_matrix(props.proj_p_loc, props.proj_p_rot)
                mat_a_new = mat_p @ mat_rel
                loc_a, rot_a = decompose_matrix(mat_a_new)
                props.proj_a_loc = loc_a
                props.proj_a_rot = rot_a
            
            rv3d.view_location = center_loc
            rv3d.view_rotation = mathutils.Euler((math.radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
            rv3d.view_distance = new_distance
            rv3d.view_perspective = 'PERSP'
        finally:
            IS_UPDATING = False
            
        context.view_layer.update()
        update_proj(self, context)
        context.evaluated_depsgraph_get().update()
        return {'FINISHED'}

class POLY_OT_rotate_selected(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_NAME.lower()}_rotate_selected"
    bl_label = "選択オブジェクトを指定軸で回転"
    bl_options = {'REGISTER', 'UNDO'}
    angle: bpy.props.FloatProperty(name="Angle (Deg)", default=90.0)
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        axis = props.obj_rot_axis
        rad = math.radians(self.angle)
        for obj in context.selected_objects:
            if obj.rotation_mode != 'XYZ':
                obj.rotation_mode = 'XYZ'
            if axis == 'X': obj.rotation_euler.x += rad
            elif axis == 'Y': obj.rotation_euler.y += rad
            elif axis == 'Z': obj.rotation_euler.z += rad
        return {'FINISHED'}

class POLY_OT_rotate_view(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_NAME.lower()}_rotate_view"
    bl_label = "画面を指定軸で回転"
    bl_options = {'REGISTER', 'UNDO'}
    angle: bpy.props.FloatProperty(name="Angle (Deg)", default=15.0)
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        axis = props.view_rot_axis
        rad = math.radians(self.angle)
        
        rv3d = get_rv3d(context)
        if not rv3d: return {'CANCELLED'}
        target_obj = context.active_object
        if target_obj:
            rv3d.view_location = target_obj.matrix_world.to_translation()
        
        if axis == 'X': vec = (1.0, 0.0, 0.0)
        elif axis == 'Y': vec = (0.0, 1.0, 0.0)
        else: vec = (0.0, 0.0, 1.0)
            
        q_rot = mathutils.Quaternion(vec, rad)
        rv3d.view_rotation = q_rot @ rv3d.view_rotation
        return {'FINISHED'}

class POLY_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX_NAME.lower()}_open_url"
    bl_label = "URL"
    url: bpy.props.StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

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

# =========================================================================
# 【UIパネル】
# =========================================================================
class POLY_PT_main_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_main_panel"
    bl_label = "多角形ツール"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        
        # 1. 画面中央戻りボタン
        box_action = layout.box()
        box_action.label(text="ビューとオブジェクト操作:", icon='VIEW_CAMERA')
        col_action = box_action.column()
        col_action.scale_y = 1.3
        col_action.operator(
            f"view3d.{PREFIX_NAME.lower()}_reset_view_and_p", 
            text="選択中心にPを配置 & ビュー初期化", 
            icon='ZOOM_ALL'
        )
        layout.separator(factor=1.5)

        # 2. 選択オブジェクトの回転操作
        box_rot = layout.box()
        box_rot.label(text="選択オブジェクトの回転:", icon='ORIENTATION_GIMBAL')
        box_rot.prop(props, "obj_rot_axis", expand=True)
        
        target_obj = context.active_object
        if target_obj:
            axis = props.obj_rot_axis
            col_rot = box_rot.column(align=True)
            if axis == 'X': col_rot.prop(target_obj, "rotation_euler", index=0, text="X 回転角度")
            elif axis == 'Y': col_rot.prop(target_obj, "rotation_euler", index=1, text="Y 回転角度")
            elif axis == 'Z': col_rot.prop(target_obj, "rotation_euler", index=2, text="Z 回転角度")
                
            row_rot = box_rot.row(align=True)
            row_rot.operator(f"object.{PREFIX_NAME.lower()}_rotate_selected", text="-90°").angle = -90.0
            row_rot.operator(f"object.{PREFIX_NAME.lower()}_rotate_selected", text="-15°").angle = -15.0
            row_rot.operator(f"object.{PREFIX_NAME.lower()}_rotate_selected", text="+15°").angle = 15.0
            row_rot.operator(f"object.{PREFIX_NAME.lower()}_rotate_selected", text="+90°").angle = 90.0
        else:
            box_rot.label(text="※オブジェクトを選択してください", icon='INFO')
            
        layout.separator(factor=1.5)

        # 3. 画面(ビュー)の回転操作
        box_vrot = layout.box()
        box_vrot.label(text="画面(ビュー)自体の回転:", icon='VIEW3D')
        box_vrot.prop(props, "view_rot_axis", expand=True)
        
        axis_v = props.view_rot_axis
        col_vrot = box_vrot.column(align=True)
        if axis_v == 'X': col_vrot.prop(props, "view_rotation_euler", index=0, text="画面 X 回転")
        elif axis_v == 'Y': col_vrot.prop(props, "view_rotation_euler", index=1, text="画面 Y 回転")
        elif axis_v == 'Z': col_vrot.prop(props, "view_rotation_euler", index=2, text="画面 Z 回転")
            
        row_vrot = box_vrot.row(align=True)
        row_vrot.operator(f"view3d.{PREFIX_NAME.lower()}_rotate_view", text="-90°").angle = -90.0
        row_vrot.operator(f"view3d.{PREFIX_NAME.lower()}_rotate_view", text="-15°").angle = -15.0
        row_vrot.operator(f"view3d.{PREFIX_NAME.lower()}_rotate_view", text="+15°").angle = 15.0
        row_vrot.operator(f"view3d.{PREFIX_NAME.lower()}_rotate_view", text="+90°").angle = 90.0
        
        layout.separator(factor=1.5)
        
        # 4. カメラ軌道&てんこ盛り設定
        box_cam = layout.box()
        box_cam.label(text="カメラ軌道・てんこもり設定:", icon='CAMERA_DATA')
        
        row_cam_btn = box_cam.row(align=True)
        row_cam_btn.operator(f"object.{PREFIX_NAME.lower()}_create_camera_rig", text="円周軌道", icon='MESH_CIRCLE').rig_type = 'CIRCLE'
        row_cam_btn.operator(f"object.{PREFIX_NAME.lower()}_create_camera_rig", text="線分軌道", icon='CURVE_PATH').rig_type = 'LINE'
        row_cam_btn.operator(f"object.{PREFIX_NAME.lower()}_create_camera_rig", text="曲線軌道", icon='CURVE_BEZCURVE').rig_type = 'CURVE'
        
        cam = context.scene.camera
        if cam and cam.type == 'CAMERA':
            box_cam.separator()
            box_cam.label(text=f"操作中のカメラ: {cam.name}", icon='VIEW_CAMERA')
            
            curve_obj = None
            target_obj = None
            for const in cam.constraints:
                if const.type == 'FOLLOW_PATH':
                    curve_obj = const.target
                    box_cam.prop(const, "offset_factor", text="軌道上の移動 (0~1)", slider=True)
                elif const.type == 'TRACK_TO':
                    target_obj = const.target
                    
            box_cam.separator()
            
            # --- 軌道(円/線分)の詳細設定 ---
            if curve_obj:
                box_curve = box_cam.box()
                box_curve.label(text=f"軌道({curve_obj.name})の調整:", icon='CURVE_DATA')
                col_curve = box_curve.column(align=True)
                
                if "CamTrack_Circle" in curve_obj.name:
                    col_curve.prop(props, "cam_circle_center", text="円の中心")
                    col_curve.prop(props, "cam_circle_radius", text="円の半径")
                    col_curve.prop(props, "cam_circle_rotation", text="円の傾き (XYZ)")
                elif "CamTrack_Line" in curve_obj.name:
                    col_curve.prop(props, "cam_line_start", text="始点")
                    col_curve.prop(props, "cam_line_end", text="終点")
                else:
                    col_curve.prop(curve_obj, "scale", text="軌道の広さ(スケール)")
                    col_curve.prop(curve_obj, "location", text="軌道の位置(移動)")
                    col_curve.prop(curve_obj, "rotation_euler", text="軌道の傾き(回転)")
                
            # --- [NEW] 注視点の詳細設定 ---
            if target_obj:
                box_target = box_cam.box()
                box_target.label(text=f"注視点({target_obj.name})の設定:", icon='EMPTY_DATA')
                
                # モード切り替え (ラジオボタン)
                box_target.prop(props, "cam_target_mode", expand=True)
                col_target = box_target.column(align=True)
                
                mode = props.cam_target_mode
                if mode == 'OBJECT':
                    col_target.prop(props, "cam_target_obj1", text="追従オブジェクト")
                elif mode == 'POINT':
                    col_target.prop(props, "cam_target_loc", text="指定座標(XYZ)")
                elif mode == 'MIDPOINT':
                    col_target.prop(props, "cam_target_obj1", text="オブジェクト 1")
                    col_target.prop(props, "cam_target_obj2", text="オブジェクト 2")
                
            box_cam.separator()
            
            # --- カメラのズームと視野角 (度) ---
            col_lens = box_cam.column(align=True)
            col_lens.prop(cam.data, "lens", text="ズーム (焦点距離 mm)")
            col_lens.prop(props, "cam_fov", text="水平視野角 (度)")
            
            box_cam.separator()
            
            # --- カメラの視線方向 (Pitch/Roll/Yaw) ---
            box_sight = box_cam.box()
            box_sight.label(text="視線方向 (Pitch/Roll/Yaw):", icon='ORIENTATION_GIMBAL')
            
            track_const = None
            for const in cam.constraints:
                if const.type == 'TRACK_TO':
                    track_const = const
                    break
                    
            if track_const:
                box_sight.prop(track_const, "mute", text="ターゲット注視を解除 (手動で回転)", toggle=True, icon='UNLINKED')
                col_sight = box_sight.column(align=True)
                col_sight.enabled = track_const.mute
                col_sight.prop(cam, "rotation_euler", index=0, text="Pitch (上下・X)")
                col_sight.prop(cam, "rotation_euler", index=1, text="Roll (傾き・Y)")
                col_sight.prop(cam, "rotation_euler", index=2, text="Yaw (左右・Z)")
            else:
                col_sight = box_sight.column(align=True)
                col_sight.prop(cam, "rotation_euler", index=0, text="Pitch (上下・X)")
                col_sight.prop(cam, "rotation_euler", index=1, text="Roll (傾き・Y)")
                col_sight.prop(cam, "rotation_euler", index=2, text="Yaw (左右・Z)")

            box_cam.separator()
            
            # --- カメラの被写界深度 (ボケ) ---
            box_cam.prop(cam.data.dof, "use_dof", text="被写界深度 (ボケ) を有効化", toggle=True, icon='STYLUS_PRESSURE')
            if cam.data.dof.use_dof:
                col_dof = box_cam.column(align=True)
                # ピント合わせの対象は自動的に target_obj になっている
                col_dof.prop(cam.data.dof, "focus_object", text="ピント対象")
                if not cam.data.dof.focus_object:
                    col_dof.prop(cam.data.dof, "focus_distance", text="ピント距離")
                col_dof.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
                col_dof.prop(cam.data.dof, "aperture_blades", text="絞り羽根数")
        else:
            box_cam.label(text="※アクティブなカメラがありません", icon='INFO')
            
        layout.separator(factor=1.5)
        
        # 5. システム / リンク設定
        box_sys = layout.box()
        box_sys.label(text="システム / リンク:", icon='PREFERENCES')
        box_sys.operator(f"wm.{PREFIX_NAME.lower()}_open_url", text="アドオン削除パネル 20260704", icon='URL').url = "<https://app.notion.com/p/20260704-390f5dacaf4380e6939dd28e6e2ff91d>"
        box_sys.operator(f"wm.{PREFIX_NAME.lower()}_open_url", text="ドキュメント (トーラス作成)", icon='URL').url = "<https://app.notion.com/p/390f5dacaf43801d8f08c695979e60e1>"
        box_sys.separator()
        box_sys.operator(
            f"wm.{PREFIX_NAME.lower()}_remove_addon", 
            text="アドオンを無効化して閉じる", 
            icon='CANCEL'
        )

# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
    PolyTorusProperties, 
    POLY_OT_create_camera_rig, 
    POLY_OT_reset_view_and_p,
    POLY_OT_rotate_selected,
    POLY_OT_rotate_view,
    POLY_OT_open_url, 
    POLY_OT_remove_addon,
    POLY_PT_main_panel, 
]

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

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

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