blender まとめ20260821

343 天球儀|timetimekagura












bl_info = {
    "name": "Observer Sky Model with UV Camera",
    "author": "AI Assistant",
    "version": (2, 3, 0),
    "blender": (4, 0, 0),
    "location": "View3D > Sidebar > Observer Sky",
    "description": "Visualizes geometric relations with UV-controlled & Pan-Tilt Camera System.",
    "category": "3D View",
}

import bpy
import bmesh
import math
import random
import mathutils

# ======================================================================
# 初期設定値
# ======================================================================
INIT_EARTH_RADIUS = 5.00
INIT_EARTH_SHOW = True
INIT_EARTH_COLOR = (0.100, 0.400, 0.800, 0.300)
INIT_EARTH_EQUATOR = True

INIT_OUTER_RADIUS = 10.00
INIT_OUTER_SHOW = True
INIT_OUTER_COLOR = (0.500, 0.800, 0.800, 0.100)
INIT_OUTER_WIRE = True
INIT_OUTER_WIRE_THICKNESS = 0.020

INIT_OBS_THETA = 0.00
INIT_OBS_SHOW = False
INIT_MARKER_O_SHOW = False

INIT_SKY_RADIUS = 5.00
INIT_SKY_SHOW = True
INIT_SKY_COLOR = (0.300, 0.100, 0.500, 0.150)

# Tangent Sphere (4番目の球体) 初期値
INIT_TANGENT_RADIUS = 5.00
INIT_TANGENT_SHOW = True
INIT_TANGENT_COLOR = (0.082, 0.063, 0.081, 0.800)
INIT_TANGENT_SEGMENTS = 32
INIT_TANGENT_RINGS = 16
INIT_TANGENT_WIRE_THICKNESS = 0.050

# 矢印(Arrow)の初期値
INIT_ARROW_SHOW = True
INIT_ARROW_LON = 0.00
INIT_ARROW_LAT = 30.00
INIT_ARROW_THICKNESS = 0.500
INIT_ARROW_COLOR = (0.105, 1.000, 0.000, 1.000)

INIT_STAR_COUNT = 300
INIT_STAR_SIZE = 0.050
INIT_STAR_COLOR = (1.000, 1.000, 0.800, 1.000)
INIT_STAR_EMISSION = 5.00
INIT_STAR_SEED = 42

INIT_SHOW_OP_LINE = True
INIT_SHOW_P_STAR_LINE = True
INIT_SHOW_DISTANCE = True
INIT_SHOW_AXES = True

INIT_SHOW_O_EARTH = True
INIT_SHOW_O_OUTER = True

# === メインカメラ用の初期値 ===
INIT_CAM_TARGET = 'T'
INIT_CAM_MODE = 'ORBIT'
INIT_CAM_SHOW_TARGET_MARKER = True

# 周回(ORBIT)用
INIT_CAM_RADIUS = 20.00
INIT_CAM_LON = 45.00
INIT_CAM_LAT = 30.00

# 首振り(PAN_TILT)用 
INIT_CAM_PITCH = 90.00 
INIT_CAM_YAW = 0.00
INIT_CAM_ROLL = 0.00

PREFIX = "OBS_SKY_"
COLLECTION_NAME = "000_EarthSystem"

# ======================================================================
# ユーティリティ関数群
# ======================================================================
def hide_relationship_lines():
    """ 3Dビューポートの「関係線(点線)」を非表示にする """
    for window in bpy.context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D' and hasattr(space, 'overlay'):
                        space.overlay.show_relationship_lines = False

def get_or_create_collection(name):
    if name in bpy.data.collections:
        return bpy.data.collections[name]
    new_col = bpy.data.collections.new(name)
    bpy.context.scene.collection.children.link(new_col)
    return new_col

def safe_remove_collection_contents(col_name):
    if col_name not in bpy.data.collections:
        return
    col = bpy.data.collections[col_name]
    for obj in list(col.objects):
        # カメラは削除せず再利用することで、スライダー操作中の視点外れを防止
        if obj.type == 'CAMERA':
            continue
        mesh_data = obj.data
        bpy.data.objects.remove(obj, do_unlink=True)
        if mesh_data:
            if isinstance(mesh_data, bpy.types.Mesh) and mesh_data.users == 0:
                bpy.data.meshes.remove(mesh_data)
            elif isinstance(mesh_data, bpy.types.Curve) and mesh_data.users == 0:
                bpy.data.curves.remove(mesh_data)

def create_material(name, color=(1,1,1,1), emission=0.0, alpha=1.0, is_transparent=False):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name)
    mat.use_nodes = True
    nodes = mat.node_tree.nodes
    nodes.clear()
    
    output = nodes.new(type='ShaderNodeOutputMaterial')
    output.location = (400, 0)
    
    principled = nodes.new(type='ShaderNodeBsdfPrincipled')
    principled.location = (0, 0)
    
    principled.inputs['Base Color'].default_value = color
    principled.inputs['Emission Color'].default_value = color[:3] + (1.0,)
    principled.inputs['Emission Strength'].default_value = emission
    principled.inputs['Alpha'].default_value = alpha
    
    if is_transparent and alpha < 1.0:
        mat.blend_method = 'BLEND' if hasattr(mat, 'blend_method') else 'OPAQUE'
        principled.inputs['Roughness'].default_value = 0.1
    else:
        mat.blend_method = 'OPAQUE'
    
    mat.node_tree.links.new(principled.outputs['BSDF'], output.inputs['Surface'])
    return mat

def create_sphere(name, radius, location, collection, mat=None, segments=32, rings=16):
    mesh = bpy.data.meshes.new(name)
    obj = bpy.data.objects.new(name, mesh)
    collection.objects.link(obj)
    if mat:
        obj.data.materials.append(mat)
    
    bm = bmesh.new()
    bmesh.ops.create_uvsphere(bm, u_segments=segments, v_segments=rings, radius=radius)
    bm.to_mesh(mesh)
    bm.free()
    
    obj.location = location
    for poly in mesh.polygons:
        poly.use_smooth = True
    return obj

def create_arrow(name, p1, p2, thickness, mat, collection):
    direction = p2 - p1
    length = direction.length
    if length < 1e-4:
        return None
    
    mesh = bpy.data.meshes.new(name)
    obj = bpy.data.objects.new(name, mesh)
    collection.objects.link(obj)
    if mat:
        obj.data.materials.append(mat)
        
    bm = bmesh.new()
    head_len = min(thickness * 4.0, length * 0.2)
    shaft_len = length - head_len
    
    # Shaft
    ret = bmesh.ops.create_cone(bm, cap_ends=True, segments=16, radius1=thickness, radius2=thickness, depth=shaft_len)
    bmesh.ops.translate(bm, verts=ret['verts'], vec=(0, 0, shaft_len / 2))
    
    # Head
    ret_head = bmesh.ops.create_cone(bm, cap_ends=True, segments=16, radius1=thickness*2.5, radius2=0.0, depth=head_len)
    bmesh.ops.translate(bm, verts=ret_head['verts'], vec=(0, 0, shaft_len + head_len / 2))
    
    z_axis = mathutils.Vector((0, 0, 1))
    rot_quat = z_axis.rotation_difference(direction)
    bmesh.ops.rotate(bm, verts=bm.verts, cent=(0,0,0), matrix=rot_quat.to_matrix())
    bmesh.ops.translate(bm, verts=bm.verts, vec=p1)
    
    bm.to_mesh(mesh)
    bm.free()
    for poly in mesh.polygons: poly.use_smooth = True
    return obj

def create_stars(name, P, radius, count, size, seed, color, emission, collection):
    mesh = bpy.data.meshes.new(name)
    obj = bpy.data.objects.new(name, mesh)
    collection.objects.link(obj)
    mat = create_material(PREFIX+"Mat_Star", color=color, emission=emission, alpha=color[3], is_transparent=True)
    obj.data.materials.append(mat)
    
    bm = bmesh.new()
    random.seed(seed)
    star_positions = []
    for _ in range(count):
        z = random.uniform(-1.0, 1.0)
        phi = random.uniform(0.0, 2.0 * math.pi)
        x = math.sqrt(1.0 - z*z) * math.cos(phi)
        y = math.sqrt(1.0 - z*z) * math.sin(phi)
        pos = P + mathutils.Vector((x, y, z)) * radius
        star_positions.append(pos)
        geom = bmesh.ops.create_icosphere(bm, subdivisions=1, radius=size)
        bmesh.ops.translate(bm, verts=geom['verts'], vec=pos)
        
    bm.to_mesh(mesh)
    bm.free()
    return obj, star_positions

def create_line(name, p1, p2, collection, mat=None):
    mesh = bpy.data.meshes.new(name)
    obj = bpy.data.objects.new(name, mesh)
    collection.objects.link(obj)
    if mat: obj.data.materials.append(mat)
    bm = bmesh.new()
    bm.edges.new((bm.verts.new(p1), bm.verts.new(p2)))
    bm.to_mesh(mesh)
    bm.free()
    return obj

def create_circle(name, radius, location, collection, mat=None):
    mesh = bpy.data.meshes.new(name)
    obj = bpy.data.objects.new(name, mesh)
    collection.objects.link(obj)
    if mat: obj.data.materials.append(mat)
    bm = bmesh.new()
    bmesh.ops.create_circle(bm, cap_ends=False, radius=radius, segments=64)
    bm.to_mesh(mesh)
    bm.free()
    obj.location = location
    return obj

def create_text(name, text, location, collection, scale=1.0, mat=None):
    curve = bpy.data.curves.new(type="FONT", name=name)
    curve.body = text
    curve.align_x = 'CENTER'
    curve.align_y = 'CENTER'
    obj = bpy.data.objects.new(name, curve)
    collection.objects.link(obj)
    obj.location = location
    obj.scale = (scale, scale, scale)
    if mat: obj.data.materials.append(mat)
    constraint = obj.constraints.new(type='TRACK_TO')
    constraint.track_axis = 'TRACK_Z'
    constraint.up_axis = 'UP_Y'
    return obj, constraint

def setup_camera(name, location, target_loc, collection, rot_euler=None):
    cam_obj = bpy.data.objects.get(name)
    if not cam_obj:
        cam_data = bpy.data.cameras.new(name)
        cam_obj = bpy.data.objects.new(name, cam_data)
        collection.objects.link(cam_obj)
        
    cam_obj.location = location
    
    if rot_euler is not None:
        cam_obj.rotation_euler = rot_euler
    elif target_loc is not None:
        direction = target_loc - mathutils.Vector(location)
        if direction.length > 1e-4:
            rot_quat = direction.to_track_quat('-Z', 'Y')
            cam_obj.rotation_euler = rot_quat.to_euler()
        else:
            cam_obj.rotation_euler = (0, 0, 0)
            
    return cam_obj

# ======================================================================
# 座標・情報計算関数
# ======================================================================
def calculate_measurements(props):
    R = props.earth_radius
    theta = math.radians(props.obs_longitude)
    P = mathutils.Vector((R * math.cos(theta), R * math.sin(theta), 0))
    
    R_sky = props.sky_radius
    dir_OP = mathutils.Vector((math.cos(theta), math.sin(theta), 0))
    T = P + dir_OP * R_sky
    
    lon = math.radians(props.arrow_start_lon)
    lat = math.radians(props.arrow_start_lat)
    R_T = props.tangent_radius
    Start = T + mathutils.Vector((
        R_T * math.cos(lat) * math.cos(lon),
        R_T * math.cos(lat) * math.sin(lon),
        R_T * math.sin(lat)
    ))
    End = P
    
    return {
        "start": Start,
        "end": End,
        "arrow_len": (End - Start).length,
        "dist_end_surf": abs(End.length - R),
        "dist_start_surf": abs(Start.length - R),
        "dist_end_center": End.length,
        "dist_start_center": Start.length,
        "O": mathutils.Vector((0, 0, 0)),
        "P": P,
        "T": T
    }

# ======================================================================
# メインの生成ロジック
# ======================================================================
def generate_sky_system(props, context):
    safe_remove_collection_contents(COLLECTION_NAME)
    col = get_or_create_collection(COLLECTION_NAME)
    
    measurements = calculate_measurements(props)
    O = measurements["O"]
    P = measurements["P"]
    T = measurements["T"]
    Start = measurements["start"]
    
    R = props.earth_radius
    R_outer = max(props.outer_sphere_radius, R + 0.01)
    
    # Materials
    mat_earth = create_material(PREFIX+"Mat_Earth", color=tuple(props.earth_color), alpha=props.earth_color[3], is_transparent=True)
    mat_outer = create_material(PREFIX+"Mat_Outer", color=tuple(props.outer_color), alpha=props.outer_color[3], is_transparent=True)
    mat_sky = create_material(PREFIX+"Mat_Sky", color=tuple(props.sky_color), alpha=props.sky_color[3], is_transparent=True)
    mat_tangent = create_material(PREFIX+"Mat_Tangent", color=tuple(props.tangent_color), emission=1.5, alpha=props.tangent_color[3], is_transparent=True)
    mat_arrow = create_material(PREFIX+"Mat_Arrow", color=tuple(props.arrow_color), emission=1.5, alpha=props.arrow_color[3], is_transparent=True)
    
    mat_line = create_material(PREFIX+"Mat_Line", color=(1, 1, 1, 1), emission=2.0)
    mat_line_dist = create_material(PREFIX+"Mat_Line_Dist", color=(0.2, 1, 0.2, 1), emission=2.0)
    mat_line_p_star = create_material(PREFIX+"Mat_Line_PStar", color=(1, 0.8, 0, 1), emission=1.5)
    mat_marker_O = create_material(PREFIX+"Mat_O", color=(1, 0, 0, 1), emission=3.0)
    mat_marker_P = create_material(PREFIX+"Mat_P", color=(0, 1, 0, 1), emission=3.0)
    
    # 1. Earth
    if props.earth_show:
        create_sphere("Earth", R, O, col, mat_earth)
    if props.earth_equator:
        create_circle(PREFIX+"Equator", R, O, col, mat_line)
        create_line(PREFIX+"Axis_Z", (0,0,-R_outer*1.2), (0,0,R_outer*1.2), col, mat_line)

    # 2. Outer Sphere
    if props.outer_show:
        outer_obj = create_sphere("000_OuterSphere", R_outer, O, col, mat_outer, segments=48, rings=24)
        if props.outer_wire:
            wire_mod = outer_obj.modifiers.new(name="Wireframe", type='WIREFRAME')
            wire_mod.thickness = props.outer_wire_thickness
            wire_mod.use_replace = True
            wire_mod.use_even_offset = True
            
    if props.show_o_earth:
        dir_e = mathutils.Vector((-0.707, 0.707, 0))
        P_e = O + dir_e * R
        create_line(PREFIX+"Line_O_Earth", O, P_e, col, mat_line_dist)
        create_text(PREFIX+"Text_O_Earth", f"O→Earth = {R:.1f}", P_e * 0.6, col, scale=R_outer*0.02, mat=mat_line_dist)

    if props.show_o_outer and props.outer_show:
        dir_o = mathutils.Vector((-0.707, -0.707, 0))
        P_o = O + dir_o * R_outer
        create_line(PREFIX+"Line_O_Outer", O, P_o, col, mat_line_dist)
        create_text(PREFIX+"Text_O_Outer", f"O→Outer = {R_outer:.1f}", P_o * 0.6, col, scale=R_outer*0.02, mat=mat_line_dist)

    # 3. P & Sky
    if props.marker_o_show:
        create_sphere(PREFIX+"Marker_O", R_outer*0.02, O, col, mat_marker_O, segments=16, rings=8)
    if props.obs_show:
        create_sphere(PREFIX+"Marker_P", R_outer*0.025, P, col, mat_marker_P, segments=16, rings=8)

    if props.show_op_line:
        create_line(PREFIX+"Line_OP", O, P, col, mat_line)
        if props.show_distance:
            create_text(PREFIX+"Text_OP", f"|OP| = {R:.1f}", (O + P)/2.0 + mathutils.Vector((0,0,R*0.05)), col, scale=R_outer*0.025, mat=mat_line)

    if props.sky_show:
        create_sphere("CelestialSphere", props.sky_radius, P, col, mat_sky)
        create_circle(PREFIX+"Sky_Equator", props.sky_radius, P, col, mat_line)
        
    # 4. Tangent Sphere
    if props.tangent_show:
        tangent_obj = create_sphere("TangentWireSphere", props.tangent_radius, T, col, mat_tangent, segments=props.tangent_segments, rings=props.tangent_rings)
        wire_mod = tangent_obj.modifiers.new(name="Wireframe", type='WIREFRAME')
        wire_mod.thickness = props.tangent_wire_thickness
        wire_mod.use_replace = True
        wire_mod.use_even_offset = True

    # 5. Arrow
    if props.arrow_show:
        create_arrow(PREFIX+"Arrow_Ray", Start, P, props.arrow_thickness, mat_arrow, col)
        
    # 6. Stars
    star_positions = []
    if props.star_count > 0:
        stars_obj, star_positions = create_stars(PREFIX+"Stars", P, props.sky_radius, props.star_count, props.star_size, props.star_seed, tuple(props.star_color), props.star_emission, col)
        
    if props.show_p_star_line and len(star_positions) > 0:
        random.seed(props.star_seed + 1)
        for i, s_pos in enumerate(random.sample(star_positions, min(3, len(star_positions)))):
            create_line(PREFIX+f"Line_PStar_{i}", P, s_pos, col, mat_line_p_star)
            if props.show_distance:
                create_text(PREFIX+f"Text_PStar_{i}", f"|P-Star| = {props.sky_radius:.1f}", (P + s_pos)/2.0, col, scale=R_outer*0.025, mat=mat_line_p_star)
                
    if props.show_axes:
        create_line(PREFIX+"Axis_X", O, (R_outer*1.2, 0, 0), col, mat_line)
        create_line(PREFIX+"Axis_Y", O, (0, R_outer*1.2, 0), col, mat_line)

    # 7. カメラ生成と位置・回転計算
    if props.cam_target == 'O': target_loc = O
    elif props.cam_target == 'P': target_loc = P
    elif props.cam_target == 'T': target_loc = T
    else: target_loc = O

    # 周回モードの時のみ、ターゲット位置に目印のマーカーを表示する
    if props.cam_mode == 'ORBIT' and props.cam_show_target_marker:
        mat_cam_tgt = create_material(PREFIX+"Mat_CamTargetMarker", color=(1.0, 0.2, 0.8, 1.0), emission=2.0)
        create_sphere(PREFIX+"Cam_Target_Marker", max(0.2, R_outer*0.015), target_loc, col, mat_cam_tgt, segments=16, rings=8)

    # メインカメラの計算
    if props.cam_mode == 'ORBIT':
        u = math.radians(props.cam_lon)
        v = math.radians(props.cam_lat)
        r = props.cam_radius
        cam_loc = target_loc + mathutils.Vector((
            r * math.cos(v) * math.cos(u),
            r * math.cos(v) * math.sin(u),
            r * math.sin(v)
        ))
        cam_rot = None 
    else:
        cam_loc = target_loc
        cam_rot = (math.radians(props.cam_pitch), math.radians(props.cam_roll), math.radians(props.cam_yaw))

    # 各カメラ生成
    cam_main = setup_camera(PREFIX+"Cam_Main", cam_loc, target_loc, col, rot_euler=cam_rot)
    
    setup_camera(PREFIX+"Cam_Observer", P, P + mathutils.Vector((0,0,1)), col)
    setup_camera(PREFIX+"Cam_Global", (R_outer*2.0, -R_outer*2.0, R_outer*1.5), O, col)
    setup_camera(PREFIX+"Cam_EarthCenter", O, P, col)
    setup_camera(PREFIX+"Cam_SkyCenter", P + mathutils.Vector((props.sky_radius*2, 0, props.sky_radius)), P, col)
    
    if not context.scene.camera:
        context.scene.camera = cam_main

    # テキストオブジェクトが常にカメラを向くように設定
    for obj in col.objects:
        if obj.type == 'FONT':
            for cons in obj.constraints:
                if cons.type == 'TRACK_TO':
                    cons.target = context.scene.camera
                    
    # 関係線(点線)を隠す処理を呼び出す
    hide_relationship_lines()

def update_callback(self, context):
    if self.outer_sphere_radius <= self.earth_radius:
        self.outer_sphere_radius = self.earth_radius + 0.1
    try:
        generate_sky_system(self, context)
    except Exception as e:
        print(f"Observer Sky Update Error: {e}")

# ======================================================================
# PropertyGroup
# ======================================================================
class ObserverSkyProperties(bpy.types.PropertyGroup):
    cam_target: bpy.props.EnumProperty(
        name="基準球体 (Target)",
        items=[
            ('O', "1. 地球 (O)", "地球の中心"),
            ('P', "2. 天球 (P)", "地表の観測者"),
            ('T', "3. Tangent Sphere (T)", "4番目の球体の中心")
        ],
        default=INIT_CAM_TARGET,
        update=update_callback
    )
    cam_mode: bpy.props.EnumProperty(
        name="Camera Mode",
        items=[
            ('ORBIT', "周回モード (球面移動)", "ターゲットの周囲を回る"),
            ('PAN_TILT', "首振りモード (位置固定)", "ターゲット位置に固定して首を振る")
        ],
        default=INIT_CAM_MODE,
        update=update_callback
    )
    cam_show_target_marker: bpy.props.BoolProperty(
        name="ターゲットの目印(ピンクの球)を表示", 
        default=INIT_CAM_SHOW_TARGET_MARKER, 
        update=update_callback
    )
    
    # ORBIT 用プロパティ
    cam_radius: bpy.props.FloatProperty(name="Camera Radius", default=INIT_CAM_RADIUS, min=0.1, max=1000.0, update=update_callback)
    cam_lon: bpy.props.FloatProperty(name="Camera Longitude", default=INIT_CAM_LON, min=-360.0, max=360.0, update=update_callback)
    cam_lat: bpy.props.FloatProperty(name="Camera Latitude", default=INIT_CAM_LAT, min=-90.0, max=90.0, update=update_callback)

    # PAN/TILT 用プロパティ
    cam_pitch: bpy.props.FloatProperty(name="Pitch (上下)", default=INIT_CAM_PITCH, min=-360.0, max=360.0, update=update_callback)
    cam_yaw: bpy.props.FloatProperty(name="Yaw (左右)", default=INIT_CAM_YAW, min=-360.0, max=360.0, update=update_callback)
    cam_roll: bpy.props.FloatProperty(name="Roll (回転)", default=INIT_CAM_ROLL, min=-360.0, max=360.0, update=update_callback)

    # 以降は従来のプロパティ
    obs_show: bpy.props.BoolProperty(name="Show Green Observer (P)", default=INIT_OBS_SHOW, update=update_callback)
    marker_o_show: bpy.props.BoolProperty(name="Show Red Center (O)", default=INIT_MARKER_O_SHOW, update=update_callback)

    earth_radius: bpy.props.FloatProperty(name="Earth Radius", default=INIT_EARTH_RADIUS, min=1.0, max=100.0, update=update_callback)
    earth_show: bpy.props.BoolProperty(name="Show Earth", default=INIT_EARTH_SHOW, update=update_callback)
    earth_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_EARTH_COLOR, min=0.0, max=1.0, update=update_callback)
    earth_equator: bpy.props.BoolProperty(name="Show Equator", default=INIT_EARTH_EQUATOR, update=update_callback)

    outer_sphere_radius: bpy.props.FloatProperty(name="Outer Radius", default=INIT_OUTER_RADIUS, min=1.01, max=200.0, update=update_callback)
    outer_show: bpy.props.BoolProperty(name="Show Outer Sphere", default=INIT_OUTER_SHOW, update=update_callback)
    outer_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_OUTER_COLOR, min=0.0, max=1.0, update=update_callback)
    outer_wire: bpy.props.BoolProperty(name="Wireframe", default=INIT_OUTER_WIRE, update=update_callback)
    outer_wire_thickness: bpy.props.FloatProperty(name="Wire Thickness", default=INIT_OUTER_WIRE_THICKNESS, min=0.001, max=1.0, update=update_callback)
    show_o_earth: bpy.props.BoolProperty(name="O→Earth Distance", default=INIT_SHOW_O_EARTH, update=update_callback)
    show_o_outer: bpy.props.BoolProperty(name="O→Outer Distance", default=INIT_SHOW_O_OUTER, update=update_callback)

    obs_longitude: bpy.props.FloatProperty(name="Longitude (deg)", default=INIT_OBS_THETA, min=-180.0, max=180.0, update=update_callback)

    sky_radius: bpy.props.FloatProperty(name="Sky Radius", default=INIT_SKY_RADIUS, min=1.0, max=100.0, update=update_callback)
    sky_show: bpy.props.BoolProperty(name="Show Sky", default=INIT_SKY_SHOW, update=update_callback)
    sky_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_SKY_COLOR, min=0.0, max=1.0, update=update_callback)

    tangent_radius: bpy.props.FloatProperty(name="Radius", default=INIT_TANGENT_RADIUS, min=0.1, max=100.0, update=update_callback)
    tangent_show: bpy.props.BoolProperty(name="表示する", default=INIT_TANGENT_SHOW, update=update_callback)
    tangent_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_TANGENT_COLOR, min=0.0, max=1.0, update=update_callback)
    tangent_segments: bpy.props.IntProperty(name="経度線 (Longitude)", default=INIT_TANGENT_SEGMENTS, min=3, max=128, update=update_callback)
    tangent_rings: bpy.props.IntProperty(name="緯度線 (Latitude)", default=INIT_TANGENT_RINGS, min=3, max=64, update=update_callback)
    tangent_wire_thickness: bpy.props.FloatProperty(name="骨格の太さ", default=INIT_TANGENT_WIRE_THICKNESS, min=0.001, max=1.0, update=update_callback)

    arrow_show: bpy.props.BoolProperty(name="矢印を表示", default=INIT_ARROW_SHOW, update=update_callback)
    arrow_start_lon: bpy.props.FloatProperty(name="UV経度 (Lon)", default=INIT_ARROW_LON, min=-180.0, max=180.0, update=update_callback)
    arrow_start_lat: bpy.props.FloatProperty(name="UV緯度 (Lat)", default=INIT_ARROW_LAT, min=-90.0, max=90.0, update=update_callback)
    arrow_thickness: bpy.props.FloatProperty(name="矢印の太さ", default=INIT_ARROW_THICKNESS, min=0.01, max=2.0, update=update_callback)
    arrow_color: bpy.props.FloatVectorProperty(name="矢印の色", subtype='COLOR', size=4, default=INIT_ARROW_COLOR, min=0.0, max=1.0, update=update_callback)

    star_count: bpy.props.IntProperty(name="Count", default=INIT_STAR_COUNT, min=0, max=5000, update=update_callback)
    star_size: bpy.props.FloatProperty(name="Size", default=INIT_STAR_SIZE, min=0.01, max=1.0, update=update_callback)
    star_color: bpy.props.FloatVectorProperty(name="Color (RGBA)", subtype='COLOR', size=4, default=INIT_STAR_COLOR, min=0.0, max=1.0, update=update_callback)
    star_emission: bpy.props.FloatProperty(name="Emission", default=INIT_STAR_EMISSION, min=0.0, max=50.0, update=update_callback)
    star_seed: bpy.props.IntProperty(name="Random Seed", default=INIT_STAR_SEED, update=update_callback)

    show_op_line: bpy.props.BoolProperty(name="Show O-P Line", default=INIT_SHOW_OP_LINE, update=update_callback)
    show_p_star_line: bpy.props.BoolProperty(name="Show P-Star Line", default=INIT_SHOW_P_STAR_LINE, update=update_callback)
    show_distance: bpy.props.BoolProperty(name="Show Distances", default=INIT_SHOW_DISTANCE, update=update_callback)
    show_axes: bpy.props.BoolProperty(name="Show Axes", default=INIT_SHOW_AXES, update=update_callback)

# ======================================================================
# Operators
# ======================================================================
class OBS_SKY_OT_Generate(bpy.types.Operator):
    bl_idname = "obs_sky.generate"
    bl_label = "Generate / Update Model"
    
    def execute(self, context):
        props = context.scene.obs_sky_props
        generate_sky_system(props, context)
        return {'FINISHED'}

class OBS_SKY_OT_RegenerateStars(bpy.types.Operator):
    bl_idname = "obs_sky.regenerate_stars"
    bl_label = "Randomize Stars"
    
    def execute(self, context):
        props = context.scene.obs_sky_props
        props.star_seed = random.randint(0, 10000)
        return {'FINISHED'}

class OBS_SKY_OT_SetCamera(bpy.types.Operator):
    bl_idname = "obs_sky.set_camera"
    bl_label = "Set Camera"
    
    cam_type: bpy.props.StringProperty()
    
    def execute(self, context):
        cam_name = PREFIX + self.cam_type
        if cam_name in bpy.data.objects:
            cam = bpy.data.objects[cam_name]
            context.scene.camera = cam
            cam.data.show_passepartout = True
            cam.data.passepartout_alpha = 0.8
            
            for area in context.screen.areas:
                if area.type == 'VIEW_3D':
                    area.spaces.active.region_3d.view_perspective = 'CAMERA'
                    
            self.report({'INFO'}, f"{cam_name} に切り替えました")
        return {'FINISHED'}

class OBS_SKY_OT_ResetCamOrbit(bpy.types.Operator):
    bl_idname = "obs_sky.reset_cam_orbit"
    bl_label = "経度・緯度・半径をリセット"
    bl_description = "周回モードの設定を初期値に戻します"
    
    def execute(self, context):
        props = context.scene.obs_sky_props
        props.cam_radius = INIT_CAM_RADIUS
        props.cam_lon = INIT_CAM_LON
        props.cam_lat = INIT_CAM_LAT
        return {'FINISHED'}

class OBS_SKY_OT_ResetCamPanTilt(bpy.types.Operator):
    bl_idname = "obs_sky.reset_cam_pan_tilt"
    bl_label = "首振り角度をリセット"
    bl_description = "首振りモードのPitch/Yaw/Rollを初期値に戻します"
    
    def execute(self, context):
        props = context.scene.obs_sky_props
        props.cam_pitch = INIT_CAM_PITCH
        props.cam_yaw = INIT_CAM_YAW
        props.cam_roll = INIT_CAM_ROLL
        return {'FINISHED'}

class OBS_SKY_OT_CopyInfo(bpy.types.Operator):
    bl_idname = "obs_sky.copy_info"
    bl_label = "情報をクリップボードにコピー"
    
    def execute(self, context):
        props = context.scene.obs_sky_props
        meas = calculate_measurements(props)
        info_text = f"""--- Observer Sky Measurements ---
[矢印の長さ]: {meas['arrow_len']:.3f}
[先端(End)の位置 - 地表P]: ({meas['end'].x:.3f}, {meas['end'].y:.3f}, {meas['end'].z:.3f})
[後端(Start)の位置 - UV点]: ({meas['start'].x:.3f}, {meas['start'].y:.3f}, {meas['start'].z:.3f})
"""
        context.window_manager.clipboard = info_text
        self.report({'INFO'}, "情報をクリップボードにコピーしました")
        return {'FINISHED'}

class OBS_SKY_OT_CopyInitValues(bpy.types.Operator):
    bl_idname = "obs_sky.copy_init_values"
    bl_label = "初期設定値コードをコピー"
    
    def execute(self, context):
        props = context.scene.obs_sky_props
        copy_text = f"""INIT_EARTH_RADIUS = {props.earth_radius:.2f}
INIT_CAM_TARGET = '{props.cam_target}'
INIT_CAM_MODE = '{props.cam_mode}'
INIT_CAM_RADIUS = {props.cam_radius:.2f}
INIT_CAM_LON = {props.cam_lon:.2f}
INIT_CAM_LAT = {props.cam_lat:.2f}
INIT_CAM_PITCH = {props.cam_pitch:.2f}
INIT_CAM_YAW = {props.cam_yaw:.2f}
INIT_CAM_ROLL = {props.cam_roll:.2f}
"""
        context.window_manager.clipboard = copy_text
        self.report({'INFO'}, "現在のパラメーターを初期設定値としてクリップボードにコピーしました")
        return {'FINISHED'}

class OBS_SKY_OT_RemoveAddon(bpy.types.Operator):
    bl_idname = "obs_sky.remove_addon"
    bl_label = "アドオン解除"
    
    def execute(self, context):
        bpy.app.timers.register(lambda: unregister() or None, first_interval=0.1)
        return {'FINISHED'}

# ======================================================================
# UI Panels
# ======================================================================
class OBS_SKY_PT_MainPanel(bpy.types.Panel):
    bl_label = "Observer Sky Model"
    bl_idname = "OBS_SKY_PT_MainPanel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Observer Sky"
    
    def draw(self, context):
        layout = self.layout
        props = context.scene.obs_sky_props
        
        box = layout.box()
        box.prop(props, "obs_show", icon='USER')
        box.prop(props, "marker_o_show", icon='WORLD')
        layout.separator()
        layout.operator(OBS_SKY_OT_Generate.bl_idname, icon='PLAY')

        # ======================================================
        # Main Camera Control 
        # ======================================================
        box = layout.box()
        box.label(text="メインカメラ制御", icon='CAMERA_DATA')
        box.prop(props, "cam_target")
        box.separator()
        
        box.prop(props, "cam_mode", expand=True)
        
        if props.cam_mode == 'ORBIT':
            box.prop(props, "cam_show_target_marker")
            box.separator()
            box.prop(props, "cam_radius", text="半径 (中心からの距離)")
            box.separator()
            col = box.column(align=True)
            col.prop(props, "cam_lon", text="経度 (U)")
            col.prop(props, "cam_lat", text="緯度 (V)")
            box.separator()
            box.operator(OBS_SKY_OT_ResetCamOrbit.bl_idname, icon='FILE_REFRESH')
        else: # PAN_TILT
            col = box.column(align=True)
            col.prop(props, "cam_pitch", text="Pitch (上下)")
            col.prop(props, "cam_yaw", text="Yaw (左右)")
            col.prop(props, "cam_roll", text="Roll (回転)")
            box.separator()
            box.operator(OBS_SKY_OT_ResetCamPanTilt.bl_idname, icon='FILE_REFRESH')
        
        box.separator()
        box.operator(OBS_SKY_OT_SetCamera.bl_idname, text="メインカメラの視点に入る", icon='HIDE_OFF').cam_type = "Cam_Main"
        # ======================================================
        
        box = layout.box()
        box.label(text="Tangent Sphere (4番目の骨格球体)", icon='SHADING_WIRE')
        box.prop(props, "tangent_show")
        if props.tangent_show:
            box.prop(props, "tangent_radius")
            box.prop(props, "tangent_wire_thickness")
            row_div = box.row()
            row_div.prop(props, "tangent_segments")
            row_div.prop(props, "tangent_rings")
            box.prop(props, "tangent_color")
            
        box = layout.box()
        box.label(text="矢印の設定 (Arrow / Ray)", icon='EMPTY_AXIS')
        box.prop(props, "arrow_show")
        if props.arrow_show:
            box.prop(props, "arrow_start_lon")
            box.prop(props, "arrow_start_lat")
            box.prop(props, "arrow_thickness")
            box.prop(props, "arrow_color")
            
        box = layout.box()
        box.label(text="Earth & Concentric (Center: O)", icon='MESH_UVSPHERE')
        box.prop(props, "earth_radius")
        row = box.row()
        row.prop(props, "earth_show")
        row.prop(props, "earth_equator")
        box.prop(props, "earth_color")
        
        box.separator()
        box.label(text="Outer Sphere [000]:")
        box.prop(props, "outer_sphere_radius")
        row = box.row()
        row.prop(props, "outer_show")
        row.prop(props, "outer_wire")
        if props.outer_wire:
            box.prop(props, "outer_wire_thickness")
        box.prop(props, "outer_color")
        
        box.separator()
        box.prop(props, "show_o_earth")
        box.prop(props, "show_o_outer")
        
        box = layout.box()
        box.label(text="Observer Position", icon='DRIVER_TRANSFORM')
        box.prop(props, "obs_longitude")
        
        box = layout.box()
        box.label(text="Celestial Sphere (Center: P)", icon='SPHERE')
        box.prop(props, "sky_radius")
        box.prop(props, "sky_show")
        box.prop(props, "sky_color")
        
        box = layout.box()
        box.label(text="Stars (Surface of Sky)", icon='SOLO_ON')
        box.prop(props, "star_count")
        box.prop(props, "star_size")
        box.prop(props, "star_color")
        box.prop(props, "star_emission")
        box.operator(OBS_SKY_OT_RegenerateStars.bl_idname, icon='FILE_REFRESH')
        
        box = layout.box()
        box.label(text="Visualization / Vectors", icon='GIZMO')
        col = box.column(align=True)
        col.prop(props, "show_op_line")
        col.prop(props, "show_p_star_line")
        col.prop(props, "show_distance")
        col.prop(props, "show_axes")
        
        box = layout.box()
        box.label(text="その他の固定カメラ (Other Cameras)", icon='VIEW_CAMERA')
        col = box.column(align=True)
        col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Observer View").cam_type = "Cam_Observer"
        col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Global View").cam_type = "Cam_Global"
        col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Earth Center View").cam_type = "Cam_EarthCenter"
        col.operator(OBS_SKY_OT_SetCamera.bl_idname, text="Sky View").cam_type = "Cam_SkyCenter"
        
        box = layout.box()
        box.label(text="Measurements & Info (情報パネル)", icon='INFO')
        box.operator(OBS_SKY_OT_CopyInfo.bl_idname, icon='COPYDOWN')
        layout.separator()
        layout.operator(OBS_SKY_OT_CopyInitValues.bl_idname, icon='COPYDOWN', text="初期設定値コードをコピー")

class OBS_SKY_PT_AdminPanel(bpy.types.Panel):
    bl_label = "Admin / Remove"
    bl_idname = "OBS_SKY_PT_AdminPanel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Observer Sky"
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        layout.operator(OBS_SKY_OT_RemoveAddon.bl_idname, icon='CANCEL')

# ======================================================================
# Registration
# ======================================================================
setup_retry_count = 0

def init_addon_setup():
    global setup_retry_count
    if not bpy.context.window_manager.windows:
        setup_retry_count += 1
        if setup_retry_count > 50: return None
        return 0.1 
    try:
        props = bpy.context.scene.obs_sky_props
        generate_sky_system(props, bpy.context)
    except Exception as e:
        print(f"Observer Sky Auto-generate Error: {e}")
        
    for window in bpy.context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        space.show_region_ui = True
                        if hasattr(space, 'shading'):
                            space.shading.type = 'MATERIAL'
    hide_relationship_lines()
    return None

classes = (
    ObserverSkyProperties,
    OBS_SKY_OT_Generate,
    OBS_SKY_OT_RegenerateStars,
    OBS_SKY_OT_SetCamera,
    OBS_SKY_OT_ResetCamOrbit,
    OBS_SKY_OT_ResetCamPanTilt,
    OBS_SKY_OT_CopyInfo,
    OBS_SKY_OT_CopyInitValues,
    OBS_SKY_OT_RemoveAddon,
    OBS_SKY_PT_MainPanel,
    OBS_SKY_PT_AdminPanel,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Scene.obs_sky_props = bpy.props.PointerProperty(type=ObserverSkyProperties)
    if not bpy.app.timers.is_registered(init_addon_setup):
        bpy.app.timers.register(init_addon_setup, first_interval=0.1)

def unregister():
    if bpy.app.timers.is_registered(init_addon_setup):
        bpy.app.timers.unregister(init_addon_setup)
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)
    del bpy.types.Scene.obs_sky_props

if __name__ == "__main__":
    register()