Sidebar", "description": "トーラス・円錐台・球体・集中矢印・骨格半球ドーム(裏面色・透明度完全対応・汎用円錐台)", "category": "3D View" } # ========================================================= # 2. コード作成日時 # ========================================================= # Code Created : 2026_0725_1630 # ========================================================= # 3. import # ========================================================= import bpy import bmesh import math import time import webbrowser import addon_utils from mathutils import Vector, Euler # ========================================================= # 4. 共通定義 # ========================================================= ADDON_TITLE = "TCS & Arrows" PREFIX_VAL = "TCS_ARR" ADDON"> Sidebar", "description": "トーラス・円錐台・球体・集中矢印・骨格半球ドーム(裏面色・透明度完全対応・汎用円錐台)", "category": "3D View" } # ========================================================= # 2. コード作成日時 # ========================================================= # Code Created : 2026_0725_1630 # ========================================================= # 3. import # ========================================================= import bpy import bmesh import math import time import webbrowser import addon_utils from mathutils import Vector, Euler # ========================================================= # 4. 共通定義 # ========================================================= ADDON_TITLE = "TCS & Arrows" PREFIX_VAL = "TCS_ARR" ADDON"> Sidebar", "description": "トーラス・円錐台・球体・集中矢印・骨格半球ドーム(裏面色・透明度完全対応・汎用円錐台)", "category": "3D View" } # ========================================================= # 2. コード作成日時 # ========================================================= # Code Created : 2026_0725_1630 # ========================================================= # 3. import # ========================================================= import bpy import bmesh import math import time import webbrowser import addon_utils from mathutils import Vector, Euler # ========================================================= # 4. 共通定義 # ========================================================= ADDON_TITLE = "TCS & Arrows" PREFIX_VAL = "TCS_ARR" ADDON">
# =========================================================
# 1. bl_info
# =========================================================
bl_info = {
"name": "TCS & Arrows Generator",
"author": "Your Name",
"version": (2, 6, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar",
"description": "トーラス・円錐台・球体・集中矢印・骨格半球ドーム(裏面色・透明度完全対応・汎用円錐台)",
"category": "3D View"
}
# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0725_1630
# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import time
import webbrowser
import addon_utils
from mathutils import Vector, Euler
# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "TCS & Arrows"
PREFIX_VAL = "TCS_ARR"
ADDON_CATEGORY = "TCS Gen"
PREFIX = PREFIX_VAL.lower()
# =========================================================
# 5. リンク定義
# =========================================================
LINK_NAME_1 = "マニュアルページ"
LINK_URL_1 = "<https://example.com/manual>"
LINK_NAME_2 = "コンテナ builder"
LINK_URL_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"
LINK_NAME_3 = "posfie"
LINK_URL_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_4 = "zionadchat"
LINK_URL_4 = "<https://x.com/zionadchat>"
LINK_NAME_5 = ""
LINK_URL_5 = ""
# =========================================================
# 6. 共通ユーティリティ
# =========================================================
class TCS_ARR_Utility:
@staticmethod
def get_or_create_collection(name):
col = bpy.data.collections.get(name)
if not col:
col = bpy.data.collections.new(name)
bpy.context.scene.collection.children.link(col)
return col
@staticmethod
def get_or_create_material(name, color):
"""通常の単一マテリアル生成"""
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
try:
if hasattr(mat, "blend_method"):
mat.blend_method = 'BLEND'
if hasattr(mat, "shadow_method"):
mat.shadow_method = 'NONE'
except Exception:
pass
if mat.use_nodes:
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = (color[0], color[1], color[2], 1.0)
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
mat.diffuse_color = (color[0], color[1], color[2], color[3])
return mat
@staticmethod
def get_or_create_double_sided_material(name, front_color, back_color):
"""表裏で色・透明度を分ける両面マテリアル生成(円錐用)"""
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
try:
if hasattr(mat, "blend_method"):
mat.blend_method = 'BLEND'
if hasattr(mat, "shadow_method"):
mat.shadow_method = 'NONE'
except Exception:
pass
if mat.use_nodes:
tree = mat.node_tree
bsdf_front = tree.nodes.get("Principled BSDF Front")
if not bsdf_front:
tree.nodes.clear()
out_node = tree.nodes.new(type='ShaderNodeOutputMaterial')
out_node.location = (400, 0)
mix_shader = tree.nodes.new(type='ShaderNodeMixShader')
mix_shader.location = (200, 0)
bsdf_front = tree.nodes.new(type='ShaderNodeBsdfPrincipled')
bsdf_front.name = "Principled BSDF Front"
bsdf_front.label = "Front Color"
bsdf_front.location = (0, 150)
bsdf_back = tree.nodes.new(type='ShaderNodeBsdfPrincipled')
bsdf_back.name = "Principled BSDF Back"
bsdf_back.label = "Back Color"
bsdf_back.location = (0, -200)
geom_node = tree.nodes.new(type='ShaderNodeNewGeometry')
geom_node.location = (0, 400)
tree.links.new(geom_node.outputs["Backfacing"], mix_shader.inputs[0])
tree.links.new(bsdf_front.outputs["BSDF"], mix_shader.inputs[1])
tree.links.new(bsdf_back.outputs["BSDF"], mix_shader.inputs[2])
tree.links.new(mix_shader.outputs["Shader"], out_node.inputs["Surface"])
bsdf_front = tree.nodes.get("Principled BSDF Front")
bsdf_back = tree.nodes.get("Principled BSDF Back")
if bsdf_front:
if "Base Color" in bsdf_front.inputs:
bsdf_front.inputs["Base Color"].default_value = (front_color[0], front_color[1], front_color[2], 1.0)
if "Alpha" in bsdf_front.inputs:
bsdf_front.inputs["Alpha"].default_value = front_color[3]
if bsdf_back:
if "Base Color" in bsdf_back.inputs:
bsdf_back.inputs["Base Color"].default_value = (back_color[0], back_color[1], back_color[2], 1.0)
if "Alpha" in bsdf_back.inputs:
bsdf_back.inputs["Alpha"].default_value = back_color[3]
mat.diffuse_color = (front_color[0], front_color[1], front_color[2], front_color[3])
return mat
@staticmethod
def apply_mesh(obj_name, mesh_func, mat):
col = TCS_ARR_Utility.get_or_create_collection(f"{PREFIX_VAL}_Collection")
obj = bpy.data.objects.get(obj_name)
if obj:
old_mesh = obj.data
obj.data = mesh_func()
if old_mesh.users == 0:
bpy.data.meshes.remove(old_mesh, do_unlink=True)
else:
mesh = mesh_func()
obj = bpy.data.objects.new(obj_name, mesh)
col.objects.link(obj)
if len(obj.data.materials) == 0:
obj.data.materials.append(mat)
else:
obj.data.materials[0] = mat
return obj
@staticmethod
def apply_curve(obj_name, curve_func, mat):
"""カーブオブジェクト用の適用処理(ドーム用)"""
col = TCS_ARR_Utility.get_or_create_collection(f"{PREFIX_VAL}_Collection")
obj = bpy.data.objects.get(obj_name)
if obj:
old_curve = obj.data
obj.data = curve_func()
if old_curve.users == 0:
bpy.data.curves.remove(old_curve, do_unlink=True)
else:
curve = curve_func()
obj = bpy.data.objects.new(obj_name, curve)
col.objects.link(obj)
if len(obj.data.materials) == 0:
obj.data.materials.append(mat)
else:
obj.data.materials[0] = mat
return obj
@staticmethod
def detach_object(context, obj_name, base_new_name, flag_attr):
obj = bpy.data.objects.get(obj_name)
if not obj:
return False
ts_str = time.strftime("%H%M%S")
new_name = f"{base_new_name}_{ts_str}"
obj.name = new_name
# Mesh/Curve 両対応のための名称変更
if obj.data:
obj.data.name = f"{new_name}_Data"
for slot in obj.material_slots:
if slot.material and slot.material.name.startswith(PREFIX_VAL):
new_mat = slot.material.copy()
new_mat.name = f"{new_name}_Mat"
slot.material = new_mat
main_col = context.scene.collection
if obj.name not in main_col.objects:
main_col.objects.link(obj)
addon_col = bpy.data.collections.get(f"{PREFIX_VAL}_Collection")
if addon_col and obj.name in addon_col.objects:
addon_col.objects.unlink(obj)
setattr(context.scene.tcs_arr_props, flag_attr, False)
return True
# =========================================================
# 7. 更新処理 (Update Functions)
# =========================================================
def update_torus(self, context):
if not self.is_tr_created: return
mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Torus", self.tr_cl)
def make_torus():
major_r = self.tr_mj_rd
minor_r = self.tr_mn_rd
segments, minor_segments = 48, 16
verts, faces = [], []
for i in range(segments):
a1 = (i / segments) * 2.0 * math.pi
c1, s1 = math.cos(a1), math.sin(a1)
for j in range(minor_segments):
a2 = (j / minor_segments) * 2.0 * math.pi
c2, s2 = math.cos(a2), math.sin(a2)
x = (major_r + minor_r * c2) * c1
y = (major_r + minor_r * c2) * s1
z = minor_r * s2
verts.append((x, y, z))
for i in range(segments):
for j in range(minor_segments):
i_next = (i + 1) % segments
j_next = (j + 1) % minor_segments
v0 = i * minor_segments + j
v1 = i_next * minor_segments + j
v2 = i_next * minor_segments + j_next
v3 = i * minor_segments + j_next
faces.append((v0, v1, v2, v3))
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Torus_Mesh")
me.from_pydata(verts, [], faces)
me.update()
return me
obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Torus", make_torus, mat)
obj.location = self.tr_loc
obj.rotation_euler = self.tr_rot
if self.is_aw_created:
update_arrows(self, context)
def update_cone(self, context):
if not self.is_cn_created: return
mat = TCS_ARR_Utility.get_or_create_double_sided_material(
f"{PREFIX_VAL}_Mat_Cone", self.cn_cl, self.cn_cl_back
)
def make_cone():
bm = bmesh.new()
ht = self.cn_ht
abs_ht = abs(ht) if abs(ht) > 0.001 else 0.001
if ht >= 0:
r1 = self.cn_rd_bottom
r2 = self.cn_rd_top
z_offset = abs_ht / 2.0
else:
r1 = self.cn_rd_top
r2 = self.cn_rd_bottom
z_offset = -abs_ht / 2.0
bmesh.ops.create_cone(
bm,
cap_ends=not self.cn_no_caps,
cap_tris=True,
segments=48,
radius1=r1,
radius2=r2,
depth=abs_ht
)
bmesh.ops.translate(bm, verts=bm.verts, vec=(0, 0, z_offset))
if self.cn_flip_normals:
bmesh.ops.reverse_faces(bm, faces=bm.faces)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Cone_Mesh")
bm.to_mesh(me)
bm.free()
return me
obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Cone", make_cone, mat)
obj.location = self.cn_loc
obj.rotation_euler = self.cn_rot
def update_sphere(self, context):
if not self.is_sp_created: return
mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Sphere", self.sp_cl)
def make_sphere():
bm = bmesh.new()
bmesh.ops.create_uvsphere(
bm, u_segments=32, v_segments=16, radius=self.sp_rd
)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Sphere_Mesh")
bm.to_mesh(me)
bm.free()
return me
obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Sphere", make_sphere, mat)
obj.location = self.sp_loc
obj.rotation_euler = self.sp_rot
def update_arrows(self, context):
if not self.is_aw_created: return
mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Arrows", self.aw_cl)
def make_arrows():
bm = bmesh.new()
cnt = self.aw_cnt
tg_loc = Vector(self.aw_tg_loc)
tr_loc = Vector(self.tr_loc)
tr_mat = Euler(self.tr_rot).to_matrix()
tr_R = self.tr_mj_rd
aw_thk = self.aw_thk
pct = self.aw_offset_pct / 100.0
for i in range(cnt):
theta = i * (2 * math.pi / cnt)
local_p = Vector((tr_R * math.cos(theta), tr_R * math.sin(theta), 0))
start_p = tr_loc + tr_mat @ local_p
vec = tg_loc - start_p
full_dist = vec.length
if full_dist < 0.001: continue
dist = full_dist * pct
if dist < 0.001: continue
dir_vec = vec.normalized()
rot_quat = Vector((0,0,1)).rotation_difference(dir_vec)
head_len = aw_thk * 4.0
if head_len > dist * 0.5:
head_len = dist * 0.5
shaft_len = dist - head_len
geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12,
radius1=aw_thk, radius2=aw_thk, depth=shaft_len)
verts_shaft = geom['verts']
bmesh.ops.translate(bm, verts=verts_shaft, vec=(0,0,shaft_len/2))
geom_h = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12,
radius1=aw_thk*2.5, radius2=0, depth=head_len)
verts_head = geom_h['verts']
bmesh.ops.translate(bm, verts=verts_head, vec=(0,0,shaft_len + head_len/2))
verts_all = verts_shaft + verts_head
bmesh.ops.transform(bm, verts=verts_all, matrix=rot_quat.to_matrix().to_4x4())
bmesh.ops.translate(bm, verts=verts_all, vec=start_p)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Arrows_Mesh")
bm.to_mesh(me)
bm.free()
return me
obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Arrows", make_arrows, mat)
obj.location = (0, 0, 0)
obj.rotation_euler = (0, 0, 0)
def update_dome(self, context):
if not self.is_dm_created: return
mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Dome", self.dm_cl)
def make_dome():
crv = bpy.data.curves.new(f"{PREFIX_VAL}_Dome_Curve", 'CURVE')
crv.dimensions = '3D'
crv.fill_mode = 'FULL'
crv.bevel_depth = self.dm_thk
crv.bevel_resolution = 4
rd = self.dm_rd
ln_cn = self.dm_ln_cn
lt_cn = self.dm_lt_cn
sg = self.dm_sg
# 経度のスプライン生成
for i in range(ln_cn):
theta = (2 * math.pi / ln_cn) * i
spl = crv.splines.new('POLY')
spl.points.add(sg - 1)
for j in range(sg):
phi = (math.pi / 2) * (j / (sg - 1))
x = rd * math.sin(phi) * math.cos(theta)
y = rd * math.sin(phi) * math.sin(theta)
z = rd * math.cos(phi)
spl.points[j].co = (x, y, z, 1.0)
# 緯度のスプライン生成
for i in range(1, lt_cn + 1):
phi = (math.pi / 2) * (i / lt_cn)
spl = crv.splines.new('POLY')
spl.points.add(sg - 1)
for j in range(sg):
theta = (2 * math.pi) * (j / (sg - 1))
x = rd * math.sin(phi) * math.cos(theta)
y = rd * math.sin(phi) * math.sin(theta)
z = rd * math.cos(phi)
spl.points[j].co = (x, y, z, 1.0)
spl.use_cyclic_u = True
return crv
obj = TCS_ARR_Utility.apply_curve(f"{PREFIX_VAL}_Dome", make_dome, mat)
obj.location = self.dm_loc
obj.rotation_euler = self.dm_rot
# =========================================================
# 8. プロパティグループ
# =========================================================
class TCS_ARR_PropertyGroup(bpy.types.PropertyGroup):
# Torus
is_tr_created: bpy.props.BoolProperty(default=False)
tr_loc: bpy.props.FloatVectorProperty(name="Location", default=(0,0,0), update=update_torus)
tr_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_torus)
tr_mj_rd: bpy.props.FloatProperty(name="Major Radius", default=3.0, min=0.1, update=update_torus)
tr_mn_rd: bpy.props.FloatProperty(name="Minor Radius", default=0.2, min=0.01, update=update_torus)
tr_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(0.2, 0.6, 1.0, 1.0), min=0, max=1, update=update_torus)
# Cone
is_cn_created: bpy.props.BoolProperty(default=False)
cn_loc: bpy.props.FloatVectorProperty(name="Location", default=(5,0,0), update=update_cone)
cn_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_cone)
cn_rd_top: bpy.props.FloatProperty(name="上面半径", default=0.0, min=0.0, update=update_cone)
cn_rd_bottom: bpy.props.FloatProperty(name="下面半径", default=2.0, min=0.0, update=update_cone)
cn_ht: bpy.props.FloatProperty(name="Height (マイナス許容)", default=4.0, update=update_cone)
cn_no_caps: bpy.props.BoolProperty(name="蓋無し", default=False, update=update_cone)
cn_flip_normals: bpy.props.BoolProperty(name="法線の反転 (裏表入替)", default=False, update=update_cone)
cn_cl: bpy.props.FloatVectorProperty(name="表面色", subtype='COLOR', size=4, default=(1.0, 0.5, 0.1, 1.0), min=0, max=1, update=update_cone)
cn_cl_back: bpy.props.FloatVectorProperty(name="裏面色", subtype='COLOR', size=4, default=(0.1, 0.5, 1.0, 1.0), min=0, max=1, update=update_cone)
# Sphere
is_sp_created: bpy.props.BoolProperty(default=False)
sp_loc: bpy.props.FloatVectorProperty(name="Location", default=(-5,0,0), update=update_sphere)
sp_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_sphere)
sp_rd: bpy.props.FloatProperty(name="Radius", default=1.5, min=0.1, update=update_sphere)
sp_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(0.2, 0.9, 0.4, 1.0), min=0, max=1, update=update_sphere)
# Arrows
is_aw_created: bpy.props.BoolProperty(default=False)
aw_cnt: bpy.props.IntProperty(name="矢印の本数", default=8, min=1, max=100, update=update_arrows)
aw_tg_loc: bpy.props.FloatVectorProperty(name="Target Loc", default=(0,0,10.0), update=update_arrows)
aw_offset_pct: bpy.props.FloatProperty(name="先端オフセット (%)", default=100.0, min=1.0, max=100.0, subtype='PERCENTAGE', update=update_arrows)
aw_thk: bpy.props.FloatProperty(name="Thickness", default=0.1, min=0.01, update=update_arrows)
aw_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(1.0, 0.1, 0.1, 1.0), min=0, max=1, update=update_arrows)
# Dome (Hemisphere Skeleton)
is_dm_created: bpy.props.BoolProperty(default=False)
dm_loc: bpy.props.FloatVectorProperty(name="Location", default=(0,5,0), update=update_dome)
dm_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_dome)
dm_rd: bpy.props.FloatProperty(name="ドーム半径", default=3.0, min=0.1, update=update_dome)
dm_thk: bpy.props.FloatProperty(name="フレーム太さ", default=0.1, min=0.01, update=update_dome)
dm_ln_cn: bpy.props.IntProperty(name="経度の本数", default=12, min=3, max=100, update=update_dome)
dm_lt_cn: bpy.props.IntProperty(name="緯度の本数", default=6, min=1, max=100, update=update_dome)
dm_sg: bpy.props.IntProperty(name="分割数 (滑らかさ)", default=32, min=4, max=128, update=update_dome)
dm_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(0.5, 0.3, 0.8, 1.0), min=0, max=1, update=update_dome)
# =========================================================
# 9. Operators
# =========================================================
class OT_create_torus(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_torus"
bl_label = "トーラスを生成"
def execute(self, context):
context.scene.tcs_arr_props.is_tr_created = True
update_torus(context.scene.tcs_arr_props, context)
return {'FINISHED'}
class OT_create_cone(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_cone"
bl_label = "円錐台を生成"
def execute(self, context):
context.scene.tcs_arr_props.is_cn_created = True
update_cone(context.scene.tcs_arr_props, context)
return {'FINISHED'}
class OT_create_sphere(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_sphere"
bl_label = "球体を生成"
def execute(self, context):
context.scene.tcs_arr_props.is_sp_created = True
update_sphere(context.scene.tcs_arr_props, context)
return {'FINISHED'}
class OT_create_arrows(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_arrows"
bl_label = "矢印を生成"
bl_description = "トーラスの円周からターゲット位置に向けて矢印を生成します"
def execute(self, context):
context.scene.tcs_arr_props.is_aw_created = True
update_arrows(context.scene.tcs_arr_props, context)
return {'FINISHED'}
class OT_create_dome(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_dome"
bl_label = "骨格ドームを生成"
bl_description = "経度・緯度のカーブ構造を持つ半球ドームを生成します"
def execute(self, context):
context.scene.tcs_arr_props.is_dm_created = True
update_dome(context.scene.tcs_arr_props, context)
return {'FINISHED'}
class OT_detach_torus(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_torus"
bl_label = "切り離し(独立)"
def execute(self, context):
if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Torus", "Detached_Torus", "is_tr_created"):
self.report({'INFO'}, "トーラスを独立させました")
return {'FINISHED'}
class OT_detach_cone(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_cone"
bl_label = "切り離し(独立)"
def execute(self, context):
if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Cone", "Detached_Cone", "is_cn_created"):
self.report({'INFO'}, "円錐台を独立させました")
return {'FINISHED'}
class OT_detach_sphere(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_sphere"
bl_label = "切り離し(独立)"
def execute(self, context):
if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Sphere", "Detached_Sphere", "is_sp_created"):
self.report({'INFO'}, "球体を独立させました")
return {'FINISHED'}
class OT_detach_arrows(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_arrows"
bl_label = "切り離し(独立)"
def execute(self, context):
if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Arrows", "Detached_Arrows", "is_aw_created"):
self.report({'INFO'}, "矢印を独立させました")
return {'FINISHED'}
class OT_detach_dome(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_dome"
bl_label = "切り離し(独立)"
def execute(self, context):
if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Dome", "Detached_Dome", "is_dm_created"):
self.report({'INFO'}, "骨格ドームを独立させました")
return {'FINISHED'}
class OT_open_url(bpy.types.Operator):
bl_idname = f"{PREFIX}.open_url"
bl_label = "リンクを開く"
url: bpy.props.StringProperty()
def execute(self, context):
webbrowser.open(self.url)
return {'FINISHED'}
def delayed_unregister():
try:
addon_utils.disable(__name__)
except Exception:
pass
try:
unregister()
except Exception as e:
print(f"[{ADDON_TITLE}] Delayed Unregister Error: {e}")
return None
class OT_remove_addon(bpy.types.Operator):
bl_idname = f"{PREFIX}.remove_addon"
bl_label = "Clean up & Uninstall"
bl_description = "生成したデータを全て消去し、アドオンを無効化してUIを閉じます"
def execute(self, context):
col_name = f"{PREFIX_VAL}_Collection"
col = bpy.data.collections.get(col_name)
if col:
for obj in list(col.objects):
bpy.data.objects.remove(obj, do_unlink=True)
bpy.data.collections.remove(col)
for mesh in list(bpy.data.meshes):
if mesh.name.startswith(PREFIX_VAL):
bpy.data.meshes.remove(mesh, do_unlink=True)
# 追加: カーブデータもクリーンアップ
for crv in list(bpy.data.curves):
if crv.name.startswith(PREFIX_VAL):
bpy.data.curves.remove(crv, do_unlink=True)
for mat in list(bpy.data.materials):
if mat.name.startswith(f"{PREFIX_VAL}_Mat"):
bpy.data.materials.remove(mat, do_unlink=True)
bpy.app.timers.register(delayed_unregister, first_interval=0.2)
self.report({'INFO'}, "アドオンのデータをクリーンアップし、終了しました")
return {'FINISHED'}
# =========================================================
# 10. パネル構成 (UI)
# =========================================================
class PT_Base(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
class PT_Torus(PT_Base):
bl_label = "1. トーラス (Torus)"
bl_idname = f"{PREFIX_VAL}_PT_torus"
bl_order = 1
def draw(self, context):
layout = self.layout
props = context.scene.tcs_arr_props
col = layout.column(align=True)
col.prop(props, "tr_loc")
col.prop(props, "tr_rot")
col.separator()
col.prop(props, "tr_mj_rd")
col.prop(props, "tr_mn_rd")
col.prop(props, "tr_cl")
layout.separator()
row = layout.row(align=True)
if not props.is_tr_created:
row.operator(OT_create_torus.bl_idname, icon='MESH_TORUS')
else:
row.operator(OT_create_torus.bl_idname, text="更新", icon='FILE_REFRESH')
row.operator(OT_detach_torus.bl_idname, icon='UNLINKED')
class PT_Cone(PT_Base):
bl_label = "2. 円錐台 (Cone)"
bl_idname = f"{PREFIX_VAL}_PT_cone"
bl_order = 2
def draw(self, context):
layout = self.layout
props = context.scene.tcs_arr_props
col = layout.column(align=True)
col.prop(props, "cn_loc")
col.prop(props, "cn_rot")
col.separator()
row = col.row(align=True)
row.prop(props, "cn_rd_top")
row.prop(props, "cn_rd_bottom")
col.prop(props, "cn_ht")
col.separator()
row = col.row(align=True)
row.prop(props, "cn_no_caps")
row.prop(props, "cn_flip_normals")
col.separator()
row = col.row(align=True)
row.prop(props, "cn_cl", text="表面")
row.prop(props, "cn_cl_back", text="裏面")
layout.separator()
row = layout.row(align=True)
if not props.is_cn_created:
row.operator(OT_create_cone.bl_idname, icon='MESH_CONE')
else:
row.operator(OT_create_cone.bl_idname, text="更新", icon='FILE_REFRESH')
row.operator(OT_detach_cone.bl_idname, icon='UNLINKED')
class PT_Sphere(PT_Base):
bl_label = "3. 球体 (Sphere)"
bl_idname = f"{PREFIX_VAL}_PT_sphere"
bl_order = 3
def draw(self, context):
layout = self.layout
props = context.scene.tcs_arr_props
col = layout.column(align=True)
col.prop(props, "sp_loc")
col.prop(props, "sp_rot")
col.separator()
col.prop(props, "sp_rd")
col.prop(props, "sp_cl")
layout.separator()
row = layout.row(align=True)
if not props.is_sp_created:
row.operator(OT_create_sphere.bl_idname, icon='MESH_UVSPHERE')
else:
row.operator(OT_create_sphere.bl_idname, text="更新", icon='FILE_REFRESH')
row.operator(OT_detach_sphere.bl_idname, icon='UNLINKED')
class PT_Arrows(PT_Base):
bl_label = "4. 集中矢印 (Arrows)"
bl_idname = f"{PREFIX_VAL}_PT_arrows"
bl_order = 4
def draw(self, context):
layout = self.layout
props = context.scene.tcs_arr_props
layout.label(text="※トーラス円周からターゲットへ向かいます", icon='INFO')
col = layout.column(align=True)
col.prop(props, "aw_cnt")
col.prop(props, "aw_tg_loc")
col.prop(props, "aw_offset_pct")
col.separator()
col.prop(props, "aw_thk")
col.prop(props, "aw_cl")
layout.separator()
row = layout.row(align=True)
if not props.is_aw_created:
row.operator(OT_create_arrows.bl_idname, icon='TRACKING')
else:
row.operator(OT_create_arrows.bl_idname, text="更新", icon='FILE_REFRESH')
row.operator(OT_detach_arrows.bl_idname, icon='UNLINKED')
class PT_Dome(PT_Base):
bl_label = "5. 骨格半球ドーム (Dome)"
bl_idname = f"{PREFIX_VAL}_PT_dome"
bl_order = 5
def draw(self, context):
layout = self.layout
props = context.scene.tcs_arr_props
col = layout.column(align=True)
col.prop(props, "dm_loc")
col.prop(props, "dm_rot")
col.separator()
col.prop(props, "dm_rd")
col.prop(props, "dm_thk")
col.separator()
row = col.row(align=True)
row.prop(props, "dm_ln_cn")
row.prop(props, "dm_lt_cn")
col.prop(props, "dm_sg")
col.separator()
col.prop(props, "dm_cl")
layout.separator()
row = layout.row(align=True)
if not props.is_dm_created:
row.operator(OT_create_dome.bl_idname, icon='MESH_UVSPHERE')
else:
row.operator(OT_create_dome.bl_idname, text="更新", icon='FILE_REFRESH')
row.operator(OT_detach_dome.bl_idname, icon='UNLINKED')
class PT_Links(PT_Base):
bl_label = "Links"
bl_idname = f"{PREFIX_VAL}_PT_links"
bl_order = 10
def draw(self, context):
layout = self.layout
box = layout.box()
if LINK_NAME_1: box.operator(OT_open_url.bl_idname, text=LINK_NAME_1, icon='URL').url = LINK_URL_1
if LINK_NAME_2: box.operator(OT_open_url.bl_idname, text=LINK_NAME_2, icon='URL').url = LINK_URL_2
if LINK_NAME_3: box.operator(OT_open_url.bl_idname, text=LINK_NAME_3, icon='URL').url = LINK_URL_3
if LINK_NAME_4: box.operator(OT_open_url.bl_idname, text=LINK_NAME_4, icon='URL').url = LINK_URL_4
if LINK_NAME_5: box.operator(OT_open_url.bl_idname, text=LINK_NAME_5, icon='URL').url = LINK_URL_5
class PT_Remove(PT_Base):
bl_label = "アドオン削除"
bl_idname = f"{PREFIX_VAL}_PT_remove"
bl_options = {'DEFAULT_CLOSED'}
bl_order = 11
def draw(self, context):
layout = self.layout
box = layout.box()
box.label(text="生成物を全消去してアドオンを終了します", icon='ERROR')
box.operator(OT_remove_addon.bl_idname, icon='CANCEL')
# =========================================================
# 11. 自動セットアップ
# =========================================================
def setup_workspace_on_activation():
if not hasattr(bpy.context, 'window_manager') or not bpy.context.window_manager.windows:
return 0.1
try:
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.shading.type = 'MATERIAL'
space.show_region_ui = True
except Exception:
pass
return None
# =========================================================
# 12. 登録と解除
# =========================================================
classes = [
TCS_ARR_PropertyGroup,
OT_create_torus, OT_create_cone, OT_create_sphere, OT_create_arrows, OT_create_dome,
OT_detach_torus, OT_detach_cone, OT_detach_sphere, OT_detach_arrows, OT_detach_dome,
OT_open_url, OT_remove_addon,
PT_Torus, PT_Cone, PT_Sphere, PT_Arrows, PT_Dome, PT_Links, PT_Remove
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.tcs_arr_props = bpy.props.PointerProperty(type=TCS_ARR_PropertyGroup)
if not bpy.app.timers.is_registered(setup_workspace_on_activation):
bpy.app.timers.register(setup_workspace_on_activation, first_interval=0.1)
def unregister():
try:
del bpy.types.Scene.tcs_arr_props
except Exception:
pass
for cls in reversed(classes):
try:
bpy.utils.unregister_class(cls)
except RuntimeError:
pass
if bpy.app.timers.is_registered(setup_workspace_on_activation):
bpy.app.timers.unregister(setup_workspace_on_activation)
if __name__ == "__main__":
register()
# =========================================================
# 1. bl_info
# =========================================================
bl_info = {
"name": "TCS & Arrows Generator",
"author": "Your Name",
"version": (2, 4, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar",
"description": "トーラス・円錐台・球体・集中矢印(透明度完全対応・汎用円錐台・オフセット対応版)",
"category": "3D View"
}
# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0725_1530
# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import time
import webbrowser
import addon_utils
from mathutils import Vector, Euler
# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "TCS & Arrows"
PREFIX_VAL = "TCS_ARR"
ADDON_CATEGORY = "TCS Gen"
PREFIX = PREFIX_VAL.lower()
# =========================================================
# 5. リンク定義
# =========================================================
LINK_NAME_1 = "マニュアルページ"
LINK_URL_1 = "<https://example.com/manual>"
LINK_NAME_2 = "コンテナ builder"
LINK_URL_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"
LINK_NAME_3 = "posfie"
LINK_URL_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_4 = "zionadchat"
LINK_URL_4 = "<https://x.com/zionadchat>"
LINK_NAME_5 = ""
LINK_URL_5 = ""
# =========================================================
# 6. 共通ユーティリティ
# =========================================================
class TCS_ARR_Utility:
@staticmethod
def get_or_create_collection(name):
col = bpy.data.collections.get(name)
if not col:
col = bpy.data.collections.new(name)
bpy.context.scene.collection.children.link(col)
return col
@staticmethod
def get_or_create_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
# 透明度をマテリアルプレビューでも反映させるための設定
try:
if hasattr(mat, "blend_method"):
mat.blend_method = 'BLEND'
if hasattr(mat, "shadow_method"):
mat.shadow_method = 'NONE'
except Exception:
pass
if mat.use_nodes:
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = (color[0], color[1], color[2], 1.0)
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
mat.diffuse_color = (color[0], color[1], color[2], color[3])
return mat
@staticmethod
def apply_mesh(obj_name, mesh_func, mat):
col = TCS_ARR_Utility.get_or_create_collection(f"{PREFIX_VAL}_Collection")
obj = bpy.data.objects.get(obj_name)
if obj:
old_mesh = obj.data
obj.data = mesh_func()
if old_mesh.users == 0:
bpy.data.meshes.remove(old_mesh, do_unlink=True)
else:
mesh = mesh_func()
obj = bpy.data.objects.new(obj_name, mesh)
col.objects.link(obj)
if len(obj.data.materials) == 0:
obj.data.materials.append(mat)
else:
obj.data.materials[0] = mat
return obj
@staticmethod
def detach_object(context, obj_name, base_new_name, flag_attr):
obj = bpy.data.objects.get(obj_name)
if not obj:
return False
ts_str = time.strftime("%H%M%S")
new_name = f"{base_new_name}_{ts_str}"
obj.name = new_name
if obj.data:
obj.data.name = f"{new_name}_Mesh"
for slot in obj.material_slots:
if slot.material and slot.material.name.startswith(PREFIX_VAL):
new_mat = slot.material.copy()
new_mat.name = f"{new_name}_Mat"
slot.material = new_mat
main_col = context.scene.collection
if obj.name not in main_col.objects:
main_col.objects.link(obj)
addon_col = bpy.data.collections.get(f"{PREFIX_VAL}_Collection")
if addon_col and obj.name in addon_col.objects:
addon_col.objects.unlink(obj)
setattr(context.scene.tcs_arr_props, flag_attr, False)
return True
# =========================================================
# 7. 更新処理 (Update Functions)
# =========================================================
def update_torus(self, context):
if not self.is_tr_created: return
mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Torus", self.tr_cl)
def make_torus():
major_r = self.tr_mj_rd
minor_r = self.tr_mn_rd
segments, minor_segments = 48, 16
verts, faces = [], []
for i in range(segments):
a1 = (i / segments) * 2.0 * math.pi
c1, s1 = math.cos(a1), math.sin(a1)
for j in range(minor_segments):
a2 = (j / minor_segments) * 2.0 * math.pi
c2, s2 = math.cos(a2), math.sin(a2)
x = (major_r + minor_r * c2) * c1
y = (major_r + minor_r * c2) * s1
z = minor_r * s2
verts.append((x, y, z))
for i in range(segments):
for j in range(minor_segments):
i_next = (i + 1) % segments
j_next = (j + 1) % minor_segments
v0 = i * minor_segments + j
v1 = i_next * minor_segments + j
v2 = i_next * minor_segments + j_next
v3 = i * minor_segments + j_next
faces.append((v0, v1, v2, v3))
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Torus_Mesh")
me.from_pydata(verts, [], faces)
me.update()
return me
obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Torus", make_torus, mat)
obj.location = self.tr_loc
obj.rotation_euler = self.tr_rot
if self.is_aw_created:
update_arrows(self, context)
def update_cone(self, context):
if not self.is_cn_created: return
mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Cone", self.cn_cl)
def make_cone():
bm = bmesh.new()
ht = self.cn_ht
abs_ht = abs(ht) if abs(ht) > 0.001 else 0.001
# 高さの正負による制御
# Z=0 の位置が常に「下面(Bottom)」の指定半径になるように調整
if ht >= 0:
r1 = self.cn_rd_bottom
r2 = self.cn_rd_top
z_offset = abs_ht / 2.0
else:
# 高さがマイナスの場合は下方向に伸びる(Z=0が下面のまま)
r1 = self.cn_rd_top
r2 = self.cn_rd_bottom
z_offset = -abs_ht / 2.0
bmesh.ops.create_cone(
bm,
cap_ends=not self.cn_no_caps, # 蓋の有無
cap_tris=True,
segments=48,
radius1=r1,
radius2=r2,
depth=abs_ht
)
# 指定した基準(Z=0)への移動
bmesh.ops.translate(bm, verts=bm.verts, vec=(0, 0, z_offset))
# 面の法線(裏表)の反転
if self.cn_flip_normals:
bmesh.ops.reverse_faces(bm, faces=bm.faces)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Cone_Mesh")
bm.to_mesh(me)
bm.free()
return me
obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Cone", make_cone, mat)
obj.location = self.cn_loc
obj.rotation_euler = self.cn_rot
def update_sphere(self, context):
if not self.is_sp_created: return
mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Sphere", self.sp_cl)
def make_sphere():
bm = bmesh.new()
bmesh.ops.create_uvsphere(
bm, u_segments=32, v_segments=16, radius=self.sp_rd
)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Sphere_Mesh")
bm.to_mesh(me)
bm.free()
return me
obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Sphere", make_sphere, mat)
obj.location = self.sp_loc
obj.rotation_euler = self.sp_rot
def update_arrows(self, context):
if not self.is_aw_created: return
mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Arrows", self.aw_cl)
def make_arrows():
bm = bmesh.new()
cnt = self.aw_cnt
tg_loc = Vector(self.aw_tg_loc)
tr_loc = Vector(self.tr_loc)
tr_mat = Euler(self.tr_rot).to_matrix()
tr_R = self.tr_mj_rd
aw_thk = self.aw_thk
pct = self.aw_offset_pct / 100.0
for i in range(cnt):
theta = i * (2 * math.pi / cnt)
local_p = Vector((tr_R * math.cos(theta), tr_R * math.sin(theta), 0))
start_p = tr_loc + tr_mat @ local_p
vec = tg_loc - start_p
full_dist = vec.length
if full_dist < 0.001: continue
dist = full_dist * pct
if dist < 0.001: continue
dir_vec = vec.normalized()
rot_quat = Vector((0,0,1)).rotation_difference(dir_vec)
head_len = aw_thk * 4.0
if head_len > dist * 0.5:
head_len = dist * 0.5
shaft_len = dist - head_len
geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12,
radius1=aw_thk, radius2=aw_thk, depth=shaft_len)
verts_shaft = geom['verts']
bmesh.ops.translate(bm, verts=verts_shaft, vec=(0,0,shaft_len/2))
geom_h = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12,
radius1=aw_thk*2.5, radius2=0, depth=head_len)
verts_head = geom_h['verts']
bmesh.ops.translate(bm, verts=verts_head, vec=(0,0,shaft_len + head_len/2))
verts_all = verts_shaft + verts_head
bmesh.ops.transform(bm, verts=verts_all, matrix=rot_quat.to_matrix().to_4x4())
bmesh.ops.translate(bm, verts=verts_all, vec=start_p)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Arrows_Mesh")
bm.to_mesh(me)
bm.free()
return me
obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Arrows", make_arrows, mat)
obj.location = (0, 0, 0)
obj.rotation_euler = (0, 0, 0)
# =========================================================
# 8. プロパティグループ
# =========================================================
class TCS_ARR_PropertyGroup(bpy.types.PropertyGroup):
# Torus
is_tr_created: bpy.props.BoolProperty(default=False)
tr_loc: bpy.props.FloatVectorProperty(name="Location", default=(0,0,0), update=update_torus)
tr_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_torus)
tr_mj_rd: bpy.props.FloatProperty(name="Major Radius", default=3.0, min=0.1, update=update_torus)
tr_mn_rd: bpy.props.FloatProperty(name="Minor Radius", default=0.2, min=0.01, update=update_torus)
tr_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(0.2, 0.6, 1.0, 1.0), min=0, max=1, update=update_torus)
# Cone (円錐台対応)
is_cn_created: bpy.props.BoolProperty(default=False)
cn_loc: bpy.props.FloatVectorProperty(name="Location", default=(5,0,0), update=update_cone)
cn_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_cone)
cn_rd_top: bpy.props.FloatProperty(name="上面半径", default=0.0, min=0.0, update=update_cone)
cn_rd_bottom: bpy.props.FloatProperty(name="下面半径", default=2.0, min=0.0, update=update_cone)
cn_ht: bpy.props.FloatProperty(name="Height (マイナス許容)", default=4.0, update=update_cone)
# 円錐の拡張設定
cn_no_caps: bpy.props.BoolProperty(name="蓋無し", default=False, update=update_cone)
cn_flip_normals: bpy.props.BoolProperty(name="法線の反転 (裏表)", default=False, update=update_cone)
cn_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(1.0, 0.5, 0.1, 1.0), min=0, max=1, update=update_cone)
# Sphere
is_sp_created: bpy.props.BoolProperty(default=False)
sp_loc: bpy.props.FloatVectorProperty(name="Location", default=(-5,0,0), update=update_sphere)
sp_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_sphere)
sp_rd: bpy.props.FloatProperty(name="Radius", default=1.5, min=0.1, update=update_sphere)
sp_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(0.2, 0.9, 0.4, 1.0), min=0, max=1, update=update_sphere)
# Arrows
is_aw_created: bpy.props.BoolProperty(default=False)
aw_cnt: bpy.props.IntProperty(name="矢印の本数", default=8, min=1, max=100, update=update_arrows)
aw_tg_loc: bpy.props.FloatVectorProperty(name="Target Loc", default=(0,0,10.0), update=update_arrows)
aw_offset_pct: bpy.props.FloatProperty(name="先端オフセット (%)", default=100.0, min=1.0, max=100.0, subtype='PERCENTAGE', update=update_arrows)
aw_thk: bpy.props.FloatProperty(name="Thickness", default=0.1, min=0.01, update=update_arrows)
aw_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(1.0, 0.1, 0.1, 1.0), min=0, max=1, update=update_arrows)
# =========================================================
# 9. Operators
# =========================================================
class OT_create_torus(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_torus"
bl_label = "トーラスを生成"
def execute(self, context):
context.scene.tcs_arr_props.is_tr_created = True
update_torus(context.scene.tcs_arr_props, context)
return {'FINISHED'}
class OT_create_cone(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_cone"
bl_label = "円錐台を生成"
def execute(self, context):
context.scene.tcs_arr_props.is_cn_created = True
update_cone(context.scene.tcs_arr_props, context)
return {'FINISHED'}
class OT_create_sphere(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_sphere"
bl_label = "球体を生成"
def execute(self, context):
context.scene.tcs_arr_props.is_sp_created = True
update_sphere(context.scene.tcs_arr_props, context)
return {'FINISHED'}
class OT_create_arrows(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_arrows"
bl_label = "矢印を生成"
bl_description = "トーラスの円周からターゲット位置に向けて矢印を生成します"
def execute(self, context):
context.scene.tcs_arr_props.is_aw_created = True
update_arrows(context.scene.tcs_arr_props, context)
return {'FINISHED'}
class OT_detach_torus(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_torus"
bl_label = "切り離し(独立)"
def execute(self, context):
if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Torus", "Detached_Torus", "is_tr_created"):
self.report({'INFO'}, "トーラスを独立させました")
return {'FINISHED'}
class OT_detach_cone(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_cone"
bl_label = "切り離し(独立)"
def execute(self, context):
if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Cone", "Detached_Cone", "is_cn_created"):
self.report({'INFO'}, "円錐台を独立させました")
return {'FINISHED'}
class OT_detach_sphere(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_sphere"
bl_label = "切り離し(独立)"
def execute(self, context):
if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Sphere", "Detached_Sphere", "is_sp_created"):
self.report({'INFO'}, "球体を独立させました")
return {'FINISHED'}
class OT_detach_arrows(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_arrows"
bl_label = "切り離し(独立)"
def execute(self, context):
if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Arrows", "Detached_Arrows", "is_aw_created"):
self.report({'INFO'}, "矢印を独立させました")
return {'FINISHED'}
class OT_open_url(bpy.types.Operator):
bl_idname = f"{PREFIX}.open_url"
bl_label = "リンクを開く"
url: bpy.props.StringProperty()
def execute(self, context):
webbrowser.open(self.url)
return {'FINISHED'}
def delayed_unregister():
try:
addon_utils.disable(__name__)
except Exception:
pass
try:
unregister()
except Exception as e:
print(f"[{ADDON_TITLE}] Delayed Unregister Error: {e}")
return None
class OT_remove_addon(bpy.types.Operator):
bl_idname = f"{PREFIX}.remove_addon"
bl_label = "Clean up & Uninstall"
bl_description = "生成したデータを全て消去し、アドオンを無効化してUIを閉じます"
def execute(self, context):
col_name = f"{PREFIX_VAL}_Collection"
col = bpy.data.collections.get(col_name)
if col:
for obj in list(col.objects):
bpy.data.objects.remove(obj, do_unlink=True)
bpy.data.collections.remove(col)
for mesh in list(bpy.data.meshes):
if mesh.name.startswith(PREFIX_VAL):
bpy.data.meshes.remove(mesh, do_unlink=True)
for mat in list(bpy.data.materials):
if mat.name.startswith(f"{PREFIX_VAL}_Mat"):
bpy.data.materials.remove(mat, do_unlink=True)
bpy.app.timers.register(delayed_unregister, first_interval=0.2)
self.report({'INFO'}, "アドオンのデータをクリーンアップし、終了しました")
return {'FINISHED'}
# =========================================================
# 10. パネル構成 (UI)
# =========================================================
class PT_Base(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
class PT_Torus(PT_Base):
bl_label = "1. トーラス (Torus)"
bl_idname = f"{PREFIX_VAL}_PT_torus"
bl_order = 1
def draw(self, context):
layout = self.layout
props = context.scene.tcs_arr_props
col = layout.column(align=True)
col.prop(props, "tr_loc")
col.prop(props, "tr_rot")
col.separator()
col.prop(props, "tr_mj_rd")
col.prop(props, "tr_mn_rd")
col.prop(props, "tr_cl")
layout.separator()
row = layout.row(align=True)
if not props.is_tr_created:
row.operator(OT_create_torus.bl_idname, icon='MESH_TORUS')
else:
row.operator(OT_create_torus.bl_idname, text="更新", icon='FILE_REFRESH')
row.operator(OT_detach_torus.bl_idname, icon='UNLINKED')
class PT_Cone(PT_Base):
bl_label = "2. 円錐台 (Cone)"
bl_idname = f"{PREFIX_VAL}_PT_cone"
bl_order = 2
def draw(self, context):
layout = self.layout
props = context.scene.tcs_arr_props
col = layout.column(align=True)
col.prop(props, "cn_loc")
col.prop(props, "cn_rot")
col.separator()
# 円錐台用の上下半径設定
row = col.row(align=True)
row.prop(props, "cn_rd_top")
row.prop(props, "cn_rd_bottom")
col.prop(props, "cn_ht")
col.separator()
# 蓋の有無と裏表(法線)設定
row = col.row(align=True)
row.prop(props, "cn_no_caps")
row.prop(props, "cn_flip_normals")
col.separator()
col.prop(props, "cn_cl")
layout.separator()
row = layout.row(align=True)
if not props.is_cn_created:
row.operator(OT_create_cone.bl_idname, icon='MESH_CONE')
else:
row.operator(OT_create_cone.bl_idname, text="更新", icon='FILE_REFRESH')
row.operator(OT_detach_cone.bl_idname, icon='UNLINKED')
class PT_Sphere(PT_Base):
bl_label = "3. 球体 (Sphere)"
bl_idname = f"{PREFIX_VAL}_PT_sphere"
bl_order = 3
def draw(self, context):
layout = self.layout
props = context.scene.tcs_arr_props
col = layout.column(align=True)
col.prop(props, "sp_loc")
col.prop(props, "sp_rot")
col.separator()
col.prop(props, "sp_rd")
col.prop(props, "sp_cl")
layout.separator()
row = layout.row(align=True)
if not props.is_sp_created:
row.operator(OT_create_sphere.bl_idname, icon='MESH_UVSPHERE')
else:
row.operator(OT_create_sphere.bl_idname, text="更新", icon='FILE_REFRESH')
row.operator(OT_detach_sphere.bl_idname, icon='UNLINKED')
class PT_Arrows(PT_Base):
bl_label = "4. 集中矢印 (Arrows)"
bl_idname = f"{PREFIX_VAL}_PT_arrows"
bl_order = 4
def draw(self, context):
layout = self.layout
props = context.scene.tcs_arr_props
layout.label(text="※トーラス円周からターゲットへ向かいます", icon='INFO')
col = layout.column(align=True)
col.prop(props, "aw_cnt")
col.prop(props, "aw_tg_loc")
col.prop(props, "aw_offset_pct")
col.separator()
col.prop(props, "aw_thk")
col.prop(props, "aw_cl")
layout.separator()
row = layout.row(align=True)
if not props.is_aw_created:
row.operator(OT_create_arrows.bl_idname, icon='TRACKING')
else:
row.operator(OT_create_arrows.bl_idname, text="更新", icon='FILE_REFRESH')
row.operator(OT_detach_arrows.bl_idname, icon='UNLINKED')
class PT_Links(PT_Base):
bl_label = "Links"
bl_idname = f"{PREFIX_VAL}_PT_links"
bl_order = 10
def draw(self, context):
layout = self.layout
box = layout.box()
if LINK_NAME_1: box.operator(OT_open_url.bl_idname, text=LINK_NAME_1, icon='URL').url = LINK_URL_1
if LINK_NAME_2: box.operator(OT_open_url.bl_idname, text=LINK_NAME_2, icon='URL').url = LINK_URL_2
if LINK_NAME_3: box.operator(OT_open_url.bl_idname, text=LINK_NAME_3, icon='URL').url = LINK_URL_3
if LINK_NAME_4: box.operator(OT_open_url.bl_idname, text=LINK_NAME_4, icon='URL').url = LINK_URL_4
if LINK_NAME_5: box.operator(OT_open_url.bl_idname, text=LINK_NAME_5, icon='URL').url = LINK_URL_5
class PT_Remove(PT_Base):
bl_label = "アドオン削除"
bl_idname = f"{PREFIX_VAL}_PT_remove"
bl_options = {'DEFAULT_CLOSED'}
bl_order = 11
def draw(self, context):
layout = self.layout
box = layout.box()
box.label(text="生成物を全消去してアドオンを終了します", icon='ERROR')
box.operator(OT_remove_addon.bl_idname, icon='CANCEL')
# =========================================================
# 11. 自動セットアップ
# =========================================================
def setup_workspace_on_activation():
if not hasattr(bpy.context, 'window_manager') or not bpy.context.window_manager.windows:
return 0.1
try:
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.shading.type = 'MATERIAL'
space.show_region_ui = True
except Exception:
pass
return None
# =========================================================
# 12. 登録と解除
# =========================================================
classes = [
TCS_ARR_PropertyGroup,
OT_create_torus, OT_create_cone, OT_create_sphere, OT_create_arrows,
OT_detach_torus, OT_detach_cone, OT_detach_sphere, OT_detach_arrows,
OT_open_url, OT_remove_addon,
PT_Torus, PT_Cone, PT_Sphere, PT_Arrows, PT_Links, PT_Remove
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.tcs_arr_props = bpy.props.PointerProperty(type=TCS_ARR_PropertyGroup)
if not bpy.app.timers.is_registered(setup_workspace_on_activation):
bpy.app.timers.register(setup_workspace_on_activation, first_interval=0.1)
def unregister():
try:
del bpy.types.Scene.tcs_arr_props
except Exception:
pass
for cls in reversed(classes):
try:
bpy.utils.unregister_class(cls)
except RuntimeError:
pass
if bpy.app.timers.is_registered(setup_workspace_on_activation):
bpy.app.timers.unregister(setup_workspace_on_activation)
if __name__ == "__main__":
register()
# =========================================================
# 1. bl_info
# =========================================================
bl_info = {
"name": "TCS & Arrows Generator",
"author": "Your Name",
"version": (2, 3, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar",
"description": "トーラス・円錐・球体・集中矢印(透明度完全対応・逆さ円錐・オフセット対応版)",
"category": "3D View"
}
# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0725_1530
# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import time
import webbrowser
import addon_utils
from mathutils import Vector, Euler
# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "TCS & Arrows"
PREFIX_VAL = "TCS_ARR"
ADDON_CATEGORY = "TCS Gen"
PREFIX = PREFIX_VAL.lower()
# =========================================================
# 5. リンク定義
# =========================================================
LINK_NAME_1 = "マニュアルページ"
LINK_URL_1 = "<https://example.com/manual>"
LINK_NAME_2 = "コンテナ builder"
LINK_URL_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"
LINK_NAME_3 = "posfie"
LINK_URL_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_4 = "zionadchat"
LINK_URL_4 = "<https://x.com/zionadchat>"
LINK_NAME_5 = ""
LINK_URL_5 = ""
# =========================================================
# 6. 共通ユーティリティ
# =========================================================
class TCS_ARR_Utility:
@staticmethod
def get_or_create_collection(name):
col = bpy.data.collections.get(name)
if not col:
col = bpy.data.collections.new(name)
bpy.context.scene.collection.children.link(col)
return col
@staticmethod
def get_or_create_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
# 透明度をマテリアルプレビューでも反映させるための設定
try:
if hasattr(mat, "blend_method"):
mat.blend_method = 'BLEND'
if hasattr(mat, "shadow_method"):
mat.shadow_method = 'NONE'
except Exception:
pass
if mat.use_nodes:
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
# Base ColorにはRGBを渡し、Alphaには透明度を個別に渡す(透過不良の原因を排除)
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = (color[0], color[1], color[2], 1.0)
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
# ビューポート(Solid)のカラーにもAlphaを反映
mat.diffuse_color = (color[0], color[1], color[2], color[3])
return mat
@staticmethod
def apply_mesh(obj_name, mesh_func, mat):
col = TCS_ARR_Utility.get_or_create_collection(f"{PREFIX_VAL}_Collection")
obj = bpy.data.objects.get(obj_name)
if obj:
old_mesh = obj.data
obj.data = mesh_func()
if old_mesh.users == 0:
bpy.data.meshes.remove(old_mesh, do_unlink=True)
else:
mesh = mesh_func()
obj = bpy.data.objects.new(obj_name, mesh)
col.objects.link(obj)
if len(obj.data.materials) == 0:
obj.data.materials.append(mat)
else:
obj.data.materials[0] = mat
return obj
@staticmethod
def detach_object(context, obj_name, base_new_name, flag_attr):
"""アドオンの管理下からオブジェクトを切り離し、独立させる"""
obj = bpy.data.objects.get(obj_name)
if not obj:
return False
ts_str = time.strftime("%H%M%S")
new_name = f"{base_new_name}_{ts_str}"
obj.name = new_name
if obj.data:
obj.data.name = f"{new_name}_Mesh"
for slot in obj.material_slots:
if slot.material and slot.material.name.startswith(PREFIX_VAL):
new_mat = slot.material.copy()
new_mat.name = f"{new_name}_Mat"
slot.material = new_mat
main_col = context.scene.collection
if obj.name not in main_col.objects:
main_col.objects.link(obj)
addon_col = bpy.data.collections.get(f"{PREFIX_VAL}_Collection")
if addon_col and obj.name in addon_col.objects:
addon_col.objects.unlink(obj)
setattr(context.scene.tcs_arr_props, flag_attr, False)
return True
# =========================================================
# 7. 更新処理 (Update Functions)
# =========================================================
def update_torus(self, context):
if not self.is_tr_created: return
mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Torus", self.tr_cl)
def make_torus():
major_r = self.tr_mj_rd
minor_r = self.tr_mn_rd
segments, minor_segments = 48, 16
verts, faces = [], []
for i in range(segments):
a1 = (i / segments) * 2.0 * math.pi
c1, s1 = math.cos(a1), math.sin(a1)
for j in range(minor_segments):
a2 = (j / minor_segments) * 2.0 * math.pi
c2, s2 = math.cos(a2), math.sin(a2)
x = (major_r + minor_r * c2) * c1
y = (major_r + minor_r * c2) * s1
z = minor_r * s2
verts.append((x, y, z))
for i in range(segments):
for j in range(minor_segments):
i_next = (i + 1) % segments
j_next = (j + 1) % minor_segments
v0 = i * minor_segments + j
v1 = i_next * minor_segments + j
v2 = i_next * minor_segments + j_next
v3 = i * minor_segments + j_next
faces.append((v0, v1, v2, v3))
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Torus_Mesh")
me.from_pydata(verts, [], faces)
me.update()
return me
obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Torus", make_torus, mat)
obj.location = self.tr_loc
obj.rotation_euler = self.tr_rot
# トーラスが更新されたら矢印も追従して更新する
if self.is_aw_created:
update_arrows(self, context)
def update_cone(self, context):
if not self.is_cn_created: return
mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Cone", self.cn_cl)
def make_cone():
bm = bmesh.new()
ht = self.cn_ht
abs_ht = abs(ht) if abs(ht) > 0.001 else 0.001
# マイナス値の場合は逆さ円錐(下に向かって尖る)にする
if ht >= 0:
r1, r2 = self.cn_rd, 0
z_offset = abs_ht / 2.0
else:
r1, r2 = 0, self.cn_rd
z_offset = -abs_ht / 2.0
bmesh.ops.create_cone(
bm, cap_ends=True, cap_tris=True, segments=48,
radius1=r1, radius2=r2, depth=abs_ht
)
# Z=0 の位置が常に底面になるように移動
bmesh.ops.translate(bm, verts=bm.verts, vec=(0, 0, z_offset))
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Cone_Mesh")
bm.to_mesh(me)
bm.free()
return me
obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Cone", make_cone, mat)
obj.location = self.cn_loc
obj.rotation_euler = self.cn_rot
def update_sphere(self, context):
if not self.is_sp_created: return
mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Sphere", self.sp_cl)
def make_sphere():
bm = bmesh.new()
bmesh.ops.create_uvsphere(
bm, u_segments=32, v_segments=16, radius=self.sp_rd
)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Sphere_Mesh")
bm.to_mesh(me)
bm.free()
return me
obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Sphere", make_sphere, mat)
obj.location = self.sp_loc
obj.rotation_euler = self.sp_rot
def update_arrows(self, context):
if not self.is_aw_created: return
mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Arrows", self.aw_cl)
def make_arrows():
bm = bmesh.new()
cnt = self.aw_cnt
tg_loc = Vector(self.aw_tg_loc)
# トーラスの位置・回転・半径を取得して発生源とする
tr_loc = Vector(self.tr_loc)
tr_mat = Euler(self.tr_rot).to_matrix()
tr_R = self.tr_mj_rd
aw_thk = self.aw_thk
pct = self.aw_offset_pct / 100.0 # オフセット割合(%)
for i in range(cnt):
theta = i * (2 * math.pi / cnt)
local_p = Vector((tr_R * math.cos(theta), tr_R * math.sin(theta), 0))
start_p = tr_loc + tr_mat @ local_p
vec = tg_loc - start_p
full_dist = vec.length
if full_dist < 0.001:
continue
# 割合(オフセット)を反映した長さを計算
dist = full_dist * pct
if dist < 0.001:
continue
dir_vec = vec.normalized()
rot_quat = Vector((0,0,1)).rotation_difference(dir_vec)
head_len = aw_thk * 4.0
if head_len > dist * 0.5:
head_len = dist * 0.5
shaft_len = dist - head_len
# 軸
geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12,
radius1=aw_thk, radius2=aw_thk, depth=shaft_len)
verts_shaft = geom['verts']
bmesh.ops.translate(bm, verts=verts_shaft, vec=(0,0,shaft_len/2))
# 矢尻
geom_h = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12,
radius1=aw_thk*2.5, radius2=0, depth=head_len)
verts_head = geom_h['verts']
bmesh.ops.translate(bm, verts=verts_head, vec=(0,0,shaft_len + head_len/2))
verts_all = verts_shaft + verts_head
bmesh.ops.transform(bm, verts=verts_all, matrix=rot_quat.to_matrix().to_4x4())
bmesh.ops.translate(bm, verts=verts_all, vec=start_p)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Arrows_Mesh")
bm.to_mesh(me)
bm.free()
return me
obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Arrows", make_arrows, mat)
obj.location = (0, 0, 0)
obj.rotation_euler = (0, 0, 0)
# =========================================================
# 8. プロパティグループ
# =========================================================
class TCS_ARR_PropertyGroup(bpy.types.PropertyGroup):
# Torus
is_tr_created: bpy.props.BoolProperty(default=False)
tr_loc: bpy.props.FloatVectorProperty(name="Location", default=(0,0,0), update=update_torus)
tr_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_torus)
tr_mj_rd: bpy.props.FloatProperty(name="Major Radius", default=3.0, min=0.1, update=update_torus)
tr_mn_rd: bpy.props.FloatProperty(name="Minor Radius", default=0.2, min=0.01, update=update_torus)
tr_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(0.2, 0.6, 1.0, 1.0), min=0, max=1, update=update_torus)
# Cone
is_cn_created: bpy.props.BoolProperty(default=False)
cn_loc: bpy.props.FloatVectorProperty(name="Location", default=(5,0,0), update=update_cone)
cn_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_cone)
cn_rd: bpy.props.FloatProperty(name="Radius", default=2.0, min=0.1, update=update_cone)
cn_ht: bpy.props.FloatProperty(name="Height (マイナス許容)", default=4.0, update=update_cone) # min制約を撤廃
cn_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(1.0, 0.5, 0.1, 1.0), min=0, max=1, update=update_cone)
# Sphere
is_sp_created: bpy.props.BoolProperty(default=False)
sp_loc: bpy.props.FloatVectorProperty(name="Location", default=(-5,0,0), update=update_sphere)
sp_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_sphere)
sp_rd: bpy.props.FloatProperty(name="Radius", default=1.5, min=0.1, update=update_sphere)
sp_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(0.2, 0.9, 0.4, 1.0), min=0, max=1, update=update_sphere)
# Arrows
is_aw_created: bpy.props.BoolProperty(default=False)
aw_cnt: bpy.props.IntProperty(name="矢印の本数", default=8, min=1, max=100, update=update_arrows)
aw_tg_loc: bpy.props.FloatVectorProperty(name="Target Loc", default=(0,0,10.0), update=update_arrows)
aw_offset_pct: bpy.props.FloatProperty(name="先端オフセット (%)", default=100.0, min=1.0, max=100.0, subtype='PERCENTAGE', update=update_arrows)
aw_thk: bpy.props.FloatProperty(name="Thickness", default=0.1, min=0.01, update=update_arrows)
aw_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(1.0, 0.1, 0.1, 1.0), min=0, max=1, update=update_arrows)
# =========================================================
# 9. Operators
# =========================================================
class OT_create_torus(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_torus"
bl_label = "トーラスを生成"
def execute(self, context):
context.scene.tcs_arr_props.is_tr_created = True
update_torus(context.scene.tcs_arr_props, context)
return {'FINISHED'}
class OT_create_cone(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_cone"
bl_label = "円錐を生成"
def execute(self, context):
context.scene.tcs_arr_props.is_cn_created = True
update_cone(context.scene.tcs_arr_props, context)
return {'FINISHED'}
class OT_create_sphere(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_sphere"
bl_label = "球体を生成"
def execute(self, context):
context.scene.tcs_arr_props.is_sp_created = True
update_sphere(context.scene.tcs_arr_props, context)
return {'FINISHED'}
class OT_create_arrows(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_arrows"
bl_label = "矢印を生成"
bl_description = "トーラスの円周からターゲット位置に向けて矢印を生成します"
def execute(self, context):
context.scene.tcs_arr_props.is_aw_created = True
update_arrows(context.scene.tcs_arr_props, context)
return {'FINISHED'}
class OT_detach_torus(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_torus"
bl_label = "切り離し(独立)"
def execute(self, context):
if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Torus", "Detached_Torus", "is_tr_created"):
self.report({'INFO'}, "トーラスを独立させました")
return {'FINISHED'}
class OT_detach_cone(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_cone"
bl_label = "切り離し(独立)"
def execute(self, context):
if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Cone", "Detached_Cone", "is_cn_created"):
self.report({'INFO'}, "円錐を独立させました")
return {'FINISHED'}
class OT_detach_sphere(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_sphere"
bl_label = "切り離し(独立)"
def execute(self, context):
if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Sphere", "Detached_Sphere", "is_sp_created"):
self.report({'INFO'}, "球体を独立させました")
return {'FINISHED'}
class OT_detach_arrows(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_arrows"
bl_label = "切り離し(独立)"
def execute(self, context):
if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Arrows", "Detached_Arrows", "is_aw_created"):
self.report({'INFO'}, "矢印を独立させました")
return {'FINISHED'}
class OT_open_url(bpy.types.Operator):
bl_idname = f"{PREFIX}.open_url"
bl_label = "リンクを開く"
url: bpy.props.StringProperty()
def execute(self, context):
webbrowser.open(self.url)
return {'FINISHED'}
def delayed_unregister():
try:
addon_utils.disable(__name__)
except Exception:
pass
try:
unregister()
except Exception as e:
print(f"[{ADDON_TITLE}] Delayed Unregister Error: {e}")
return None
class OT_remove_addon(bpy.types.Operator):
bl_idname = f"{PREFIX}.remove_addon"
bl_label = "Clean up & Uninstall"
bl_description = "生成したデータを全て消去し、アドオンを無効化してUIを閉じます"
def execute(self, context):
col_name = f"{PREFIX_VAL}_Collection"
col = bpy.data.collections.get(col_name)
if col:
for obj in list(col.objects):
bpy.data.objects.remove(obj, do_unlink=True)
bpy.data.collections.remove(col)
for mesh in list(bpy.data.meshes):
if mesh.name.startswith(PREFIX_VAL):
bpy.data.meshes.remove(mesh, do_unlink=True)
for mat in list(bpy.data.materials):
if mat.name.startswith(f"{PREFIX_VAL}_Mat"):
bpy.data.materials.remove(mat, do_unlink=True)
bpy.app.timers.register(delayed_unregister, first_interval=0.2)
self.report({'INFO'}, "アドオンのデータをクリーンアップし、終了しました")
return {'FINISHED'}
# =========================================================
# 10. パネル構成 (UI)
# =========================================================
class PT_Base(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
class PT_Torus(PT_Base):
bl_label = "1. トーラス (Torus)"
bl_idname = f"{PREFIX_VAL}_PT_torus"
bl_order = 1
def draw(self, context):
layout = self.layout
props = context.scene.tcs_arr_props
col = layout.column(align=True)
col.prop(props, "tr_loc")
col.prop(props, "tr_rot")
col.separator()
col.prop(props, "tr_mj_rd")
col.prop(props, "tr_mn_rd")
col.prop(props, "tr_cl")
layout.separator()
row = layout.row(align=True)
if not props.is_tr_created:
row.operator(OT_create_torus.bl_idname, icon='MESH_TORUS')
else:
row.operator(OT_create_torus.bl_idname, text="更新", icon='FILE_REFRESH')
row.operator(OT_detach_torus.bl_idname, icon='UNLINKED')
class PT_Cone(PT_Base):
bl_label = "2. 円錐 (Cone)"
bl_idname = f"{PREFIX_VAL}_PT_cone"
bl_order = 2
def draw(self, context):
layout = self.layout
props = context.scene.tcs_arr_props
col = layout.column(align=True)
col.prop(props, "cn_loc")
col.prop(props, "cn_rot")
col.separator()
col.prop(props, "cn_rd")
col.prop(props, "cn_ht")
col.prop(props, "cn_cl")
layout.separator()
row = layout.row(align=True)
if not props.is_cn_created:
row.operator(OT_create_cone.bl_idname, icon='MESH_CONE')
else:
row.operator(OT_create_cone.bl_idname, text="更新", icon='FILE_REFRESH')
row.operator(OT_detach_cone.bl_idname, icon='UNLINKED')
class PT_Sphere(PT_Base):
bl_label = "3. 球体 (Sphere)"
bl_idname = f"{PREFIX_VAL}_PT_sphere"
bl_order = 3
def draw(self, context):
layout = self.layout
props = context.scene.tcs_arr_props
col = layout.column(align=True)
col.prop(props, "sp_loc")
col.prop(props, "sp_rot")
col.separator()
col.prop(props, "sp_rd")
col.prop(props, "sp_cl")
layout.separator()
row = layout.row(align=True)
if not props.is_sp_created:
row.operator(OT_create_sphere.bl_idname, icon='MESH_UVSPHERE')
else:
row.operator(OT_create_sphere.bl_idname, text="更新", icon='FILE_REFRESH')
row.operator(OT_detach_sphere.bl_idname, icon='UNLINKED')
class PT_Arrows(PT_Base):
bl_label = "4. 集中矢印 (Arrows)"
bl_idname = f"{PREFIX_VAL}_PT_arrows"
bl_order = 4
def draw(self, context):
layout = self.layout
props = context.scene.tcs_arr_props
layout.label(text="※トーラス円周からターゲットへ向かいます", icon='INFO')
col = layout.column(align=True)
col.prop(props, "aw_cnt")
col.prop(props, "aw_tg_loc")
col.prop(props, "aw_offset_pct")
col.separator()
col.prop(props, "aw_thk")
col.prop(props, "aw_cl")
layout.separator()
row = layout.row(align=True)
if not props.is_aw_created:
row.operator(OT_create_arrows.bl_idname, icon='TRACKING')
else:
row.operator(OT_create_arrows.bl_idname, text="更新", icon='FILE_REFRESH')
row.operator(OT_detach_arrows.bl_idname, icon='UNLINKED')
class PT_Links(PT_Base):
bl_label = "Links"
bl_idname = f"{PREFIX_VAL}_PT_links"
bl_order = 10
def draw(self, context):
layout = self.layout
box = layout.box()
if LINK_NAME_1: box.operator(OT_open_url.bl_idname, text=LINK_NAME_1, icon='URL').url = LINK_URL_1
if LINK_NAME_2: box.operator(OT_open_url.bl_idname, text=LINK_NAME_2, icon='URL').url = LINK_URL_2
if LINK_NAME_3: box.operator(OT_open_url.bl_idname, text=LINK_NAME_3, icon='URL').url = LINK_URL_3
if LINK_NAME_4: box.operator(OT_open_url.bl_idname, text=LINK_NAME_4, icon='URL').url = LINK_URL_4
if LINK_NAME_5: box.operator(OT_open_url.bl_idname, text=LINK_NAME_5, icon='URL').url = LINK_URL_5
class PT_Remove(PT_Base):
bl_label = "アドオン削除"
bl_idname = f"{PREFIX_VAL}_PT_remove"
bl_options = {'DEFAULT_CLOSED'}
bl_order = 11
def draw(self, context):
layout = self.layout
box = layout.box()
box.label(text="生成物を全消去してアドオンを終了します", icon='ERROR')
box.operator(OT_remove_addon.bl_idname, icon='CANCEL')
# =========================================================
# 11. 自動セットアップ
# =========================================================
def setup_workspace_on_activation():
if not hasattr(bpy.context, 'window_manager') or not bpy.context.window_manager.windows:
return 0.1
try:
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.shading.type = 'MATERIAL'
space.show_region_ui = True
except Exception:
pass
return None
# =========================================================
# 12. 登録と解除
# =========================================================
classes = [
TCS_ARR_PropertyGroup,
OT_create_torus, OT_create_cone, OT_create_sphere, OT_create_arrows,
OT_detach_torus, OT_detach_cone, OT_detach_sphere, OT_detach_arrows,
OT_open_url, OT_remove_addon,
PT_Torus, PT_Cone, PT_Sphere, PT_Arrows, PT_Links, PT_Remove
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.tcs_arr_props = bpy.props.PointerProperty(type=TCS_ARR_PropertyGroup)
if not bpy.app.timers.is_registered(setup_workspace_on_activation):
bpy.app.timers.register(setup_workspace_on_activation, first_interval=0.1)
def unregister():
try:
del bpy.types.Scene.tcs_arr_props
except Exception:
pass
for cls in reversed(classes):
try:
bpy.utils.unregister_class(cls)
except RuntimeError:
pass
if bpy.app.timers.is_registered(setup_workspace_on_activation):
bpy.app.timers.unregister(setup_workspace_on_activation)
if __name__ == "__main__":
register()
# =========================================================
# 1. bl_info
# =========================================================
bl_info = {
"name": "TCS & Arrows Generator",
"author": "Your Name",
"version": (2, 1, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar",
"description": "トーラス・円錐・球体・集中矢印を個別に生成・管理・自己クリーンアップ可能",
"category": "3D View"
}
# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0725_1430
# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import time
import webbrowser
import addon_utils
from mathutils import Vector, Euler
# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "TCS & Arrows"
PREFIX_VAL = "TCS_ARR"
ADDON_CATEGORY = "TCS Gen"
PREFIX = PREFIX_VAL.lower()
# =========================================================
# 5. リンク定義
# =========================================================
LINK_NAME_1 = "マニュアルページ"
LINK_URL_1 = "<https://example.com/manual>"
LINK_NAME_2 = "コンテナ builder"
LINK_URL_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"
LINK_NAME_3 = "posfie"
LINK_URL_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_4 = "zionadchat"
LINK_URL_4 = "<https://x.com/zionadchat>"
LINK_NAME_5 = ""
LINK_URL_5 = ""
# =========================================================
# 6. 共通ユーティリティ
# =========================================================
class TCS_ARR_Utility:
@staticmethod
def get_or_create_collection(name):
col = bpy.data.collections.get(name)
if not col:
col = bpy.data.collections.new(name)
bpy.context.scene.collection.children.link(col)
return col
@staticmethod
def get_or_create_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
if mat.use_nodes:
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
return mat
@staticmethod
def apply_mesh(obj_name, mesh_func, mat):
col = TCS_ARR_Utility.get_or_create_collection(f"{PREFIX_VAL}_Collection")
obj = bpy.data.objects.get(obj_name)
if obj:
old_mesh = obj.data
obj.data = mesh_func()
if old_mesh.users == 0:
bpy.data.meshes.remove(old_mesh, do_unlink=True)
else:
mesh = mesh_func()
obj = bpy.data.objects.new(obj_name, mesh)
col.objects.link(obj)
if len(obj.data.materials) == 0:
obj.data.materials.append(mat)
else:
obj.data.materials[0] = mat
return obj
@staticmethod
def detach_object(context, obj_name, base_new_name, flag_attr):
"""アドオンの管理下からオブジェクトを切り離し、独立させる"""
obj = bpy.data.objects.get(obj_name)
if not obj:
return False
ts_str = time.strftime("%H%M%S")
new_name = f"{base_new_name}_{ts_str}"
obj.name = new_name
if obj.data:
obj.data.name = f"{new_name}_Mesh"
for slot in obj.material_slots:
if slot.material and slot.material.name.startswith(PREFIX_VAL):
new_mat = slot.material.copy()
new_mat.name = f"{new_name}_Mat"
slot.material = new_mat
main_col = context.scene.collection
if obj.name not in main_col.objects:
main_col.objects.link(obj)
addon_col = bpy.data.collections.get(f"{PREFIX_VAL}_Collection")
if addon_col and obj.name in addon_col.objects:
addon_col.objects.unlink(obj)
setattr(context.scene.tcs_arr_props, flag_attr, False)
return True
# =========================================================
# 7. 更新処理 (Update Functions)
# =========================================================
def update_torus(self, context):
if not self.is_tr_created: return
mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Torus", self.tr_cl)
def make_torus():
major_r = self.tr_mj_rd
minor_r = self.tr_mn_rd
segments, minor_segments = 48, 16
verts, faces = [], []
for i in range(segments):
a1 = (i / segments) * 2.0 * math.pi
c1, s1 = math.cos(a1), math.sin(a1)
for j in range(minor_segments):
a2 = (j / minor_segments) * 2.0 * math.pi
c2, s2 = math.cos(a2), math.sin(a2)
x = (major_r + minor_r * c2) * c1
y = (major_r + minor_r * c2) * s1
z = minor_r * s2
verts.append((x, y, z))
for i in range(segments):
for j in range(minor_segments):
i_next = (i + 1) % segments
j_next = (j + 1) % minor_segments
v0 = i * minor_segments + j
v1 = i_next * minor_segments + j
v2 = i_next * minor_segments + j_next
v3 = i * minor_segments + j_next
faces.append((v0, v1, v2, v3))
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Torus_Mesh")
me.from_pydata(verts, [], faces)
me.update()
return me
obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Torus", make_torus, mat)
obj.location = self.tr_loc
obj.rotation_euler = self.tr_rot
# トーラスが更新されたら矢印も追従して更新する
if self.is_aw_created:
update_arrows(self, context)
def update_cone(self, context):
if not self.is_cn_created: return
mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Cone", self.cn_cl)
def make_cone():
bm = bmesh.new()
bmesh.ops.create_cone(
bm, cap_ends=True, cap_tris=True, segments=48,
radius1=self.cn_rd, radius2=0, depth=self.cn_ht
)
bmesh.ops.translate(bm, verts=bm.verts, vec=(0, 0, self.cn_ht / 2))
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Cone_Mesh")
bm.to_mesh(me)
bm.free()
return me
obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Cone", make_cone, mat)
obj.location = self.cn_loc
obj.rotation_euler = self.cn_rot
def update_sphere(self, context):
if not self.is_sp_created: return
mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Sphere", self.sp_cl)
def make_sphere():
bm = bmesh.new()
bmesh.ops.create_uvsphere(
bm, u_segments=32, v_segments=16, radius=self.sp_rd
)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Sphere_Mesh")
bm.to_mesh(me)
bm.free()
return me
obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Sphere", make_sphere, mat)
obj.location = self.sp_loc
obj.rotation_euler = self.sp_rot
def update_arrows(self, context):
if not self.is_aw_created: return
mat = TCS_ARR_Utility.get_or_create_material(f"{PREFIX_VAL}_Mat_Arrows", self.aw_cl)
def make_arrows():
bm = bmesh.new()
cnt = self.aw_cnt
tg_loc = Vector(self.aw_tg_loc)
# トーラスの位置・回転・半径を取得して発生源とする
tr_loc = Vector(self.tr_loc)
tr_mat = Euler(self.tr_rot).to_matrix()
tr_R = self.tr_mj_rd
aw_thk = self.aw_thk
for i in range(cnt):
theta = i * (2 * math.pi / cnt)
local_p = Vector((tr_R * math.cos(theta), tr_R * math.sin(theta), 0))
start_p = tr_loc + tr_mat @ local_p
vec = tg_loc - start_p
dist = vec.length
if dist < 0.001:
continue
dir_vec = vec.normalized()
rot_quat = Vector((0,0,1)).rotation_difference(dir_vec)
head_len = aw_thk * 4.0
if head_len > dist * 0.5:
head_len = dist * 0.5
shaft_len = dist - head_len
# 軸
geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12,
radius1=aw_thk, radius2=aw_thk, depth=shaft_len)
verts_shaft = geom['verts']
bmesh.ops.translate(bm, verts=verts_shaft, vec=(0,0,shaft_len/2))
# 矢尻
geom_h = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12,
radius1=aw_thk*2.5, radius2=0, depth=head_len)
verts_head = geom_h['verts']
bmesh.ops.translate(bm, verts=verts_head, vec=(0,0,shaft_len + head_len/2))
verts_all = verts_shaft + verts_head
bmesh.ops.transform(bm, verts=verts_all, matrix=rot_quat.to_matrix().to_4x4())
bmesh.ops.translate(bm, verts=verts_all, vec=start_p)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Arrows_Mesh")
bm.to_mesh(me)
bm.free()
return me
obj = TCS_ARR_Utility.apply_mesh(f"{PREFIX_VAL}_Arrows", make_arrows, mat)
obj.location = (0, 0, 0)
obj.rotation_euler = (0, 0, 0)
# =========================================================
# 8. プロパティグループ
# =========================================================
class TCS_ARR_PropertyGroup(bpy.types.PropertyGroup):
# Torus
is_tr_created: bpy.props.BoolProperty(default=False)
tr_loc: bpy.props.FloatVectorProperty(name="Location", default=(0,0,0), update=update_torus)
tr_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_torus)
tr_mj_rd: bpy.props.FloatProperty(name="Major Radius", default=3.0, min=0.1, update=update_torus)
tr_mn_rd: bpy.props.FloatProperty(name="Minor Radius", default=0.2, min=0.01, update=update_torus)
tr_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(0.2, 0.6, 1.0, 1.0), min=0, max=1, update=update_torus)
# Cone
is_cn_created: bpy.props.BoolProperty(default=False)
cn_loc: bpy.props.FloatVectorProperty(name="Location", default=(5,0,0), update=update_cone)
cn_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_cone)
cn_rd: bpy.props.FloatProperty(name="Radius", default=2.0, min=0.1, update=update_cone)
cn_ht: bpy.props.FloatProperty(name="Height", default=4.0, min=0.1, update=update_cone)
cn_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(1.0, 0.5, 0.1, 1.0), min=0, max=1, update=update_cone)
# Sphere
is_sp_created: bpy.props.BoolProperty(default=False)
sp_loc: bpy.props.FloatVectorProperty(name="Location", default=(-5,0,0), update=update_sphere)
sp_rot: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(0,0,0), update=update_sphere)
sp_rd: bpy.props.FloatProperty(name="Radius", default=1.5, min=0.1, update=update_sphere)
sp_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(0.2, 0.9, 0.4, 1.0), min=0, max=1, update=update_sphere)
# Arrows
is_aw_created: bpy.props.BoolProperty(default=False)
aw_cnt: bpy.props.IntProperty(name="矢印の本数", default=8, min=1, max=100, update=update_arrows)
aw_tg_loc: bpy.props.FloatVectorProperty(name="Target Loc", default=(0,0,10.0), update=update_arrows)
aw_thk: bpy.props.FloatProperty(name="Thickness", default=0.1, min=0.01, update=update_arrows)
aw_cl: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=(1.0, 0.1, 0.1, 1.0), min=0, max=1, update=update_arrows)
# =========================================================
# 9. Operators
# =========================================================
class OT_create_torus(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_torus"
bl_label = "トーラスを生成"
def execute(self, context):
context.scene.tcs_arr_props.is_tr_created = True
update_torus(context.scene.tcs_arr_props, context)
return {'FINISHED'}
class OT_create_cone(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_cone"
bl_label = "円錐を生成"
def execute(self, context):
context.scene.tcs_arr_props.is_cn_created = True
update_cone(context.scene.tcs_arr_props, context)
return {'FINISHED'}
class OT_create_sphere(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_sphere"
bl_label = "球体を生成"
def execute(self, context):
context.scene.tcs_arr_props.is_sp_created = True
update_sphere(context.scene.tcs_arr_props, context)
return {'FINISHED'}
class OT_create_arrows(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_arrows"
bl_label = "矢印を生成"
bl_description = "トーラスの円周からターゲット位置に向けて矢印を生成します"
def execute(self, context):
context.scene.tcs_arr_props.is_aw_created = True
update_arrows(context.scene.tcs_arr_props, context)
return {'FINISHED'}
class OT_detach_torus(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_torus"
bl_label = "切り離し(独立)"
def execute(self, context):
if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Torus", "Detached_Torus", "is_tr_created"):
self.report({'INFO'}, "トーラスを独立させました")
return {'FINISHED'}
class OT_detach_cone(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_cone"
bl_label = "切り離し(独立)"
def execute(self, context):
if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Cone", "Detached_Cone", "is_cn_created"):
self.report({'INFO'}, "円錐を独立させました")
return {'FINISHED'}
class OT_detach_sphere(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_sphere"
bl_label = "切り離し(独立)"
def execute(self, context):
if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Sphere", "Detached_Sphere", "is_sp_created"):
self.report({'INFO'}, "球体を独立させました")
return {'FINISHED'}
class OT_detach_arrows(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_arrows"
bl_label = "切り離し(独立)"
def execute(self, context):
if TCS_ARR_Utility.detach_object(context, f"{PREFIX_VAL}_Arrows", "Detached_Arrows", "is_aw_created"):
self.report({'INFO'}, "矢印を独立させました")
return {'FINISHED'}
class OT_open_url(bpy.types.Operator):
bl_idname = f"{PREFIX}.open_url"
bl_label = "リンクを開く"
url: bpy.props.StringProperty()
def execute(self, context):
webbrowser.open(self.url)
return {'FINISHED'}
# --- 安全な遅延アンインストール関数 ---
def delayed_unregister():
try:
# アドオンとしてインストールされている場合は完全に無効化する
addon_utils.disable(__name__)
except Exception:
pass
try:
unregister()
except Exception as e:
print(f"[{ADDON_TITLE}] Delayed Unregister Error: {e}")
return None
class OT_remove_addon(bpy.types.Operator):
bl_idname = f"{PREFIX}.remove_addon"
bl_label = "Clean up & Uninstall"
bl_description = "生成したデータを全て消去し、アドオンを無効化してUIを閉じます"
def execute(self, context):
# 1. データのクリーンアップ(自己アンインストール)
col_name = f"{PREFIX_VAL}_Collection"
col = bpy.data.collections.get(col_name)
if col:
# コレクション内のオブジェクトを全削除
for obj in list(col.objects):
bpy.data.objects.remove(obj, do_unlink=True)
bpy.data.collections.remove(col)
# 不要なメッシュデータを削除
for mesh in list(bpy.data.meshes):
if mesh.name.startswith(PREFIX_VAL):
bpy.data.meshes.remove(mesh, do_unlink=True)
# 不要なマテリアルデータを削除
for mat in list(bpy.data.materials):
if mat.name.startswith(f"{PREFIX_VAL}_Mat"):
bpy.data.materials.remove(mat, do_unlink=True)
# 2. 安全な遅延アンインストール(UI描画ループ外で0.2秒後に実行)
bpy.app.timers.register(delayed_unregister, first_interval=0.2)
self.report({'INFO'}, "アドオンのデータをクリーンアップし、終了しました")
return {'FINISHED'}
# =========================================================
# 10. パネル構成 (UI)
# =========================================================
class PT_Base(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
class PT_Torus(PT_Base):
bl_label = "1. トーラス (Torus)"
bl_idname = f"{PREFIX_VAL}_PT_torus"
bl_order = 1
def draw(self, context):
layout = self.layout
props = context.scene.tcs_arr_props
col = layout.column(align=True)
col.prop(props, "tr_loc")
col.prop(props, "tr_rot")
col.separator()
col.prop(props, "tr_mj_rd")
col.prop(props, "tr_mn_rd")
col.prop(props, "tr_cl")
layout.separator()
row = layout.row(align=True)
if not props.is_tr_created:
row.operator(OT_create_torus.bl_idname, icon='MESH_TORUS')
else:
row.operator(OT_create_torus.bl_idname, text="更新", icon='FILE_REFRESH')
row.operator(OT_detach_torus.bl_idname, icon='UNLINKED')
class PT_Cone(PT_Base):
bl_label = "2. 円錐 (Cone)"
bl_idname = f"{PREFIX_VAL}_PT_cone"
bl_order = 2
def draw(self, context):
layout = self.layout
props = context.scene.tcs_arr_props
col = layout.column(align=True)
col.prop(props, "cn_loc")
col.prop(props, "cn_rot")
col.separator()
col.prop(props, "cn_rd")
col.prop(props, "cn_ht")
col.prop(props, "cn_cl")
layout.separator()
row = layout.row(align=True)
if not props.is_cn_created:
row.operator(OT_create_cone.bl_idname, icon='MESH_CONE')
else:
row.operator(OT_create_cone.bl_idname, text="更新", icon='FILE_REFRESH')
row.operator(OT_detach_cone.bl_idname, icon='UNLINKED')
class PT_Sphere(PT_Base):
bl_label = "3. 球体 (Sphere)"
bl_idname = f"{PREFIX_VAL}_PT_sphere"
bl_order = 3
def draw(self, context):
layout = self.layout
props = context.scene.tcs_arr_props
col = layout.column(align=True)
col.prop(props, "sp_loc")
col.prop(props, "sp_rot")
col.separator()
col.prop(props, "sp_rd")
col.prop(props, "sp_cl")
layout.separator()
row = layout.row(align=True)
if not props.is_sp_created:
row.operator(OT_create_sphere.bl_idname, icon='MESH_UVSPHERE')
else:
row.operator(OT_create_sphere.bl_idname, text="更新", icon='FILE_REFRESH')
row.operator(OT_detach_sphere.bl_idname, icon='UNLINKED')
class PT_Arrows(PT_Base):
bl_label = "4. 集中矢印 (Arrows)"
bl_idname = f"{PREFIX_VAL}_PT_arrows"
bl_order = 4
def draw(self, context):
layout = self.layout
props = context.scene.tcs_arr_props
layout.label(text="※トーラス円周からターゲットへ向かいます", icon='INFO')
col = layout.column(align=True)
col.prop(props, "aw_cnt")
col.prop(props, "aw_tg_loc")
col.separator()
col.prop(props, "aw_thk")
col.prop(props, "aw_cl")
layout.separator()
row = layout.row(align=True)
if not props.is_aw_created:
row.operator(OT_create_arrows.bl_idname, icon='TRACKING')
else:
row.operator(OT_create_arrows.bl_idname, text="更新", icon='FILE_REFRESH')
row.operator(OT_detach_arrows.bl_idname, icon='UNLINKED')
class PT_Links(PT_Base):
bl_label = "Links"
bl_idname = f"{PREFIX_VAL}_PT_links"
bl_order = 10
def draw(self, context):
layout = self.layout
box = layout.box()
if LINK_NAME_1: box.operator(OT_open_url.bl_idname, text=LINK_NAME_1, icon='URL').url = LINK_URL_1
if LINK_NAME_2: box.operator(OT_open_url.bl_idname, text=LINK_NAME_2, icon='URL').url = LINK_URL_2
if LINK_NAME_3: box.operator(OT_open_url.bl_idname, text=LINK_NAME_3, icon='URL').url = LINK_URL_3
if LINK_NAME_4: box.operator(OT_open_url.bl_idname, text=LINK_NAME_4, icon='URL').url = LINK_URL_4
if LINK_NAME_5: box.operator(OT_open_url.bl_idname, text=LINK_NAME_5, icon='URL').url = LINK_URL_5
class PT_Remove(PT_Base):
bl_label = "アドオン削除"
bl_idname = f"{PREFIX_VAL}_PT_remove"
bl_options = {'DEFAULT_CLOSED'}
bl_order = 11
def draw(self, context):
layout = self.layout
box = layout.box()
box.label(text="生成物を全消去してアドオンを終了します", icon='ERROR')
box.operator(OT_remove_addon.bl_idname, icon='CANCEL')
# =========================================================
# 11. 自動セットアップ
# =========================================================
def setup_workspace_on_activation():
"""Nパネルの展開とマテリアルプレビューへの切り替えを自動で行う"""
if not hasattr(bpy.context, 'window_manager') or not bpy.context.window_manager.windows:
return 0.1
try:
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.shading.type = 'MATERIAL'
space.show_region_ui = True
except Exception:
pass
return None
# =========================================================
# 12. 登録と解除
# =========================================================
classes = [
TCS_ARR_PropertyGroup,
OT_create_torus, OT_create_cone, OT_create_sphere, OT_create_arrows,
OT_detach_torus, OT_detach_cone, OT_detach_sphere, OT_detach_arrows,
OT_open_url, OT_remove_addon,
PT_Torus, PT_Cone, PT_Sphere, PT_Arrows, PT_Links, PT_Remove
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.tcs_arr_props = bpy.props.PointerProperty(type=TCS_ARR_PropertyGroup)
if not bpy.app.timers.is_registered(setup_workspace_on_activation):
bpy.app.timers.register(setup_workspace_on_activation, first_interval=0.1)
# --- 修正: より安全な unregister の実装 ---
def unregister():
try:
del bpy.types.Scene.tcs_arr_props
except Exception:
pass
for cls in reversed(classes):
try:
bpy.utils.unregister_class(cls)
except RuntimeError:
pass
if bpy.app.timers.is_registered(setup_workspace_on_activation):
bpy.app.timers.unregister(setup_workspace_on_activation)
if __name__ == "__main__":
register()