Sidebar", "description": "Parametric Globe Generator for Blender 5.x", "category": "3D View", } import bpy import bmesh import math from mathutils import Matrix, Vector # ========================================== # 1. コード先頭の設定項目 (ここだけを変更して使い回す) # ========================================== TITLE = "Globe Addon" TAB_NAME = "GlobeTab" PANEL_TITLE = "Globe Panel" PREFIX_NAME = "globe_addon" COLLECTION_MAIN = "Globe_Main" COLLECTION_SUB = "Globe_Sub" OBJ_NAME = "GlobeObj" LINK_URL = "https://app.notion.com/p/20260708-397f5dacaf438022ad34c17f3d743608" SPHERE_RADIUS = 10.0 SPHERE_THICKNESS = 1.0 SPHERE_COLOR = (0.2, 0.5, 0.8, 1.0) SPHERE_INNER_COLOR = (0.8, 0.4, 0.1, 1.0) # 追加: 裏面(内側)のデフォルト色 LATITUDE_COUNT = 3 LONGITUDE_COUNT = 4 HOLE_RADIUS = 1.0 CONE_RADIUS = 2.0 CONE_DEPTH = 5.0 CONE_COLOR = (0.8, 0.2, 0.2, 1.0) # ======"> Sidebar", "description": "Parametric Globe Generator for Blender 5.x", "category": "3D View", } import bpy import bmesh import math from mathutils import Matrix, Vector # ========================================== # 1. コード先頭の設定項目 (ここだけを変更して使い回す) # ========================================== TITLE = "Globe Addon" TAB_NAME = "GlobeTab" PANEL_TITLE = "Globe Panel" PREFIX_NAME = "globe_addon" COLLECTION_MAIN = "Globe_Main" COLLECTION_SUB = "Globe_Sub" OBJ_NAME = "GlobeObj" LINK_URL = "https://app.notion.com/p/20260708-397f5dacaf438022ad34c17f3d743608" SPHERE_RADIUS = 10.0 SPHERE_THICKNESS = 1.0 SPHERE_COLOR = (0.2, 0.5, 0.8, 1.0) SPHERE_INNER_COLOR = (0.8, 0.4, 0.1, 1.0) # 追加: 裏面(内側)のデフォルト色 LATITUDE_COUNT = 3 LONGITUDE_COUNT = 4 HOLE_RADIUS = 1.0 CONE_RADIUS = 2.0 CONE_DEPTH = 5.0 CONE_COLOR = (0.8, 0.2, 0.2, 1.0) # ======"> Sidebar", "description": "Parametric Globe Generator for Blender 5.x", "category": "3D View", } import bpy import bmesh import math from mathutils import Matrix, Vector # ========================================== # 1. コード先頭の設定項目 (ここだけを変更して使い回す) # ========================================== TITLE = "Globe Addon" TAB_NAME = "GlobeTab" PANEL_TITLE = "Globe Panel" PREFIX_NAME = "globe_addon" COLLECTION_MAIN = "Globe_Main" COLLECTION_SUB = "Globe_Sub" OBJ_NAME = "GlobeObj" LINK_URL = "https://app.notion.com/p/20260708-397f5dacaf438022ad34c17f3d743608" SPHERE_RADIUS = 10.0 SPHERE_THICKNESS = 1.0 SPHERE_COLOR = (0.2, 0.5, 0.8, 1.0) SPHERE_INNER_COLOR = (0.8, 0.4, 0.1, 1.0) # 追加: 裏面(内側)のデフォルト色 LATITUDE_COUNT = 3 LONGITUDE_COUNT = 4 HOLE_RADIUS = 1.0 CONE_RADIUS = 2.0 CONE_DEPTH = 5.0 CONE_COLOR = (0.8, 0.2, 0.2, 1.0) # ======">
bl_info = {
"name": "Globe Addon",
"author": "AI",
"version": (1, 0, 4),
"blender": (5, 0, 0), # Blender 5.x 専用
"location": "View3D > Sidebar",
"description": "Parametric Globe Generator for Blender 5.x",
"category": "3D View",
}
import bpy
import bmesh
import math
from mathutils import Matrix, Vector
# ==========================================
# 1. コード先頭の設定項目 (ここだけを変更して使い回す)
# ==========================================
TITLE = "Globe Addon"
TAB_NAME = "GlobeTab"
PANEL_TITLE = "Globe Panel"
PREFIX_NAME = "globe_addon"
COLLECTION_MAIN = "Globe_Main"
COLLECTION_SUB = "Globe_Sub"
OBJ_NAME = "GlobeObj"
LINK_URL = "<https://app.notion.com/p/20260708-397f5dacaf438022ad34c17f3d743608>"
SPHERE_RADIUS = 10.0
SPHERE_THICKNESS = 1.0
SPHERE_COLOR = (0.2, 0.5, 0.8, 1.0)
SPHERE_INNER_COLOR = (0.8, 0.4, 0.1, 1.0) # 追加: 裏面(内側)のデフォルト色
LATITUDE_COUNT = 3
LONGITUDE_COUNT = 4
HOLE_RADIUS = 1.0
CONE_RADIUS = 2.0
CONE_DEPTH = 5.0
CONE_COLOR = (0.8, 0.2, 0.2, 1.0)
# ==========================================
# 2. 内部用定数 (同じ文字列を繰り返さないための派生変数)
# ==========================================
NAME_SPHERE = f"{OBJ_NAME}_Sphere"
NAME_CUTTERS = f"{OBJ_NAME}_Cutters"
NAME_CONE = f"{OBJ_NAME}_Cone"
MAT_SPHERE = f"{PREFIX_NAME}_Mat_Sphere"
MAT_SPHERE_INNER = f"{PREFIX_NAME}_Mat_Sphere_Inner" # 追加: 裏面(内側)用マテリアル
MAT_CONE = f"{PREFIX_NAME}_Mat_Cone"
PROP_ID = f"{PREFIX_NAME}_props"
# ==========================================
# 3. ユーティリティ関数群 (堅牢な生成と更新)
# ==========================================
def update_material_color(mat_name, color):
mat = bpy.data.materials.get(mat_name)
if not mat or not mat.use_nodes:
return mat
for node in mat.node_tree.nodes:
if node.type == 'BSDF_PRINCIPLED':
if "Base Color" in node.inputs:
node.inputs["Base Color"].default_value = color
break
return mat
def create_cutters_mesh(mesh, sphere_radius, lat_count, lon_count, hole_radius):
bm = bmesh.new()
length = sphere_radius * 2.5
for i in range(1, lat_count + 1):
theta = math.pi * i / (lat_count + 1)
for j in range(lon_count):
phi = 2 * math.pi * j / lon_count
x = math.sin(theta) * math.cos(phi)
y = math.sin(theta) * math.sin(phi)
z = math.cos(theta)
dir_vec = Vector((x, y, z))
rot_quat = Vector((0, 0, 1)).rotation_difference(dir_vec)
pos = dir_vec * sphere_radius
bmesh.ops.create_cone(
bm,
cap_ends=True,
segments=16,
radius1=hole_radius,
radius2=hole_radius,
depth=length,
matrix=Matrix.Translation(pos) @ rot_quat.to_matrix().to_4x4()
)
bm.to_mesh(mesh)
bm.free()
def update_meshes_and_props(context):
props = getattr(context.scene, PROP_ID)
obj_sphere = bpy.data.objects.get(NAME_SPHERE)
obj_cutters = bpy.data.objects.get(NAME_CUTTERS)
obj_cone = bpy.data.objects.get(NAME_CONE)
if obj_sphere:
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=props.sphere_radius)
bm.to_mesh(obj_sphere.data)
bm.free()
mod_solid = obj_sphere.modifiers.get("Solidify")
if mod_solid:
mod_solid.thickness = props.sphere_thickness
obj_sphere.location = props.sphere_location
obj_sphere.rotation_euler = props.sphere_rotation
# 表面と裏面(内側)のマテリアル色をそれぞれ更新
update_material_color(MAT_SPHERE, props.sphere_color)
update_material_color(MAT_SPHERE_INNER, props.sphere_inner_color)
if obj_cutters:
create_cutters_mesh(
obj_cutters.data,
props.sphere_radius,
props.hole_latitude,
props.hole_longitude,
props.hole_radius
)
if obj_cone:
bm = bmesh.new()
bmesh.ops.create_cone(
bm,
cap_ends=True,
segments=32,
radius1=props.cone_radius,
radius2=0,
depth=props.cone_depth
)
bm.to_mesh(obj_cone.data)
bm.free()
obj_cone.location = props.cone_location
obj_cone.rotation_euler = props.cone_rotation
update_material_color(MAT_CONE, props.cone_color)
def setup_base_objects(context):
for c_name in [COLLECTION_MAIN, COLLECTION_SUB]:
if c_name not in bpy.data.collections:
new_c = bpy.data.collections.new(c_name)
context.scene.collection.children.link(new_c)
main_coll = bpy.data.collections.get(COLLECTION_MAIN)
sub_coll = bpy.data.collections.get(COLLECTION_SUB)
# 表面、裏面、円錐の3つのマテリアルを確保
for mat_name in [MAT_SPHERE, MAT_SPHERE_INNER, MAT_CONE]:
if mat_name not in bpy.data.materials:
mat = bpy.data.materials.new(mat_name)
mat.use_nodes = True
if NAME_SPHERE not in bpy.data.objects:
mesh = bpy.data.meshes.new(NAME_SPHERE)
obj = bpy.data.objects.new(NAME_SPHERE, mesh)
main_coll.objects.link(obj)
mod_solid = obj.modifiers.new(name="Solidify", type='SOLIDIFY')
mod_bool = obj.modifiers.new(name="Boolean", type='BOOLEAN')
mod_bool.operation = 'DIFFERENCE'
mod_bool.solver = 'FLOAT'
# 球体のマテリアルスロットとモディファイアの設定を保証する
obj_sphere = bpy.data.objects.get(NAME_SPHERE)
if obj_sphere:
# マテリアルスロットを2つ設定(0:表面, 1:裏面)
if len(obj_sphere.data.materials) < 2:
obj_sphere.data.materials.clear()
obj_sphere.data.materials.append(bpy.data.materials[MAT_SPHERE])
obj_sphere.data.materials.append(bpy.data.materials[MAT_SPHERE_INNER])
else:
obj_sphere.data.materials[0] = bpy.data.materials[MAT_SPHERE]
obj_sphere.data.materials[1] = bpy.data.materials[MAT_SPHERE_INNER]
mod_solid = obj_sphere.modifiers.get("Solidify")
if mod_solid:
mod_solid.material_offset = 1 # 内側の面をスロット1(裏面色)にする
mod_solid.material_offset_rim = 0 # 穴の断面はスロット0(表面色)にする
if NAME_CUTTERS not in bpy.data.objects:
mesh = bpy.data.meshes.new(NAME_CUTTERS)
obj = bpy.data.objects.new(NAME_CUTTERS, mesh)
sub_coll.objects.link(obj)
obj.display_type = 'WIRE'
obj.hide_render = True
obj.hide_viewport = True
if NAME_CONE not in bpy.data.objects:
mesh = bpy.data.meshes.new(NAME_CONE)
obj = bpy.data.objects.new(NAME_CONE, mesh)
main_coll.objects.link(obj)
obj.data.materials.append(bpy.data.materials[MAT_CONE])
obj_sphere = bpy.data.objects.get(NAME_SPHERE)
obj_cutters = bpy.data.objects.get(NAME_CUTTERS)
if obj_sphere and obj_cutters:
mod_bool = obj_sphere.modifiers.get("Boolean")
if mod_bool:
mod_bool.object = obj_cutters
# ==========================================
# 4. プロパティ定義とリアルタイム更新制御
# ==========================================
is_updating = False
def trigger_update(self, context):
global is_updating
if is_updating:
return
is_updating = True
try:
update_meshes_and_props(context)
finally:
is_updating = False
class ADDON_PG_props(bpy.types.PropertyGroup):
sphere_radius: bpy.props.FloatProperty(name="半径", default=SPHERE_RADIUS, min=0.01, update=trigger_update)
sphere_thickness: bpy.props.FloatProperty(name="厚み", default=SPHERE_THICKNESS, update=trigger_update)
sphere_color: bpy.props.FloatVectorProperty(name="表面色", subtype='COLOR', default=SPHERE_COLOR, size=4, update=trigger_update)
sphere_inner_color: bpy.props.FloatVectorProperty(name="裏面色", subtype='COLOR', default=SPHERE_INNER_COLOR, size=4, update=trigger_update) # 追加: 裏面色
sphere_location: bpy.props.FloatVectorProperty(name="位置", default=(0,0,0), update=trigger_update)
sphere_rotation: bpy.props.FloatVectorProperty(name="回転", default=(0,0,0), update=trigger_update)
hole_latitude: bpy.props.IntProperty(name="緯度方向個数", default=LATITUDE_COUNT, min=1, update=trigger_update)
hole_longitude: bpy.props.IntProperty(name="経度方向個数", default=LONGITUDE_COUNT, min=1, update=trigger_update)
hole_radius: bpy.props.FloatProperty(name="穴半径", default=HOLE_RADIUS, min=0.01, update=trigger_update)
cone_radius: bpy.props.FloatProperty(name="半径", default=CONE_RADIUS, min=0.01, update=trigger_update)
cone_depth: bpy.props.FloatProperty(name="高さ", default=CONE_DEPTH, min=0.01, update=trigger_update)
cone_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=CONE_COLOR, size=4, update=trigger_update)
cone_location: bpy.props.FloatVectorProperty(name="位置", default=(0,0,0), update=trigger_update)
cone_rotation: bpy.props.FloatVectorProperty(name="回転", default=(0,0,0), update=trigger_update)
# ==========================================
# 5. オペレーター群
# ==========================================
class ADDON_OT_create(bpy.types.Operator):
bl_idname = f"{PREFIX_NAME}.create"
bl_label = "作成"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
setup_base_objects(context)
update_meshes_and_props(context)
return {'FINISHED'}
class ADDON_OT_detach(bpy.types.Operator):
bl_idname = f"{PREFIX_NAME}.detach"
bl_label = "切り離し (独立)"
bl_description = "現在のオブジェクトをアドオンの更新対象から切り離し、静的モデルとして独立させます"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
for obj_name in [NAME_SPHERE, NAME_CUTTERS, NAME_CONE]:
obj = bpy.data.objects.get(obj_name)
if obj:
obj.name = f"{obj_name}_独立"
if obj.data.materials:
for i, mat in enumerate(obj.data.materials):
if mat:
new_mat = mat.copy()
new_mat.name = f"{mat.name}_独立"
obj.data.materials[i] = new_mat
for c_name in [COLLECTION_MAIN, COLLECTION_SUB]:
coll = bpy.data.collections.get(c_name)
if coll:
coll.name = f"{c_name}_独立"
self.report({'INFO'}, "オブジェクトをアドオンから切り離しました。")
return {'FINISHED'}
class ADDON_OT_reset(bpy.types.Operator):
bl_idname = f"{PREFIX_NAME}.reset"
bl_label = "初期値へ戻す"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
global is_updating
props = getattr(context.scene, PROP_ID)
is_updating = True
try:
props.sphere_radius = SPHERE_RADIUS
props.sphere_thickness = SPHERE_THICKNESS
props.sphere_color = SPHERE_COLOR
props.sphere_inner_color = SPHERE_INNER_COLOR # リセットにも対応
props.sphere_location = (0, 0, 0)
props.sphere_rotation = (0, 0, 0)
props.hole_latitude = LATITUDE_COUNT
props.hole_longitude = LONGITUDE_COUNT
props.hole_radius = HOLE_RADIUS
props.cone_radius = CONE_RADIUS
props.cone_depth = CONE_DEPTH
props.cone_color = CONE_COLOR
props.cone_location = (0, 0, 0)
props.cone_rotation = (0, 0, 0)
finally:
is_updating = False
update_meshes_and_props(context)
return {'FINISHED'}
class ADDON_OT_remove_addon(bpy.types.Operator):
bl_idname = f"{PREFIX_NAME}.remove_addon"
bl_label = "アドオン削除"
bl_description = "このアドオンのパネルをUIから完全に削除(終了)します"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
def delay_unregister():
unregister()
return None
bpy.app.timers.register(delay_unregister, first_interval=0.1)
self.report({'INFO'}, "アドオンを終了しました")
return {'FINISHED'}
# ==========================================
# 6. UIパネル群
# ==========================================
class ADDON_PT_main(bpy.types.Panel):
bl_idname = f"{PREFIX_NAME}_pt_main"
bl_label = PANEL_TITLE
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROP_ID)
layout.operator("view3d.view_selected", text="選択オブジェクトを画面中央に表示", icon='VIEW_CAMERA')
layout.separator()
row = layout.row(align=True)
row.scale_y = 1.2
row.operator(f"{PREFIX_NAME}.create", text="作成", icon='MESH_UVSPHERE')
row.operator(f"{PREFIX_NAME}.detach", text="切り離し (独立)", icon='UNLINKED')
layout.operator(f"{PREFIX_NAME}.reset", text="初期値へ戻す", icon='FILE_REFRESH')
layout.separator()
box = layout.box()
box.label(text="③ 球体設定")
box.prop(props, "sphere_radius")
box.prop(props, "sphere_thickness")
box.prop(props, "sphere_color")
box.prop(props, "sphere_inner_color") # UIに裏面色を追加
box.prop(props, "sphere_location")
box.prop(props, "sphere_rotation")
box = layout.box()
box.label(text="④ 穴設定")
box.prop(props, "hole_latitude")
box.prop(props, "hole_longitude")
box.prop(props, "hole_radius")
box = layout.box()
box.label(text="⑤ 円錐設定")
box.prop(props, "cone_radius")
box.prop(props, "cone_depth")
box.prop(props, "cone_color")
box.prop(props, "cone_location")
box.prop(props, "cone_rotation")
class ADDON_PT_link(bpy.types.Panel):
bl_idname = f"{PREFIX_NAME}_pt_link"
bl_label = "⑥ リンク"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
op = self.layout.operator("wm.url_open", text="地球儀 穴窓面 20260708", icon='URL')
op.url = LINK_URL
class ADDON_PT_remove(bpy.types.Panel):
bl_idname = f"{PREFIX_NAME}_pt_remove"
bl_label = "⑦ アドオン削除"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
self.layout.operator(f"{PREFIX_NAME}.remove_addon", text="アドオン削除", icon='CANCEL')
# ==========================================
# 7. 登録処理
# ==========================================
classes = [
ADDON_PG_props,
ADDON_OT_create,
ADDON_OT_detach,
ADDON_OT_reset,
ADDON_OT_remove_addon,
ADDON_PT_main,
ADDON_PT_link,
ADDON_PT_remove,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
setattr(bpy.types.Scene, PROP_ID, bpy.props.PointerProperty(type=ADDON_PG_props))
def unregister():
for cls in reversed(classes):
try:
bpy.utils.unregister_class(cls)
except RuntimeError:
pass
if hasattr(bpy.types.Scene, PROP_ID):
delattr(bpy.types.Scene, PROP_ID)
if __name__ == "__main__":
register()
bl_info = {
"name": "Globe Addon",
"author": "AI",
"version": (1, 0, 3),
"blender": (5, 0, 0), # Blender 5.x 専用
"location": "View3D > Sidebar",
"description": "Parametric Globe Generator for Blender 5.x",
"category": "3D View",
}
import bpy
import bmesh
import math
from mathutils import Matrix, Vector
# ==========================================
# 1. コード先頭の設定項目 (ここだけを変更して使い回す)
# ==========================================
TITLE = "Globe Addon"
TAB_NAME = "GlobeTab"
PANEL_TITLE = "Globe Panel"
PREFIX_NAME = "globe_addon"
COLLECTION_MAIN = "Globe_Main"
COLLECTION_SUB = "Globe_Sub"
OBJ_NAME = "GlobeObj"
LINK_URL = "<https://app.notion.com/p/20260708-397f5dacaf438022ad34c17f3d743608>"
SPHERE_RADIUS = 10.0
SPHERE_THICKNESS = 1.0
SPHERE_COLOR = (0.2, 0.5, 0.8, 1.0)
LATITUDE_COUNT = 3
LONGITUDE_COUNT = 4
HOLE_RADIUS = 1.0 # パネル仕様「穴半径」用の初期値
CONE_RADIUS = 2.0
CONE_DEPTH = 5.0
CONE_COLOR = (0.8, 0.2, 0.2, 1.0)
# ==========================================
# 2. 内部用定数 (同じ文字列を繰り返さないための派生変数)
# ==========================================
NAME_SPHERE = f"{OBJ_NAME}_Sphere"
NAME_CUTTERS = f"{OBJ_NAME}_Cutters"
NAME_CONE = f"{OBJ_NAME}_Cone"
MAT_SPHERE = f"{PREFIX_NAME}_Mat_Sphere"
MAT_CONE = f"{PREFIX_NAME}_Mat_Cone"
PROP_ID = f"{PREFIX_NAME}_props"
# ==========================================
# 3. ユーティリティ関数群 (堅牢な生成と更新)
# ==========================================
def update_material_color(mat_name, color):
mat = bpy.data.materials.get(mat_name)
if not mat or not mat.use_nodes:
return mat
for node in mat.node_tree.nodes:
if node.type == 'BSDF_PRINCIPLED':
if "Base Color" in node.inputs:
node.inputs["Base Color"].default_value = color
break
return mat
def create_cutters_mesh(mesh, sphere_radius, lat_count, lon_count, hole_radius):
bm = bmesh.new()
length = sphere_radius * 2.5
for i in range(1, lat_count + 1):
theta = math.pi * i / (lat_count + 1)
for j in range(lon_count):
phi = 2 * math.pi * j / lon_count
x = math.sin(theta) * math.cos(phi)
y = math.sin(theta) * math.sin(phi)
z = math.cos(theta)
dir_vec = Vector((x, y, z))
rot_quat = Vector((0, 0, 1)).rotation_difference(dir_vec)
pos = dir_vec * sphere_radius
bmesh.ops.create_cone(
bm,
cap_ends=True,
segments=16,
radius1=hole_radius,
radius2=hole_radius,
depth=length,
matrix=Matrix.Translation(pos) @ rot_quat.to_matrix().to_4x4()
)
bm.to_mesh(mesh)
bm.free()
def update_meshes_and_props(context):
props = getattr(context.scene, PROP_ID)
obj_sphere = bpy.data.objects.get(NAME_SPHERE)
obj_cutters = bpy.data.objects.get(NAME_CUTTERS)
obj_cone = bpy.data.objects.get(NAME_CONE)
if obj_sphere:
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=props.sphere_radius)
bm.to_mesh(obj_sphere.data)
bm.free()
mod_solid = obj_sphere.modifiers.get("Solidify")
if mod_solid:
mod_solid.thickness = props.sphere_thickness
obj_sphere.location = props.sphere_location
obj_sphere.rotation_euler = props.sphere_rotation
update_material_color(MAT_SPHERE, props.sphere_color)
if obj_cutters:
create_cutters_mesh(
obj_cutters.data,
props.sphere_radius,
props.hole_latitude,
props.hole_longitude,
props.hole_radius
)
if obj_cone:
bm = bmesh.new()
bmesh.ops.create_cone(
bm,
cap_ends=True,
segments=32,
radius1=props.cone_radius,
radius2=0,
depth=props.cone_depth
)
bm.to_mesh(obj_cone.data)
bm.free()
obj_cone.location = props.cone_location
obj_cone.rotation_euler = props.cone_rotation
update_material_color(MAT_CONE, props.cone_color)
def setup_base_objects(context):
for c_name in [COLLECTION_MAIN, COLLECTION_SUB]:
if c_name not in bpy.data.collections:
new_c = bpy.data.collections.new(c_name)
context.scene.collection.children.link(new_c)
main_coll = bpy.data.collections.get(COLLECTION_MAIN)
sub_coll = bpy.data.collections.get(COLLECTION_SUB)
for mat_name in [MAT_SPHERE, MAT_CONE]:
if mat_name not in bpy.data.materials:
mat = bpy.data.materials.new(mat_name)
mat.use_nodes = True
if NAME_SPHERE not in bpy.data.objects:
mesh = bpy.data.meshes.new(NAME_SPHERE)
obj = bpy.data.objects.new(NAME_SPHERE, mesh)
main_coll.objects.link(obj)
obj.data.materials.append(bpy.data.materials[MAT_SPHERE])
mod_solid = obj.modifiers.new(name="Solidify", type='SOLIDIFY')
mod_bool = obj.modifiers.new(name="Boolean", type='BOOLEAN')
mod_bool.operation = 'DIFFERENCE'
mod_bool.solver = 'FLOAT' # Blender 5.x 用
if NAME_CUTTERS not in bpy.data.objects:
mesh = bpy.data.meshes.new(NAME_CUTTERS)
obj = bpy.data.objects.new(NAME_CUTTERS, mesh)
sub_coll.objects.link(obj)
obj.display_type = 'WIRE'
obj.hide_render = True
obj.hide_viewport = True
if NAME_CONE not in bpy.data.objects:
mesh = bpy.data.meshes.new(NAME_CONE)
obj = bpy.data.objects.new(NAME_CONE, mesh)
main_coll.objects.link(obj)
obj.data.materials.append(bpy.data.materials[MAT_CONE])
obj_sphere = bpy.data.objects.get(NAME_SPHERE)
obj_cutters = bpy.data.objects.get(NAME_CUTTERS)
if obj_sphere and obj_cutters:
mod_bool = obj_sphere.modifiers.get("Boolean")
if mod_bool:
mod_bool.object = obj_cutters
# ==========================================
# 4. プロパティ定義とリアルタイム更新制御
# ==========================================
is_updating = False
def trigger_update(self, context):
global is_updating
if is_updating:
return
is_updating = True
try:
update_meshes_and_props(context)
finally:
is_updating = False
class ADDON_PG_props(bpy.types.PropertyGroup):
sphere_radius: bpy.props.FloatProperty(name="半径", default=SPHERE_RADIUS, min=0.01, update=trigger_update)
sphere_thickness: bpy.props.FloatProperty(name="厚み", default=SPHERE_THICKNESS, update=trigger_update)
sphere_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=SPHERE_COLOR, size=4, update=trigger_update)
sphere_location: bpy.props.FloatVectorProperty(name="位置", default=(0,0,0), update=trigger_update)
sphere_rotation: bpy.props.FloatVectorProperty(name="回転", default=(0,0,0), update=trigger_update)
hole_latitude: bpy.props.IntProperty(name="緯度方向個数", default=LATITUDE_COUNT, min=1, update=trigger_update)
hole_longitude: bpy.props.IntProperty(name="経度方向個数", default=LONGITUDE_COUNT, min=1, update=trigger_update)
hole_radius: bpy.props.FloatProperty(name="穴半径", default=HOLE_RADIUS, min=0.01, update=trigger_update)
cone_radius: bpy.props.FloatProperty(name="半径", default=CONE_RADIUS, min=0.01, update=trigger_update)
cone_depth: bpy.props.FloatProperty(name="高さ", default=CONE_DEPTH, min=0.01, update=trigger_update)
cone_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=CONE_COLOR, size=4, update=trigger_update)
cone_location: bpy.props.FloatVectorProperty(name="位置", default=(0,0,0), update=trigger_update)
cone_rotation: bpy.props.FloatVectorProperty(name="回転", default=(0,0,0), update=trigger_update)
# ==========================================
# 5. オペレーター群
# ==========================================
class ADDON_OT_create(bpy.types.Operator):
bl_idname = f"{PREFIX_NAME}.create"
bl_label = "作成"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
setup_base_objects(context)
update_meshes_and_props(context)
return {'FINISHED'}
class ADDON_OT_detach(bpy.types.Operator):
bl_idname = f"{PREFIX_NAME}.detach"
bl_label = "切り離し (独立)"
bl_description = "現在のオブジェクトをアドオンの更新対象から切り離し、静的モデルとして独立させます"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
# 1. オブジェクトとマテリアルのリネーム
for obj_name in [NAME_SPHERE, NAME_CUTTERS, NAME_CONE]:
obj = bpy.data.objects.get(obj_name)
if obj:
obj.name = f"{obj_name}_独立"
if obj.data.materials:
for i, mat in enumerate(obj.data.materials):
if mat:
new_mat = mat.copy()
new_mat.name = f"{mat.name}_独立"
obj.data.materials[i] = new_mat
# 2. コレクションのリネーム
for c_name in [COLLECTION_MAIN, COLLECTION_SUB]:
coll = bpy.data.collections.get(c_name)
if coll:
coll.name = f"{c_name}_独立"
self.report({'INFO'}, "オブジェクトをアドオンから切り離しました。")
return {'FINISHED'}
class ADDON_OT_reset(bpy.types.Operator):
bl_idname = f"{PREFIX_NAME}.reset"
bl_label = "初期値へ戻す"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
global is_updating
props = getattr(context.scene, PROP_ID)
is_updating = True
try:
props.sphere_radius = SPHERE_RADIUS
props.sphere_thickness = SPHERE_THICKNESS
props.sphere_color = SPHERE_COLOR
props.sphere_location = (0, 0, 0)
props.sphere_rotation = (0, 0, 0)
props.hole_latitude = LATITUDE_COUNT
props.hole_longitude = LONGITUDE_COUNT
props.hole_radius = HOLE_RADIUS
props.cone_radius = CONE_RADIUS
props.cone_depth = CONE_DEPTH
props.cone_color = CONE_COLOR
props.cone_location = (0, 0, 0)
props.cone_rotation = (0, 0, 0)
finally:
is_updating = False
update_meshes_and_props(context)
return {'FINISHED'}
# 修正箇所: タイマーを利用して実行後に安全にアドオンを解除するように変更
class ADDON_OT_remove_addon(bpy.types.Operator):
"""アドオンを終了し、UIパネルを削除します"""
bl_idname = f"{PREFIX_NAME}.remove_addon"
bl_label = "アドオン削除"
bl_description = "このアドオンのパネルをUIから完全に削除(終了)します"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
# UI更新・オペレータ実行中に自身を削除するとエラーになるため
# 処理完了の少し後(0.1秒後)に登録解除を実行させます。
def delay_unregister():
unregister()
return None
bpy.app.timers.register(delay_unregister, first_interval=0.1)
self.report({'INFO'}, "アドオンを終了しました")
return {'FINISHED'}
# ==========================================
# 6. UIパネル群
# ==========================================
class ADDON_PT_main(bpy.types.Panel):
bl_idname = f"{PREFIX_NAME}_pt_main"
bl_label = PANEL_TITLE
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROP_ID)
# ① 選択オブジェクト表示
layout.operator("view3d.view_selected", text="選択オブジェクトを画面中央に表示", icon='VIEW_CAMERA')
layout.separator()
# ② オブジェクト作成 & 切り離し
row = layout.row(align=True)
row.scale_y = 1.2
row.operator(f"{PREFIX_NAME}.create", text="作成", icon='MESH_UVSPHERE')
row.operator(f"{PREFIX_NAME}.detach", text="切り離し (独立)", icon='UNLINKED')
layout.operator(f"{PREFIX_NAME}.reset", text="初期値へ戻す", icon='FILE_REFRESH')
layout.separator()
# ③ 球体設定
box = layout.box()
box.label(text="③ 球体設定")
box.prop(props, "sphere_radius")
box.prop(props, "sphere_thickness")
box.prop(props, "sphere_color")
box.prop(props, "sphere_location")
box.prop(props, "sphere_rotation")
# ④ 穴設定
box = layout.box()
box.label(text="④ 穴設定")
box.prop(props, "hole_latitude")
box.prop(props, "hole_longitude")
box.prop(props, "hole_radius")
# ⑤ 円錐設定
box = layout.box()
box.label(text="⑤ 円錐設定")
box.prop(props, "cone_radius")
box.prop(props, "cone_depth")
box.prop(props, "cone_color")
box.prop(props, "cone_location")
box.prop(props, "cone_rotation")
class ADDON_PT_link(bpy.types.Panel):
bl_idname = f"{PREFIX_NAME}_pt_link"
bl_label = "⑥ リンク"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
op = self.layout.operator("wm.url_open", text="地球儀 穴窓面 20260708", icon='URL')
op.url = LINK_URL
class ADDON_PT_remove(bpy.types.Panel):
bl_idname = f"{PREFIX_NAME}_pt_remove"
bl_label = "⑦ アドオン削除"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
self.layout.operator(f"{PREFIX_NAME}.remove_addon", text="アドオン削除", icon='CANCEL')
# ==========================================
# 7. 登録処理
# ==========================================
classes = [
ADDON_PG_props,
ADDON_OT_create,
ADDON_OT_detach,
ADDON_OT_reset,
ADDON_OT_remove_addon,
ADDON_PT_main,
ADDON_PT_link,
ADDON_PT_remove,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
setattr(bpy.types.Scene, PROP_ID, bpy.props.PointerProperty(type=ADDON_PG_props))
def unregister():
for cls in reversed(classes):
try:
bpy.utils.unregister_class(cls)
except RuntimeError:
pass # 登録解除時のエラーを無視して続行する
if hasattr(bpy.types.Scene, PROP_ID):
delattr(bpy.types.Scene, PROP_ID)
if __name__ == "__main__":
register()