rapture_20251020131412.png

rapture_20251020131429.png

https://aistudio.google.com/app/prompts?state={"ids":["1ErHJKWdIaYjz5pQuhxclDnCIGyuxE04K"],"action":"open","userId":"100872131270251692722","resourceKeys":{}}&usp=sharing

# blender-cod: utf-8
# ---------------------------------------------------------------------
# アドオン基本情報
# ---------------------------------------------------------------------
bl_info = {
    "name": "369 緯度経度 球体 (ido_keido_001)",
    "author": "あなたのお名前 & zionadchat",
    "version": (3, 7, 3), # ### 変更 ###: 画像の繰り返し貼付オプションを追加
    "blender": (4, 1, 0),
    "location": "View3D > Sidebar > [ 単体 緯度経度 ]",
    "description": "緯線・経線を生成。球体表面の特定経度・緯度範囲のハイライトを個別に追加作成し、表裏に別々の画像を貼る機能を持ちます。",
    "category": "Object",
}

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

# ---------------------------------------------------------------------
# アドオン設定
# ---------------------------------------------------------------------
PREFIX = "ido_keido_001"
ADDON_CATEGORY_NAME = "[ 単体 緯度経度 ]"

# ---------------------------------------------------------------------
# アドオンの初期設定値
# ---------------------------------------------------------------------
addon_defaults = {
    "axis_reference": 'Z', "lalo_radius": 10.0,
    "latitude_thickness": 0.30,
    "longitude_thickness": 0.10,
    "latitude_color": (0.8, 0.0, 0.8, 1.0), "longitude_color": (0.5, 1.0, 0.0, 1.0),
    "latitude_creation_mode": 'ANGLE', "latitude_divisions": 5, "latitude_object_name": "Latitude",
    "latitude_angle_step": math.radians(30),
    "longitude_creation_mode": 'ANGLE', "longitude_divisions": 11, "longitude_object_name": "Longitude",
    "longitude_angle_step": math.radians(30),
    "torus_plane": 'XZ', "torus_major_radius": 10.0, "torus_minor_radius": 0.25, "torus_color": (0.2, 0.8, 1.0, 0.1),
    "torus_sub_collection_name": "TorusGuide", "torus_object_name": "GuideTorus",
    "use_collections": True, "collection_name": "RayGenesis_Objects",
    "latitude_sub_collection_name": "Latitudes", "longitude_sub_collection_name": "Longitudes",
    "join_torus_guides": False,
    "join_latitudes": False,
    "join_longitudes": False,
    "join_latlon_together": False,
    "highlight_sphere_color": (0.0, 0.5, 1.0, 0.1),
    "highlight_sphere_name": "HighlightSurface",
    "highlight_sub_collection_name": "HighlightSpheres",
    "highlight_sphere_start_lon": 0.0,
    "highlight_sphere_end_lon": math.radians(180.0),
    "highlight_sphere_start_lat": math.radians(-90.0),
    "highlight_sphere_end_lat": math.radians(90.0),
    "highlight_mode": 'COLOR',
    "highlight_front_image": "",
    "highlight_back_image": "",
    # ### 追加 ###: 画像変形用の初期値
    "highlight_front_image_scale": 1.0,
    "highlight_front_image_rotation": 0.0,
    "highlight_front_image_flip_x": False,
    "highlight_back_image_scale": 1.0,
    "highlight_back_image_rotation": 0.0,
    "highlight_back_image_flip_x": False,
    # ### 新規追加 ###: 画像マッピング方法の初期値
    "highlight_front_image_mapping": 'CLIP',
    "highlight_back_image_mapping": 'CLIP',
}

# ---------------------------------------------------------------------
# リンクデータ (変更なし)
# ---------------------------------------------------------------------
THIS_LINKS = [{"label": "円錐矢印 20251014", "url": "<https://memo2017.hatenlog.com/entry/2025/10/14/090711>"}]
ADDON_LINKS = [{"label": "blender 2026", "url": "<https://www.notion.so/blender-2026-280b3deba7a280499f3dc81c81f588e3"},{"label>": "立方体と3球体 20250926", "url": "<https://memo2017.hatenlog.com/entry/2025/09/27/000842"},{"label>": "blender アドオン 公開", "url": "<https://ivory-handsaw-95b.notion.site/blender-230b3deba7a280d7b610e0e3cdc178da>"}]
NEW_DOC_LINKS = [{"label": "完成品 目次", "url": "<https://mokuji000zionad.hatenablog.com/entry/2025/05/30/135936>"}]
DOC_LINKS = [{"label": "812 地球儀 経度 緯度でのコントロール 20250302", "url": "<https://sortphotos2025.hatenablog.jp/entry/2025/03/02/211757>"}]
SOCIAL_LINKS = [{"label": "X (Twitter) zionadchat", "url": "<https://x.com/zionadchat>"}]

# ---------------------------------------------------------------------
# 共通ユーティリティ関数群
# ---------------------------------------------------------------------
def get_or_create_collection(collection_name, parent_collection=None):
    if parent_collection is None: parent_collection = bpy.context.scene.collection
    if collection_name in parent_collection.children: return parent_collection.children[collection_name]
    else: new_collection = bpy.data.collections.new(collection_name); parent_collection.children.link(new_collection); return new_collection

def link_to_collection(obj, collection):
    for coll in obj.users_collection: coll.objects.unlink(obj)
    collection.objects.link(obj)

def get_or_create_material(color, name_prefix="RayGenesis_Color"):
    mat_name = f"{name_prefix}_{color[0]:.2f}_{color[1]:.2f}_{color[2]:.2f}_{color[3]:.2f}"
    if mat_name in bpy.data.materials: return bpy.data.materials[mat_name]
    mat = bpy.data.materials.new(name=mat_name); mat.use_nodes = True; bsdf = mat.node_tree.nodes.get('Principled BSDF')
    if bsdf: bsdf.inputs['Base Color'].default_value = color; bsdf.inputs['Alpha'].default_value = color[3]; mat.blend_method = 'BLEND' if color[3] < 1.0 else 'OPAQUE'
    return mat

# ### 変更 ###: 画像の繰り返し設定を追加
def _create_highlight_image_material(mat_name, color,
                                     front_image_path, front_scale, front_rotation, front_flip_x, front_mapping,
                                     back_image_path, back_scale, back_rotation, back_flip_x, back_mapping):
    use_front_img = front_image_path and os.path.exists(bpy.path.abspath(front_image_path))
    use_back_img = back_image_path and os.path.exists(bpy.path.abspath(back_image_path))

    if not use_front_img and not use_back_img:
        return get_or_create_material(color, "HighlightSurface_Fallback")

    if mat_name in bpy.data.materials:
        bpy.data.materials.remove(bpy.data.materials[mat_name])

    mat = bpy.data.materials.new(name=mat_name)
    mat.use_nodes = True
    mat.blend_method = 'BLEND'
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    nodes.clear()

    output = nodes.new(type='ShaderNodeOutputMaterial'); output.location = (400, 0)
    mix_shader = nodes.new(type='ShaderNodeMixShader'); mix_shader.location = (200, 0)
    geom = nodes.new(type='ShaderNodeNewGeometry'); geom.location = (0, -100)
    tex_coord = nodes.new(type='ShaderNodeTexCoord'); tex_coord.location = (-1000, 0)

    links.new(geom.outputs['Backfacing'], mix_shader.inputs['Fac'])
    links.new(mix_shader.outputs['Shader'], output.inputs['Surface'])

    # --- 表側のマテリアル設定 ---
    bsdf_front = nodes.new(type='ShaderNodeBsdfPrincipled'); bsdf_front.location = (0, 300)
    links.new(bsdf_front.outputs['BSDF'], mix_shader.inputs[1])

    if use_front_img:
        try:
            img = bpy.data.images.load(bpy.path.abspath(front_image_path))
            tex = nodes.new(type='ShaderNodeTexImage'); tex.image = img; tex.location = (-200, 300)
            tex.extension = front_mapping # ### 新規追加 ###: 画像の繰り返し方法を設定
            
            # 中心を基点に変形させるためのノード群 (表側)
            vec_subtract = nodes.new(type='ShaderNodeVectorMath'); vec_subtract.operation = 'SUBTRACT'; vec_subtract.location = (-800, 300)
            vec_subtract.inputs[1].default_value = (0.5, 0.5, 0)
            mapping = nodes.new(type='ShaderNodeMapping'); mapping.location = (-600, 300)
            vec_add = nodes.new(type='ShaderNodeVectorMath'); vec_add.operation = 'ADD'; vec_add.location = (-400, 300)
            vec_add.inputs[1].default_value = (0.5, 0.5, 0)
            
            # マッピング設定 (表側)
            mapping.inputs['Rotation'].default_value[2] = front_rotation
            scale_val = 1.0 / front_scale if front_scale != 0 else 1.0
            scale_x = scale_val * (-1 if front_flip_x else 1)
            mapping.inputs['Scale'].default_value[0] = scale_x
            mapping.inputs['Scale'].default_value[1] = scale_val
            
            # ノード接続 (表側)
            links.new(tex_coord.outputs['UV'], vec_subtract.inputs[0])
            links.new(vec_subtract.outputs['Vector'], mapping.inputs['Vector'])
            links.new(mapping.outputs['Vector'], vec_add.inputs[0])
            links.new(vec_add.outputs['Vector'], tex.inputs['Vector'])
            
            links.new(tex.outputs['Color'], bsdf_front.inputs['Base Color'])
            links.new(tex.outputs['Alpha'], bsdf_front.inputs['Alpha'])
        except RuntimeError:
            bsdf_front.inputs['Base Color'].default_value = color
            bsdf_front.inputs['Alpha'].default_value = color[3]
    else:
        bsdf_front.inputs['Base Color'].default_value = color
        bsdf_front.inputs['Alpha'].default_value = color[3]

    # --- 裏側のマテリアル設定 ---
    bsdf_back = nodes.new(type='ShaderNodeBsdfPrincipled'); bsdf_back.location = (0, -300)
    links.new(bsdf_back.outputs['BSDF'], mix_shader.inputs[2])

    if use_back_img:
        try:
            img = bpy.data.images.load(bpy.path.abspath(back_image_path))
            tex = nodes.new(type='ShaderNodeTexImage'); tex.image = img; tex.location = (-200, -300)
            tex.extension = back_mapping # ### 新規追加 ###: 画像の繰り返し方法を設定

            # 中心を基点に変形させるためのノード群 (裏側)
            vec_subtract = nodes.new(type='ShaderNodeVectorMath'); vec_subtract.operation = 'SUBTRACT'; vec_subtract.location = (-800, -300)
            vec_subtract.inputs[1].default_value = (0.5, 0.5, 0)
            mapping = nodes.new(type='ShaderNodeMapping'); mapping.location = (-600, -300)
            vec_add = nodes.new(type='ShaderNodeVectorMath'); vec_add.operation = 'ADD'; vec_add.location = (-400, -300)
            vec_add.inputs[1].default_value = (0.5, 0.5, 0)

            # マッピング設定 (裏側)
            mapping.inputs['Rotation'].default_value[2] = back_rotation
            scale_val = 1.0 / back_scale if back_scale != 0 else 1.0
            scale_x = scale_val * (-1 if back_flip_x else 1)
            mapping.inputs['Scale'].default_value[0] = scale_x
            mapping.inputs['Scale'].default_value[1] = scale_val

            # ノード接続 (裏側)
            links.new(tex_coord.outputs['UV'], vec_subtract.inputs[0])
            links.new(vec_subtract.outputs['Vector'], mapping.inputs['Vector'])
            links.new(mapping.outputs['Vector'], vec_add.inputs[0])
            links.new(vec_add.outputs['Vector'], tex.inputs['Vector'])
            
            links.new(tex.outputs['Color'], bsdf_back.inputs['Base Color'])
            links.new(tex.outputs['Alpha'], bsdf_back.inputs['Alpha'])
        except RuntimeError:
            bsdf_back.inputs['Base Color'].default_value = color
            bsdf_back.inputs['Alpha'].default_value = color[3]
    else:
        bsdf_back.inputs['Base Color'].default_value = color
        bsdf_back.inputs['Alpha'].default_value = color[3]

    return mat

def assign_material(obj, material):
    if obj.data.materials: obj.data.materials[0] = material
    else: obj.data.materials.append(material)

def get_unique_sub_collection(main_coll, sub_coll_name_base):
    i = 1
    while True:
        sub_coll_name = f"{sub_coll_name_base}_{i:03d}"
        if sub_coll_name not in main_coll.children: return get_or_create_collection(sub_coll_name, main_coll)
        i += 1

def create_torus_object_base(radius, thickness):
    if radius < 0.0001: return None
    bpy.ops.mesh.primitive_torus_add(location=(0,0,0), rotation=(0,0,0), major_radius=radius, minor_radius=thickness, major_segments=128, minor_segments=16)
    return bpy.context.active_object

def join_objects(context, objects_to_join, new_name):
    if not objects_to_join: return None
    valid_objects = [obj for obj in objects_to_join if obj and obj.type == 'MESH']
    if len(valid_objects) < 2:
        if valid_objects: valid_objects[0].name = new_name; return valid_objects[0]
        return None
    view_layer = context.view_layer; bpy.ops.object.select_all(action='DESELECT')
    for obj in valid_objects: obj.select_set(True)
    view_layer.objects.active = valid_objects[0]; bpy.ops.object.join(); joined_object = view_layer.objects.active
    if joined_object: joined_object.name = new_name
    return joined_object

# ---------------------------------------------------------------------
# オブジェクト生成ヘルパー関数
# ---------------------------------------------------------------------
def _create_latitude_objects(props, mat):
    axis=props.axis_reference; created_objects=[]; latitudes_deg=[]
    if props.latitude_creation_mode == 'ANGLE':
        latitudes_deg.append(0.0); angle_step = math.degrees(props.latitude_angle_step)
        if angle_step < 0.01: return []
        curr=angle_step
        while curr<90.0-0.01: latitudes_deg.append(curr); latitudes_deg.append(-curr); curr+=angle_step
    else:
        n=props.latitude_divisions; num_segments=n+1; step_deg=90.0/num_segments; latitudes_deg.append(0.0)
        for i in range(1, num_segments): deg=i*step_deg; latitudes_deg.append(deg); latitudes_deg.append(-deg)
    base_rot=(0,0,0)
    if axis=='Y': base_rot=(math.pi/2,0,0)
    elif axis=='X': base_rot=(0,math.pi/2,0)
    for lat_deg in latitudes_deg:
        lat_rad=math.radians(lat_deg); r_ring=props.lalo_radius*math.cos(lat_rad); offset=props.lalo_radius*math.sin(lat_rad)
        obj_name=f"{props.latitude_object_name}_{axis}_{int(round(lat_deg))}"
        obj=create_torus_object_base(r_ring, props.latitude_thickness)
        if not obj: continue
        obj.name=obj_name; assign_material(obj, mat); obj.rotation_euler=base_rot
        loc=mathutils.Vector((0,0,offset))
        if axis=='Y': loc = mathutils.Matrix.Rotation(math.pi/2,3,'X') @ loc
        elif axis=='X': loc = mathutils.Matrix.Rotation(math.pi/2,3,'Y') @ loc
        obj.location=loc; created_objects.append(obj)
    return created_objects

def _create_longitude_objects(props, mat):
    axis=props.axis_reference; created_objects=[]; angles_rad=[]
    if props.longitude_creation_mode == 'ANGLE':
        step=props.longitude_angle_step; curr=0.0
        while curr < 2*math.pi-1e-5: angles_rad.append(curr); curr+=step
    else:
        n=props.longitude_divisions; num_segments=n+1; step=(2*math.pi)/num_segments
        for i in range(num_segments): angles_rad.append(i*step)
    base_orientation=mathutils.Matrix.Rotation(math.pi/2,4,'X'); axis_align=mathutils.Matrix.Identity(4)
    if axis=='Y': axis_align=mathutils.Matrix.Rotation(-math.pi/2,4,'X')
    elif axis=='X': axis_align=mathutils.Matrix.Rotation(math.pi/2,4,'Y')
    for angle in angles_rad:
        deg=math.degrees(angle); obj_name=f"{props.longitude_object_name}_{axis}_{int(round(deg))%360:03d}"
        obj=create_torus_object_base(props.lalo_radius, props.longitude_thickness)
        if not obj: continue
        obj.name=obj_name; obj.location=(0,0,0)
        assign_material(obj, mat)
        final_mat = axis_align @ mathutils.Matrix.Rotation(angle,4,'Z') @ base_orientation
        obj.rotation_euler=final_mat.to_euler(); created_objects.append(obj)
    return created_objects

def _create_highlight_surface(context):
    props = context.scene.ido_keido_001_props
    
    # 1. オブジェクトの生成と命名
    bpy.ops.mesh.primitive_uv_sphere_add(segments=128, ring_count=64, radius=props.lalo_radius, location=(0, 0, 0))
    obj = context.active_object
    obj.visible_shadow = False
    
    start_lon_deg = round(math.degrees(props.highlight_sphere_start_lon))
    end_lon_deg = round(math.degrees(props.highlight_sphere_end_lon))
    start_lat_deg = round(math.degrees(props.highlight_sphere_start_lat))
    end_lat_deg = round(math.degrees(props.highlight_sphere_end_lat))
    obj.name = f"{props.highlight_sphere_name}_Lon{start_lon_deg}-{end_lon_deg}_Lat{start_lat_deg}-{end_lat_deg}"

    # 2. マテリアルの準備と割り当て
    if props.highlight_mode == 'IMAGE':
        # ### 変更 ###: 画像の繰り返し設定をマテリアル生成関数に渡す
        highlight_mat = _create_highlight_image_material(
            mat_name=f"{obj.name}_Material", color=props.highlight_sphere_color,
            front_image_path=props.highlight_front_image,
            front_scale=props.highlight_front_image_scale,
            front_rotation=props.highlight_front_image_rotation,
            front_flip_x=props.highlight_front_image_flip_x,
            front_mapping=props.highlight_front_image_mapping,
            back_image_path=props.highlight_back_image,
            back_scale=props.highlight_back_image_scale,
            back_rotation=props.highlight_back_image_rotation,
            back_flip_x=props.highlight_back_image_flip_x,
            back_mapping=props.highlight_back_image_mapping
        )
    else:
        highlight_mat = get_or_create_material(props.highlight_sphere_color, "HighlightSurface_Color")
    transparent_mat = get_or_create_material((0,0,0,0), "TransparentSurface")
    
    obj.data.materials.append(highlight_mat)
    obj.data.materials.append(transparent_mat)
    
    # 3. UV展開 (画像テクスチャのため)
    bpy.ops.object.mode_set(mode='EDIT')
    bpy.ops.mesh.select_all(action='SELECT')
    bpy.ops.uv.sphere_project()
    
    # 4. マテリアルの初期設定(一度すべてを透明に)
    bpy.ops.mesh.select_all(action='SELECT')
    obj.active_material_index = 1
    bpy.ops.object.material_slot_assign()
    bpy.ops.mesh.select_all(action='DESELECT')
    bpy.ops.object.mode_set(mode='OBJECT')

    # 5. 範囲内のポリゴンを選択
    start_lon = math.degrees(props.highlight_sphere_start_lon)
    end_lon = math.degrees(props.highlight_sphere_end_lon)
    start_lat = math.degrees(props.highlight_sphere_start_lat)
    end_lat = math.degrees(props.highlight_sphere_end_lat)

    mesh = obj.data
    for poly in mesh.polygons:
        poly_center_world = obj.matrix_world @ poly.center
        lon_rad = math.atan2(poly_center_world.y, poly_center_world.x)
        lon_deg = math.degrees(lon_rad)
        if lon_deg < 0: lon_deg += 360
        
        asin_arg = max(-1.0, min(1.0, poly_center_world.z / props.lalo_radius))
        lat_rad = math.asin(asin_arg)
        lat_deg = math.degrees(lat_rad)

        is_in_lon_range = False
        if start_lon <= end_lon:
            if start_lon <= lon_deg <= end_lon: is_in_lon_range = True
        else:
            if lon_deg >= start_lon or lon_deg <= end_lon: is_in_lon_range = True
        
        is_in_lat_range = (start_lat <= lat_deg <= end_lat)

        if is_in_lon_range and is_in_lat_range:
            poly.select = True

    # 6. 選択したポリゴンにハイライトマテリアルを割り当て
    bpy.ops.object.mode_set(mode='EDIT')
    obj.active_material_index = 0
    bpy.ops.object.material_slot_assign()
    
    # 7. 後処理
    bpy.ops.mesh.select_all(action='DESELECT')
    bpy.ops.object.mode_set(mode='OBJECT')
    
    return obj

# ---------------------------------------------------------------------
# オペレーター (変更なし)
# ---------------------------------------------------------------------
class IDO_KEIDO_001_OT_CreateTorusGuide(Operator):
    bl_idname = f"{PREFIX}.create_torus_guide"; bl_label = "円環ガイドを作成"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props=context.scene.ido_keido_001_props; torus_obj=create_torus_object_base(props.torus_major_radius, props.torus_minor_radius)
        if not torus_obj: return {'CANCELLED'}
        plane_name=props.torus_plane; torus_obj.name=f"{props.torus_object_name}_{plane_name}"
        if plane_name=='XZ': torus_obj.rotation_euler.x=math.pi/2
        elif plane_name=='YZ': torus_obj.rotation_euler.y=math.pi/2
        assign_material(torus_obj, get_or_create_material(props.torus_color, "TorusGuide"))
        if props.use_collections:
            main_coll=get_or_create_collection(props.collection_name); sub_coll=get_unique_sub_collection(main_coll, props.torus_sub_collection_name)
            link_to_collection(torus_obj, sub_coll); self.report({'INFO'}, f"コレクション「{sub_coll.name}」に作成。")
        else: self.report({'INFO'}, "円環ガイドを作成しました。")
        return {'FINISHED'}

class IDO_KEIDO_001_OT_CreateTorusGuideAllPlanes(Operator):
    bl_idname = f"{PREFIX}.create_torus_guide_all_planes"; bl_label = "3平面に円環ガイドを作成"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props, sub_coll = context.scene.ido_keido_001_props, None
        if props.use_collections: main_coll=get_or_create_collection(props.collection_name); sub_coll=get_unique_sub_collection(main_coll, f"{props.torus_sub_collection_name}_3Planes")
        planes_rot={'XY': (0,0,0), 'XZ':(math.pi/2,0,0), 'YZ':(0,math.pi/2,0)}; mat=get_or_create_material(props.torus_color, "TorusGuide"); created_objects=[]
        for name, rotation in planes_rot.items():
            torus_obj=create_torus_object_base(props.torus_major_radius, props.torus_minor_radius)
            if not torus_obj: continue
            torus_obj.name=f"{props.torus_object_name}_{name}"; torus_obj.rotation_euler=rotation; assign_material(torus_obj, mat); created_objects.append(torus_obj)
        if props.join_torus_guides and created_objects:
            joined_name = f"{props.torus_object_name}_Joined_3Planes"; joined_obj = join_objects(context, created_objects, joined_name)
            if sub_coll and joined_obj: link_to_collection(joined_obj, sub_coll)
            self.report({'INFO'}, f"3つの円環ガイドを一体化し「{sub_coll.name}」に作成。")
        elif created_objects:
            if sub_coll:
                for obj in created_objects: link_to_collection(obj, sub_coll)
                self.report({'INFO'}, f"3つの円環ガイドを「{sub_coll.name}」に作成。")
            else: self.report({'INFO'}, "3つの円環ガイドを作成しました。")
        return {'FINISHED'}

class IDO_KEIDO_001_OT_CreateLatitudeLines(Operator):
    bl_idname = f"{PREFIX}.create_latitude_lines"; bl_label = "緯線を作成"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props=context.scene.ido_keido_001_props; mat=get_or_create_material(props.latitude_color, "LatitudeColor")
        created_objects = _create_latitude_objects(props, mat)
        if not created_objects: return {'CANCELLED'}
        if props.join_latitudes:
            joined_name = f"{props.latitude_object_name}_{props.axis_reference}_Joined"
            joined_obj = join_objects(context, created_objects, joined_name)
            if props.use_collections and joined_obj:
                main_coll = get_or_create_collection(props.collection_name); sub_coll_name = f"{props.latitude_sub_collection_name}_{props.axis_reference}_Joined"
                sub_coll = get_unique_sub_collection(main_coll, sub_coll_name); link_to_collection(joined_obj, sub_coll)
                self.report({'INFO'}, f"緯線を一体化し「{sub_coll.name}」に作成。")
            else: self.report({'INFO'}, "緯線を一体化して作成しました。")
        else:
            if props.use_collections:
                main_coll=get_or_create_collection(props.collection_name); sub_coll=get_unique_sub_collection(main_coll, f"{props.latitude_sub_collection_name}_{props.axis_reference}")
                for obj in created_objects: link_to_collection(obj, sub_coll)
                self.report({'INFO'}, f"緯線を「{sub_coll.name}」に作成。")
            else: self.report({'INFO'}, f"{len(created_objects)}本の緯線を作成しました。")
        return {'FINISHED'}

class IDO_KEIDO_001_OT_CreateLongitudeLines(Operator):
    bl_idname = f"{PREFIX}.create_longitude_lines"; bl_label = "経線を作成"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props=context.scene.ido_keido_001_props; mat=get_or_create_material(props.longitude_color, "LongitudeColor")
        created_objects = _create_longitude_objects(props, mat)
        if not created_objects: return {'CANCELLED'}
        if props.join_longitudes:
            joined_name = f"{props.longitude_object_name}_{props.axis_reference}_Joined"
            joined_obj = join_objects(context, created_objects, joined_name)
            if props.use_collections and joined_obj:
                main_coll = get_or_create_collection(props.collection_name); sub_coll_name = f"{props.longitude_sub_collection_name}_{props.axis_reference}_Joined"
                sub_coll = get_unique_sub_collection(main_coll, sub_coll_name); link_to_collection(joined_obj, sub_coll)
                self.report({'INFO'}, f"経線を一体化し「{sub_coll.name}」に作成。")
            else: self.report({'INFO'}, "経線を一体化して作成しました。")
        else:
            if props.use_collections:
                main_coll=get_or_create_collection(props.collection_name); sub_coll=get_unique_sub_collection(main_coll, f"{props.longitude_sub_collection_name}_{props.axis_reference}")
                for obj in created_objects: link_to_collection(obj, sub_coll)
                self.report({'INFO'}, f"経線を「{sub_coll.name}」に作成。")
            else: self.report({'INFO'}, f"{len(created_objects)}本の経線を作成しました。")
        return {'FINISHED'}

class IDO_KEIDO_001_OT_CreateHighlightSurface(Operator):
    bl_idname = f"{PREFIX}.create_highlight_surface"
    bl_label = "球体表面ハイライトを作成"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = context.scene.ido_keido_001_props
        surface_obj = _create_highlight_surface(context)
        if props.use_collections:
            main_coll = get_or_create_collection(props.collection_name)
            sub_coll = get_unique_sub_collection(main_coll, props.highlight_sub_collection_name)
            link_to_collection(surface_obj, sub_coll)
            self.report({'INFO'}, f"ハイライト用表面を「{sub_coll.name}」に作成しました。")
        else:
            self.report({'INFO'}, "ハイライト用表面を作成しました。")
        return {'FINISHED'}

class IDO_KEIDO_001_OT_CreateAllLatLonLines(Operator):
    bl_idname = f"{PREFIX}.create_all_latlon_lines"; bl_label = "緯線と経線を作成"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props=context.scene.ido_keido_001_props
        lat_mat=get_or_create_material(props.latitude_color, "LatitudeColor"); lon_mat=get_or_create_material(props.longitude_color, "LongitudeColor")
        lat_objects=_create_latitude_objects(props,lat_mat); lon_objects=_create_longitude_objects(props,lon_mat)
        if not lat_objects and not lon_objects: return {'CANCELLED'}
        processed_lats=[]; processed_lons=[]
        if props.join_latitudes and lat_objects:
            obj=join_objects(context,lat_objects,f"{props.latitude_object_name}_{props.axis_reference}_Joined");
            if obj: processed_lats.append(obj)
        else: processed_lats.extend(lat_objects)
        if props.join_longitudes and lon_objects:
            obj=join_objects(context,lon_objects,f"{props.longitude_object_name}_{props.axis_reference}_Joined");
            if obj: processed_lons.append(obj)
        else: processed_lons.extend(lon_objects)
        if props.join_latlon_together:
            all_objs=processed_lats+processed_lons
            if all_objs:
                final_obj=join_objects(context,all_objs,f"LatLon_Sphere_{props.axis_reference}_Joined")
                if props.use_collections and final_obj:
                    main_coll=get_or_create_collection(props.collection_name); sub_coll=get_unique_sub_collection(main_coll,f"LatLon_Sphere_{props.axis_reference}_Joined")
                    link_to_collection(final_obj,sub_coll); self.report({'INFO'},f"緯線・経線を一体化し「{sub_coll.name}」に作成。")
                else: self.report({'INFO'},"緯線・経線を一体化して作成しました。")
        else:
            if props.use_collections:
                main_coll=get_or_create_collection(props.collection_name)
                if processed_lats:
                    lat_sub_coll_name=f"{props.latitude_sub_collection_name}_{props.axis_reference}";
                    if props.join_latitudes: lat_sub_coll_name+="_Joined"
                    lat_sub_coll=get_unique_sub_collection(main_coll,lat_sub_coll_name)
                    for obj in processed_lats: link_to_collection(obj,lat_sub_coll)
                if processed_lons:
                    lon_sub_coll_name=f"{props.longitude_sub_collection_name}_{props.axis_reference}";
                    if props.join_longitudes: lon_sub_coll_name+="_Joined"
                    lon_sub_coll=get_unique_sub_collection(main_coll,lon_sub_coll_name)
                    for obj in processed_lons: link_to_collection(obj,lon_sub_coll)
                self.report({'INFO'},"緯線と経線をそれぞれのコレクションに作成。")
            else: self.report({'INFO'},"緯線と経線を作成しました。")
        return {'FINISHED'}

class IDO_KEIDO_001_OT_ResetTorusGuideSettings(Operator):
    bl_idname = f"{PREFIX}.reset_torus_guide_settings"; bl_label = "円環ガイド設定をリセット"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props=context.scene.ido_keido_001_props; defaults=addon_defaults
        props.torus_plane=defaults["torus_plane"]; props.torus_major_radius=defaults["torus_major_radius"]
        props.torus_minor_radius=defaults["torus_minor_radius"]; props.torus_color=defaults["torus_color"]
        props.torus_sub_collection_name=defaults["torus_sub_collection_name"]; props.torus_object_name=defaults["torus_object_name"]
        props.join_torus_guides = defaults["join_torus_guides"]; self.report({'INFO'}, "円環ガイドの設定を初期値にリセットしました。"); return {'FINISHED'}

class IDO_KEIDO_001_OT_ResetLatLonSettings(Operator):
    bl_idname = f"{PREFIX}.reset_latlon_settings"; bl_label = "緯線・経線設定をリセット"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props=context.scene.ido_keido_001_props; defaults=addon_defaults
        props.axis_reference=defaults["axis_reference"]; props.lalo_radius=defaults["lalo_radius"]
        props.latitude_thickness=defaults["latitude_thickness"]
        props.longitude_thickness=defaults["longitude_thickness"]
        props.latitude_color=defaults["latitude_color"]; props.longitude_color=defaults["longitude_color"]
        props.latitude_creation_mode=defaults["latitude_creation_mode"]; props.latitude_angle_step=defaults["latitude_angle_step"]
        props.latitude_divisions=defaults["latitude_divisions"]; props.latitude_object_name=defaults["latitude_object_name"]
        props.longitude_creation_mode=defaults["longitude_creation_mode"]; props.longitude_angle_step=defaults["longitude_angle_step"]
        props.longitude_divisions=defaults["longitude_divisions"]; props.longitude_object_name=defaults["longitude_object_name"]
        props.join_latitudes = defaults["join_latitudes"]; props.join_longitudes = defaults["join_longitudes"]
        props.join_latlon_together = defaults["join_latlon_together"]
        props.highlight_sphere_color = defaults["highlight_sphere_color"]
        props.highlight_sphere_name = defaults["highlight_sphere_name"]
        props.highlight_sphere_start_lon = defaults["highlight_sphere_start_lon"]
        props.highlight_sphere_end_lon = defaults["highlight_sphere_end_lon"]
        props.highlight_sphere_start_lat = defaults["highlight_sphere_start_lat"]
        props.highlight_sphere_end_lat = defaults["highlight_sphere_end_lat"]
        props.highlight_mode = defaults["highlight_mode"]
        props.highlight_front_image = defaults["highlight_front_image"]
        props.highlight_back_image = defaults["highlight_back_image"]
        # ### 追加 ###: 画像変形プロパティをリセット
        props.highlight_front_image_scale = defaults["highlight_front_image_scale"]
        props.highlight_front_image_rotation = defaults["highlight_front_image_rotation"]
        props.highlight_front_image_flip_x = defaults["highlight_front_image_flip_x"]
        props.highlight_back_image_scale = defaults["highlight_back_image_scale"]
        props.highlight_back_image_rotation = defaults["highlight_back_image_rotation"]
        props.highlight_back_image_flip_x = defaults["highlight_back_image_flip_x"]
        # ### 新規追加 ###: 画像マッピングプロパティをリセット
        props.highlight_front_image_mapping = defaults["highlight_front_image_mapping"]
        props.highlight_back_image_mapping = defaults["highlight_back_image_mapping"]
        self.report({'INFO'}, "緯線・経線の設定を初期値にリセットしました。"); return {'FINISHED'}

class IDO_KEIDO_001_OT_OpenURL(Operator):
    bl_idname = f"{PREFIX}.open_url"; bl_label = "Open URL"; url: StringProperty(default="");
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

def delayed_unregister(): unregister(); return None
class IDO_KEIDO_001_OT_RemoveAddon(Operator):
    bl_idname = f"{PREFIX}.remove_addon"; bl_label = "アドオンを無効化してUIを削除"; bl_options = {'REGISTER'}
    @classmethod
    def poll(cls, context): return not bpy.app.timers.is_registered(delayed_unregister)
    def execute(self, context):
        bpy.ops.preferences.addon_disable(module=__name__); bpy.app.timers.register(delayed_unregister)
        self.report({'INFO'}, f"アドオン '{bl_info['name']}' を無効化しました。UIを削除します。"); return {'FINISHED'}

# ---------------------------------------------------------------------
# UI パネル
# ---------------------------------------------------------------------
class IDO_KEIDO_001_PT_LatLonPanel(Panel):
    bl_label = "2. 緯線・経線 作成"; bl_idname = f"{PREFIX}_PT_latlon_panel"; bl_space_type='VIEW_3D'; bl_region_type='UI'; bl_category=ADDON_CATEGORY_NAME
    def draw(self, context):
        layout, props = self.layout, context.scene.ido_keido_001_props
        row = layout.row(align=True); row.alignment = 'RIGHT'; row.operator(IDO_KEIDO_001_OT_ResetLatLonSettings.bl_idname, text="リセット", icon='FILE_REFRESH')
        box_axis=layout.box(); row=box_axis.row(); row.label(text="極となる基準軸:", icon='EMPTY_AXIS'); row.prop(props, "axis_reference", text="")
        box_common=layout.box(); box_common.label(text="共通球体設定"); col=box_common.column(align=True); col.prop(props, "lalo_radius")
        row=layout.row(); col1=row.column(); box1=col1.box(); box1.label(text="緯線 (Parallels)", icon='MESH_GRID'); cc=box1.column(align=True)
        cc.prop(props, "latitude_thickness")
        cc.prop(props,"latitude_color",text=""); cc.prop(props,"latitude_creation_mode",text="");
        if props.latitude_creation_mode=='ANGLE': cc.prop(props, "latitude_angle_step")
        else: cc.prop(props, "latitude_divisions")
        cc.prop(props, "join_latitudes", text="一体化")
        col2=row.column(); box2=col2.box(); box2.label(text="経線 (Meridians)", icon='MESH_UVSPHERE'); cc=box2.column(align=True)
        cc.prop(props, "longitude_thickness")
        cc.prop(props,"longitude_color",text=""); cc.prop(props,"longitude_creation_mode",text="")
        if props.longitude_creation_mode=='ANGLE': cc.prop(props, "longitude_angle_step")
        else: cc.prop(props, "longitude_divisions")
        cc.prop(props, "join_longitudes", text="一体化")
        box_name=layout.box(); box_name.label(text="命名とコレクション"); col=box_name.column(align=True)
        col.prop(props,"latitude_object_name",text="緯線名Prefix"); col.prop(props,"longitude_object_name",text="経線名Prefix")
        layout.separator(); axis_str=f"({props.axis_reference}軸基準)"; row=layout.row(align=True); row.scale_y=1.2
        row.operator(IDO_KEIDO_001_OT_CreateLatitudeLines.bl_idname,text=f"緯線を作成 {axis_str}",icon='MESH_GRID'); row.operator(IDO_KEIDO_001_OT_CreateLongitudeLines.bl_idname,text=f"経線を作成 {axis_str}",icon='MESH_UVSPHERE')
        box_join_all=layout.box(); box_join_all.prop(props,"join_latlon_together",text="緯線と経線を最終的に結合")
        row2=layout.row(); row2.scale_y=1.5; row2.operator(IDO_KEIDO_001_OT_CreateAllLatLonLines.bl_idname,text=f"緯線と経線を両方作成 {axis_str}",icon='WORLD')
        
        layout.separator(factor=2)
        box_highlight = layout.box()
        box_highlight.label(text="3. 球体表面ハイライト", icon='MATERIAL')
        col_highlight = box_highlight.column(align=True)
        
        col_highlight.prop(props, "highlight_mode", expand=True)
        
        # ### 変更 ###: 画像の繰り返し設定UIを追加
        if props.highlight_mode == 'IMAGE':
            img_box = col_highlight.box()
            img_box.prop(props, "highlight_front_image", text="オモテ画像")
            row_front_map = img_box.row()
            row_front_map.prop(props, "highlight_front_image_mapping", expand=True)
            row_front_transform = img_box.row(align=True)
            row_front_transform.prop(props, "highlight_front_image_scale")
            row_front_transform.prop(props, "highlight_front_image_rotation")
            row_front_transform.prop(props, "highlight_front_image_flip_x")
            
            img_box.separator()
            
            img_box.prop(props, "highlight_back_image", text="ウラ画像")
            row_back_map = img_box.row()
            row_back_map.prop(props, "highlight_back_image_mapping", expand=True)
            row_back_transform = img_box.row(align=True)
            row_back_transform.prop(props, "highlight_back_image_scale")
            row_back_transform.prop(props, "highlight_back_image_rotation")
            row_back_transform.prop(props, "highlight_back_image_flip_x")
        
        col_highlight.prop(props, "highlight_sphere_color", text="色 (Fallback)")
        
        row_lon_range = col_highlight.row(align=True)
        row_lon_range.prop(props, "highlight_sphere_start_lon")
        row_lon_range.prop(props, "highlight_sphere_end_lon")
        
        row_lat_range = col_highlight.row(align=True)
        row_lat_range.prop(props, "highlight_sphere_start_lat")
        row_lat_range.prop(props, "highlight_sphere_end_lat")
        
        col_highlight.prop(props, "highlight_sphere_name", text="オブジェクト名")
        col_highlight.prop(props, "highlight_sub_collection_name", text="コレクション名")
        row_highlight = box_highlight.row()
        row_highlight.scale_y = 1.5
        row_highlight.operator(IDO_KEIDO_001_OT_CreateHighlightSurface.bl_idname, text="球体ハイライトを作成", icon='ADD')

class IDO_KEIDO_001_PT_LinksPanel(Panel):
    bl_label="8. リンク集"; bl_idname=f"{PREFIX}_PT_links"; bl_space_type='VIEW_3D'; bl_region_type='UI'; bl_category=ADDON_CATEGORY_NAME; bl_options={'DEFAULT_CLOSED'}
    def draw(self, context):
        layout=self.layout
        def draw_links_in_box(link_list, title):
            box=layout.box(); box.label(text=title)
            col=box.column(align=True)
            for link in link_list:
                op=col.operator(IDO_KEIDO_001_OT_OpenURL.bl_idname, text=link["label"], icon='URL'); op.url = link["url"]
        draw_links_in_box(THIS_LINKS, "このアドオンのドキュメント:")
        draw_links_in_box(ADDON_LINKS, "関連ドキュメント:")
        draw_links_in_box(NEW_DOC_LINKS, "更新情報 / 目次:")
        draw_links_in_box(DOC_LINKS, "過去のドキュメント:")
        draw_links_in_box(SOCIAL_LINKS, "関連リンク / SNS:")

class IDO_KEIDO_001_PT_AdminPanel(Panel):
    bl_label="9. 設定"; bl_idname=f"{PREFIX}_PT_admin_panel"; bl_space_type='VIEW_3D'; bl_region_type='UI'; bl_category=ADDON_CATEGORY_NAME; bl_options={'DEFAULT_CLOSED'}
    def draw(self, context):
        layout,props=self.layout,context.scene.ido_keido_001_props; box=layout.box(); box.label(text="コレクション設定",icon='OUTLINER_COLLECTION')
        box.prop(props,"use_collections",text="コレクションを作成して整理"); row=box.row(); row.enabled=props.use_collections
        row.prop(props,"collection_name",text="ルートコレクション名"); row=box.row(); row.enabled=props.use_collections
        row.prop(props,"latitude_sub_collection_name",text="緯線サブフォルダ"); row.prop(props,"longitude_sub_collection_name",text="経線サブフォルダ")
        layout.separator(); layout.operator(IDO_KEIDO_001_OT_RemoveAddon.bl_idname, text="このアドオンを無効化", icon='CANCEL')
        layout.label(text=f"Version: {'.'.join(map(str, bl_info['version']))}")
        
# ---------------------------------------------------------------------
# プロパティ定義
# ---------------------------------------------------------------------
class IdoKeido001Properties(PropertyGroup):
    torus_plane:EnumProperty(name="配置平面",items=[('XZ',"XZ", ""),('XY',"XY",""),('YZ',"YZ","")],default=addon_defaults["torus_plane"])
    torus_major_radius:FloatProperty(name="半径",default=addon_defaults["torus_major_radius"],min=0.01,unit='LENGTH')
    torus_minor_radius:FloatProperty(name="太さ",default=addon_defaults["torus_minor_radius"],min=0.001,unit='LENGTH',precision=3)
    torus_color:FloatVectorProperty(name="色",subtype='COLOR',size=4,default=addon_defaults["torus_color"],min=0.0,max=1.0)
    torus_sub_collection_name:StringProperty(name="ガイドCol名",default=addon_defaults["torus_sub_collection_name"])
    torus_object_name:StringProperty(name="ガイドObj名",default=addon_defaults["torus_object_name"])
    axis_reference:EnumProperty(name="基準軸 (極)",items=[('Z',"Z軸", ""),('Y',"Y軸",""),('X',"X軸","")],default=addon_defaults["axis_reference"],description="この軸が球体の極となります")
    lalo_radius:FloatProperty(name="球体半径",default=addon_defaults["lalo_radius"],min=0.01,unit='LENGTH')
    latitude_thickness:FloatProperty(name="緯線の太さ",default=addon_defaults["latitude_thickness"],min=0.001,unit='LENGTH',precision=3)
    longitude_thickness:FloatProperty(name="経線の太さ",default=addon_defaults["longitude_thickness"],min=0.001,unit='LENGTH',precision=3)
    latitude_color:FloatVectorProperty(name="緯線の色",subtype='COLOR',size=4,default=addon_defaults["latitude_color"],min=0.0,max=1.0)
    longitude_color:FloatVectorProperty(name="経線の色",subtype='COLOR',size=4,default=addon_defaults["longitude_color"],min=0.0,max=1.0)
    latitude_creation_mode:EnumProperty(name="緯線生成",items=[('ANGLE',"角度指定",""),('DIVISIONS',"分割数指定","")],default=addon_defaults["latitude_creation_mode"])
    latitude_angle_step:FloatProperty(name="Step",subtype='ANGLE',default=addon_defaults["latitude_angle_step"],min=math.radians(1.0),max=math.radians(89.0))
    latitude_divisions:IntProperty(name="N (赤道~極)",description="赤道から極までをN+1分割",default=addon_defaults["latitude_divisions"],min=1,max=90)
    latitude_object_name:StringProperty(name="Lat Obj Name",default=addon_defaults["latitude_object_name"])
    longitude_creation_mode:EnumProperty(name="経線生成",items=[('ANGLE',"角度指定",""),('DIVISIONS',"分割数指定","")],default=addon_defaults["longitude_creation_mode"])
    longitude_angle_step:FloatProperty(name="Step",subtype='ANGLE',default=addon_defaults["longitude_angle_step"],min=math.radians(1.0),max=math.radians(180.0))
    longitude_divisions:IntProperty(name="N (一周)",description="360度をN+1分割",default=addon_defaults["longitude_divisions"],min=1,max=360)
    longitude_object_name:StringProperty(name="Lon Obj Name",default=addon_defaults["longitude_object_name"])
    use_collections:BoolProperty(name="Use Collections",default=addon_defaults["use_collections"])
    collection_name:StringProperty(name="Root Collection",default=addon_defaults["collection_name"])
    latitude_sub_collection_name:StringProperty(name="Lat Col Name",default=addon_defaults["latitude_sub_collection_name"])
    longitude_sub_collection_name:StringProperty(name="Lon Col Name",default=addon_defaults["longitude_sub_collection_name"])
    join_torus_guides: BoolProperty(name="一体化", description="生成した3平面のガイドを1つのオブジェクトに結合します", default=addon_defaults["join_torus_guides"])
    join_latitudes: BoolProperty(name="一体化", description="生成した緯線を1つのオブジェクトに結合します", default=addon_defaults["join_latitudes"])
    join_longitudes: BoolProperty(name="一体化", description="生成した経線を1つのオブジェクトに結合します", default=addon_defaults["join_longitudes"])
    join_latlon_together: BoolProperty(name="緯線と経線を結合", description="生成した緯線と経線をすべて1つのオブジェクトに結合します", default=addon_defaults["join_latlon_together"])
    highlight_sphere_color:FloatVectorProperty(name="色",subtype='COLOR',size=4,default=addon_defaults["highlight_sphere_color"],min=0.0,max=1.0)
    highlight_sphere_name:StringProperty(name="名前",default=addon_defaults["highlight_sphere_name"])
    highlight_sub_collection_name:StringProperty(name="コレクション名",default=addon_defaults["highlight_sub_collection_name"])
    
    highlight_sphere_start_lon:FloatProperty(name="経度 開始",description="ハイライトを開始する経度 (0-360度, +X軸が0度)",default=addon_defaults["highlight_sphere_start_lon"],min=0.0,max=math.radians(360.0),unit='ROTATION',subtype='ANGLE',precision=1)
    highlight_sphere_end_lon:FloatProperty(name="経度 終了",description="ハイライトを終了する経度 (0-360度, +X軸が0度)",default=addon_defaults["highlight_sphere_end_lon"],min=0.0,max=math.radians(360.0),unit='ROTATION',subtype='ANGLE',precision=1)
    highlight_sphere_start_lat:FloatProperty(name="緯度 開始",description="ハイライトを開始する緯度 (-90 - +90度)",default=addon_defaults["highlight_sphere_start_lat"],min=math.radians(-90.0),max=math.radians(90.0),unit='ROTATION',subtype='ANGLE',precision=1)
    highlight_sphere_end_lat:FloatProperty(name="緯度 終了",description="ハイライトを終了する緯度 (-90 - +90度)",default=addon_defaults["highlight_sphere_end_lat"],min=math.radians(-90.0),max=math.radians(90.0),unit='ROTATION',subtype='ANGLE',precision=1)
    
    highlight_mode: EnumProperty(
        name="モード",
        items=[('COLOR', "色のみ", "指定した色でハイライトします"),
               ('IMAGE', "画像テクスチャ", "表裏に別々の画像を貼り付けます")],
        default=addon_defaults["highlight_mode"]
    )
    
    highlight_front_image: StringProperty(name="オモテ画像",description="ハイライト表面に貼る画像 (表側)",default=addon_defaults["highlight_front_image"],subtype='FILE_PATH')
    highlight_back_image: StringProperty(name="ウラ画像",description="ハイライト表面に貼る画像 (裏側)",default=addon_defaults["highlight_back_image"],subtype='FILE_PATH')
    
    # ### 追加 ###: 画像変形プロパティ
    highlight_front_image_scale: FloatProperty(name="縮小割合", description="画像の縮小・拡大率。2を指定すると1/2のサイズになります", default=addon_defaults["highlight_front_image_scale"], min=0.01, soft_max=10.0, precision=2)
    highlight_front_image_rotation: FloatProperty(name="回転", description="画像をZ軸中心に回転します", subtype='ANGLE', default=addon_defaults["highlight_front_image_rotation"], unit='ROTATION')
    highlight_front_image_flip_x: BoolProperty(name="左右反転", description="画像を左右に反転します", default=addon_defaults["highlight_front_image_flip_x"])
    
    highlight_back_image_scale: FloatProperty(name="縮小割合", description="画像の縮小・拡大率。2を指定すると1/2のサイズになります", default=addon_defaults["highlight_back_image_scale"], min=0.01, soft_max=10.0, precision=2)
    highlight_back_image_rotation: FloatProperty(name="回転", description="画像をZ軸中心に回転します", subtype='ANGLE', default=addon_defaults["highlight_back_image_rotation"], unit='ROTATION')
    highlight_back_image_flip_x: BoolProperty(name="左右反転", description="画像を左右に反転します", default=addon_defaults["highlight_back_image_flip_x"])
    
    # ### 新規追加 ###: 画像の繰り返し設定プロパティ
    highlight_front_image_mapping: EnumProperty(
        name="貼り方",
        items=[('CLIP', "1つだけ貼る", "画像を縮小して中央に1つだけ配置します"),
               ('REPEAT', "周辺に繰り返しで貼る", "画像をタイル状に繰り返し配置します")],
        default=addon_defaults["highlight_front_image_mapping"]
    )
    highlight_back_image_mapping: EnumProperty(
        name="貼り方",
        items=[('CLIP', "1つだけ貼る", "画像を縮小して中央に1つだけ配置します"),
               ('REPEAT', "周辺に繰り返しで貼る", "画像をタイル状に繰り返し配置します")],
        default=addon_defaults["highlight_back_image_mapping"]
    )

# ---------------------------------------------------------------------
# 登録クラスリスト (変更なし)
# ---------------------------------------------------------------------
classes = (
    IdoKeido001Properties, IDO_KEIDO_001_OT_CreateTorusGuide, IDO_KEIDO_001_OT_CreateTorusGuideAllPlanes,
    IDO_KEIDO_001_OT_CreateLatitudeLines, IDO_KEIDO_001_OT_CreateLongitudeLines,
    IDO_KEIDO_001_OT_CreateHighlightSurface,
    IDO_KEIDO_001_OT_CreateAllLatLonLines,
    IDO_KEIDO_001_OT_ResetTorusGuideSettings, IDO_KEIDO_001_OT_ResetLatLonSettings, IDO_KEIDO_001_OT_OpenURL, IDO_KEIDO_001_OT_RemoveAddon,
    IDO_KEIDO_001_PT_LatLonPanel, IDO_KEIDO_001_PT_LinksPanel, IDO_KEIDO_001_PT_AdminPanel,
)

# ---------------------------------------------------------------------
# 登録・登録解除処理 (変更なし)
# ---------------------------------------------------------------------
def register():
    for cls in classes: bpy.utils.register_class(cls)
    bpy.types.Scene.ido_keido_001_props = PointerProperty(type=IdoKeido001Properties)

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

if __name__ == "__main__":
    register()