Sidebar", "description": "Blender5専用: 自動UI展開・文字化け回避・設定完全書き出し版", "category": "308光円錐" } # ========================================================= # 2. コード作成日時 # ========================================================= # Code Created : 2026_0725_1800 # ========================================================= # 3. import # ========================================================= import bpy import bmesh import math import time import re import os import webbrowser import addon_utils from mathutils import Vector, Euler # ========================================================= # 4. 共通定義 # ========================================================= ADDON_TITLE = "zionad 308 光円錐" PREFIX_VAL = "TCS_ARR_G5" ADDON_CATEGORY = "[ 308 光円錐 ]" PREFIX = PREFIX_VAL.lower() # ========================================================= # 5. リンク定"> Sidebar", "description": "Blender5専用: 自動UI展開・文字化け回避・設定完全書き出し版", "category": "308光円錐" } # ========================================================= # 2. コード作成日時 # ========================================================= # Code Created : 2026_0725_1800 # ========================================================= # 3. import # ========================================================= import bpy import bmesh import math import time import re import os import webbrowser import addon_utils from mathutils import Vector, Euler # ========================================================= # 4. 共通定義 # ========================================================= ADDON_TITLE = "zionad 308 光円錐" PREFIX_VAL = "TCS_ARR_G5" ADDON_CATEGORY = "[ 308 光円錐 ]" PREFIX = PREFIX_VAL.lower() # ========================================================= # 5. リンク定"> Sidebar", "description": "Blender5専用: 自動UI展開・文字化け回避・設定完全書き出し版", "category": "308光円錐" } # ========================================================= # 2. コード作成日時 # ========================================================= # Code Created : 2026_0725_1800 # ========================================================= # 3. import # ========================================================= import bpy import bmesh import math import time import re import os import webbrowser import addon_utils from mathutils import Vector, Euler # ========================================================= # 4. 共通定義 # ========================================================= ADDON_TITLE = "zionad 308 光円錐" PREFIX_VAL = "TCS_ARR_G5" ADDON_CATEGORY = "[ 308 光円錐 ]" PREFIX = PREFIX_VAL.lower() # ========================================================= # 5. リンク定">
bl_info = {
"name": "zionad 308 光円錐",
"author": "zionadchat",
"version": (3, 2, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar",
"description": "Blender5専用: 自動UI展開・文字化け回避・設定完全書き出し版",
"category": "308光円錐"
}
# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0725_1800
# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import time
import re
import os
import webbrowser
import addon_utils
from mathutils import Vector, Euler
# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "zionad 308 光円錐"
PREFIX_VAL = "TCS_ARR_G5"
ADDON_CATEGORY = "[ 308 光円錐 ]"
PREFIX = PREFIX_VAL.lower()
# =========================================================
# 5. リンク定義
# =========================================================
LNK_NM_1 = "308 トーラス等間隔 矢印"
LNK_UR_1 = "<https://app.notion.com/p/308-3a8347b3454a8055b265fd41eea1dcea>"
LNK_NM_2 = "コンテナ builder"
LNK_UR_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"
LNK_NM_3 = "posfie"
LNK_UR_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LNK_NM_4 = "zionadchat"
LNK_UR_4 = "<https://x.com/zionadchat>"
# =========================================================
# 6. 初期設定
# =========================================================
# --- DEFAULTS_BLOCK_START ---
# --- DEFAULTS_BLOCK_START ---
# 数値 (半径・高さ・太さ・本数など)
TR_MJ_RD_DFLT = 3.0
TR_MN_RD_DFLT = 0.20000000298023224
CN_RD_TP_DFLT = 0.0
CN_RD_BT_DFLT = 2.0
CN_HT_DFLT = 4.0
SP_RD_DFLT = 1.5
AW_CNT_DFLT = 8
AW_THK_DFLT = 0.10000000149011612
DM_RD_DFLT = 3.0
DM_THK_DFLT = 0.10000000149011612
# ドームの形状 ( 'HEMI': 半球, 'FULL': 球体 )
DM_MODE_DFLT = 'HEMI'
# XYZ (各オブジェクトの初期位置)
TR_LOC_X_DFLT = 0.0000
TR_LOC_Y_DFLT = 0.0000
TR_LOC_Z_DFLT = 0.0000
CN_LOC_X_DFLT = 5.0000
CN_LOC_Y_DFLT = 0.0000
CN_LOC_Z_DFLT = 0.0000
SP_LOC_X_DFLT = -5.0000
SP_LOC_Y_DFLT = 0.0000
SP_LOC_Z_DFLT = 0.0000
DM_LOC_X_DFLT = 0.0000
DM_LOC_Y_DFLT = 0.0000
DM_LOC_Z_DFLT = 0.0000
# 透明度
ALPHA_FRONT_DFLT = 1.0000
ALPHA_BACK_DFLT = 1.0000
# テキスト設定
TEXT_BODY_DFLT = "Sample Text"
TEXT_SIZE_DFLT = 1.5
TEXT_OFFSET_X_DFLT = 0.0000
TEXT_OFFSET_Y_DFLT = 0.0000
TEXT_OFFSET_Z_DFLT = 3.0000
# 色 (R, G, B, A)
COLOR_TR_DFLT = (0.2000, 0.6000, 1.0000, 1.0000)
COLOR_CN_FR_DFLT = (1.0000, 0.5000, 0.1000, 1.0000)
COLOR_CN_BK_DFLT = (0.1000, 0.5000, 1.0000, 1.0000)
COLOR_SP_DFLT = (0.2000, 0.9000, 0.4000, 1.0000)
COLOR_AW_DFLT = (1.0000, 0.1000, 0.1000, 1.0000)
COLOR_DM_DFLT = (0.5000, 0.3000, 0.8000, 1.0000)
COLOR_TX_DFLT = (1.0000, 1.0000, 1.0000, 1.0000)
# --- DEFAULTS_BLOCK_END ---
# =========================================================
# 7. コアユーティリティ
# =========================================================
def gt_cr_clc(nm_st):
c_dt = bpy.data.collections.get(nm_st)
if not c_dt:
c_dt = bpy.data.collections.new(nm_st)
bpy.context.scene.collection.children.link(c_dt)
return c_dt
def gt_cr_mt(nm_st, cl_tp):
m_dt = bpy.data.materials.get(nm_st)
if not m_dt:
m_dt = bpy.data.materials.new(nm_st)
m_dt.use_nodes = True
try:
m_dt.blend_method = 'BLEND'
m_dt.shadow_method = 'NONE'
except Exception: pass
if m_dt.use_nodes:
bs_nd = m_dt.node_tree.nodes.get("Principled BSDF")
if bs_nd:
if "Base Color" in bs_nd.inputs: bs_nd.inputs["Base Color"].default_value = (cl_tp[0], cl_tp[1], cl_tp[2], 1.0)
if "Alpha" in bs_nd.inputs: bs_nd.inputs["Alpha"].default_value = cl_tp[3]
m_dt.diffuse_color = cl_tp
return m_dt
def gt_cr_mt_dbl(nm_st, c_fr, c_bk):
m_dt = bpy.data.materials.get(nm_st)
if not m_dt:
m_dt = bpy.data.materials.new(nm_st)
m_dt.use_nodes = True
try:
m_dt.blend_method = 'BLEND'
m_dt.shadow_method = 'NONE'
except Exception: pass
if m_dt.use_nodes:
tr_dt = m_dt.node_tree
bs_fr = tr_dt.nodes.get("Pr_Fr")
if not bs_fr:
tr_dt.nodes.clear()
out_nd = tr_dt.nodes.new('ShaderNodeOutputMaterial')
out_nd.location = (400, 0)
mx_nd = tr_dt.nodes.new('ShaderNodeMixShader')
mx_nd.location = (200, 0)
bs_fr = tr_dt.nodes.new('ShaderNodeBsdfPrincipled')
bs_fr.name = "Pr_Fr"
bs_fr.location = (0, 150)
bs_bk = tr_dt.nodes.new('ShaderNodeBsdfPrincipled')
bs_bk.name = "Pr_Bk"
bs_bk.location = (0, -200)
gm_nd = tr_dt.nodes.new('ShaderNodeNewGeometry')
gm_nd.location = (0, 400)
tr_dt.links.new(gm_nd.outputs["Backfacing"], mx_nd.inputs[0])
tr_dt.links.new(bs_fr.outputs["BSDF"], mx_nd.inputs[1])
tr_dt.links.new(bs_bk.outputs["BSDF"], mx_nd.inputs[2])
tr_dt.links.new(mx_nd.outputs["Shader"], out_nd.inputs["Surface"])
bs_fr = tr_dt.nodes.get("Pr_Fr")
bs_bk = tr_dt.nodes.get("Pr_Bk")
if bs_fr:
if "Base Color" in bs_fr.inputs: bs_fr.inputs["Base Color"].default_value = (c_fr[0], c_fr[1], c_fr[2], 1.0)
if "Alpha" in bs_fr.inputs: bs_fr.inputs["Alpha"].default_value = c_fr[3]
if bs_bk:
if "Base Color" in bs_bk.inputs: bs_bk.inputs["Base Color"].default_value = (c_bk[0], c_bk[1], c_bk[2], 1.0)
if "Alpha" in bs_bk.inputs: bs_bk.inputs["Alpha"].default_value = c_bk[3]
m_dt.diffuse_color = c_fr
return m_dt
def aply_dt(ob_nm, dt_fn, m_dt, dt_tp='MESH'):
c_dt = gt_cr_clc(f"{PREFIX_VAL}_Col")
ob_dt = bpy.data.objects.get(ob_nm)
if ob_dt:
od_dt = ob_dt.data
ob_dt.data = dt_fn()
if od_dt.users == 0:
if dt_tp == 'MESH': bpy.data.meshes.remove(od_dt, do_unlink=True)
elif dt_tp == 'CURVE': bpy.data.curves.remove(od_dt, do_unlink=True)
else:
nd_dt = dt_fn()
ob_dt = bpy.data.objects.new(ob_nm, nd_dt)
c_dt.objects.link(ob_dt)
if not ob_dt.data.materials: ob_dt.data.materials.append(m_dt)
else: ob_dt.data.materials[0] = m_dt
return ob_dt
def dtch_ob(ctx, ob_nm, nw_nm, flg_st):
ob_dt = bpy.data.objects.get(ob_nm)
if not ob_dt: return False
ts = time.strftime("%H%M%S")
rn_nm = f"{nw_nm}_{ts}"
ob_dt.name = rn_nm
if ob_dt.data: ob_dt.data.name = f"{rn_nm}_Dt"
for sl in ob_dt.material_slots:
if sl.material and sl.material.name.startswith(PREFIX_VAL):
nw_mt = sl.material.copy()
nw_mt.name = f"{rn_nm}_Mt"
sl.material = nw_mt
m_cl = ctx.scene.collection
if ob_dt.name not in m_cl.objects: m_cl.objects.link(ob_dt)
a_cl = bpy.data.collections.get(f"{PREFIX_VAL}_Col")
if a_cl and ob_dt.name in a_cl.objects: a_cl.objects.unlink(ob_dt)
setattr(ctx.scene.tc_g5_prp, flg_st, False)
return True
# =========================================================
# 8. 更新処理 (Update)
# =========================================================
def upd_tr(sf, cx):
if not sf.b_tr: return
m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Tr", sf.cl_tr)
def mk_tr():
v, f = [], []
S, MS = 48, 16
for i in range(S):
a1 = (i/S)*2*math.pi
c1, s1 = math.cos(a1), math.sin(a1)
for j in range(MS):
a2 = (j/MS)*2*math.pi
c2, s2 = math.cos(a2), math.sin(a2)
v.append(((sf.v_tr_mj + sf.v_tr_mn*c2)*c1, (sf.v_tr_mj + sf.v_tr_mn*c2)*s1, sf.v_tr_mn*s2))
for i in range(S):
for j in range(MS):
i_n, j_n = (i+1)%S, (j+1)%MS
f.append((i*MS+j, i_n*MS+j, i_n*MS+j_n, i*MS+j_n))
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Tr")
me.from_pydata(v, [], f)
me.update()
return me
ob = aply_dt(f"{PREFIX_VAL}_Ob_Tr", mk_tr, m_dt)
ob.location, ob.rotation_euler = sf.lc_tr, sf.rt_tr
if sf.b_aw: upd_aw(sf, cx)
def upd_cn(sf, cx):
if not sf.b_cn: return
m_dt = gt_cr_mt_dbl(f"{PREFIX_VAL}_Mt_Cn", sf.cl_cn_fr, sf.cl_cn_bk)
def mk_cn():
bm = bmesh.new()
ah = max(abs(sf.v_cn_ht), 0.001)
r1, r2, zo = (sf.v_cn_bt, sf.v_cn_tp, ah/2) if sf.v_cn_ht >= 0 else (sf.v_cn_tp, sf.v_cn_bt, -ah/2)
bmesh.ops.create_cone(bm, cap_ends=not sf.b_cn_cp, cap_tris=True, segments=48, radius1=r1, radius2=r2, depth=ah)
bmesh.ops.translate(bm, verts=bm.verts, vec=(0,0,zo))
if sf.b_cn_fl: bmesh.ops.reverse_faces(bm, faces=bm.faces)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Cn")
bm.to_mesh(me)
bm.free()
return me
ob = aply_dt(f"{PREFIX_VAL}_Ob_Cn", mk_cn, m_dt)
ob.location, ob.rotation_euler = sf.lc_cn, sf.rt_cn
def upd_sp(sf, cx):
if not sf.b_sp: return
m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Sp", sf.cl_sp)
def mk_sp():
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=sf.v_sp_rd)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Sp")
bm.to_mesh(me)
bm.free()
return me
ob = aply_dt(f"{PREFIX_VAL}_Ob_Sp", mk_sp, m_dt)
ob.location, ob.rotation_euler = sf.lc_sp, sf.rt_sp
def upd_aw(sf, cx):
if not sf.b_aw: return
m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Aw", sf.cl_aw)
def mk_aw():
bm = bmesh.new()
tl, tm = Vector(sf.lc_tr), Euler(sf.rt_tr).to_matrix()
for i in range(sf.v_aw_cn):
th = i*(2*math.pi/sf.v_aw_cn)
sp = tl + tm @ Vector((sf.v_tr_mj*math.cos(th), sf.v_tr_mj*math.sin(th), 0))
vc = Vector(sf.lc_aw_tg) - sp
ds = vc.length * (sf.v_aw_of/100.0)
if ds < 0.001: continue
hl = min(sf.v_aw_th*4.0, ds*0.5)
sl = ds - hl
rq = Vector((0,0,1)).rotation_difference(vc.normalized())
gs = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12, radius1=sf.v_aw_th, radius2=sf.v_aw_th, depth=sl)
bmesh.ops.translate(bm, verts=gs['verts'], vec=(0,0,sl/2))
gh = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12, radius1=sf.v_aw_th*2.5, radius2=0, depth=hl)
bmesh.ops.translate(bm, verts=gh['verts'], vec=(0,0,sl+hl/2))
va = gs['verts'] + gh['verts']
bmesh.ops.transform(bm, verts=va, matrix=rq.to_matrix().to_4x4())
bmesh.ops.translate(bm, verts=va, vec=sp)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Aw")
bm.to_mesh(me)
bm.free()
return me
ob = aply_dt(f"{PREFIX_VAL}_Ob_Aw", mk_aw, m_dt)
ob.location, ob.rotation_euler = (0,0,0), (0,0,0)
def upd_dm(sf, cx):
if not sf.b_dm: return
m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Dm", sf.cl_dm)
def mk_dm():
cv = bpy.data.curves.new(f"{PREFIX_VAL}_Cv_Dm", 'CURVE')
cv.dimensions, cv.fill_mode = '3D', 'FULL'
cv.bevel_depth, cv.bevel_resolution = sf.v_dm_th, 4
is_fl = (sf.s_dm_md == 'FULL')
ph_mx = math.pi if is_fl else (math.pi / 2)
lt_ed = (sf.v_dm_lt * 2) if is_fl else (sf.v_dm_lt + 1)
for i in range(sf.v_dm_ln):
th = (2*math.pi/sf.v_dm_ln)*i
sl = cv.splines.new('POLY')
sl.points.add(sf.v_dm_sg - 1)
for j in range(sf.v_dm_sg):
ph = ph_mx * (j/(sf.v_dm_sg - 1))
sl.points[j].co = (sf.v_dm_rd*math.sin(ph)*math.cos(th),
sf.v_dm_rd*math.sin(ph)*math.sin(th),
sf.v_dm_rd*math.cos(ph), 1)
for i in range(1, lt_ed):
ph = (math.pi/2)*(i/sf.v_dm_lt)
sl = cv.splines.new('POLY')
sl.points.add(sf.v_dm_sg - 1)
for j in range(sf.v_dm_sg):
th = (2*math.pi)*(j/(sf.v_dm_sg - 1))
sl.points[j].co = (sf.v_dm_rd*math.sin(ph)*math.cos(th),
sf.v_dm_rd*math.sin(ph)*math.sin(th),
sf.v_dm_rd*math.cos(ph), 1)
sl.use_cyclic_u = True
return cv
ob = aply_dt(f"{PREFIX_VAL}_Ob_Dm", mk_dm, m_dt, 'CURVE')
ob.location, ob.rotation_euler = sf.lc_dm, sf.rt_dm
def upd_tx(sf, cx):
if not sf.b_tx: return
m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Tx", sf.cl_tx)
def mk_tx():
cv = bpy.data.curves.new(f"{PREFIX_VAL}_Cv_Tx", 'FONT')
cv.body = sf.s_tx_bd
cv.size = sf.v_tx_sz
cv.extrude = 0.05
return cv
ob = aply_dt(f"{PREFIX_VAL}_Ob_Tx", mk_tx, m_dt, 'CURVE')
ob.location = sf.lc_tx
ob.rotation_euler = sf.rt_tx
# =========================================================
# 9. プロパティ定義
# =========================================================
class TC_G5_Prp(bpy.types.PropertyGroup):
b_tr: bpy.props.BoolProperty(default=False)
lc_tr: bpy.props.FloatVectorProperty(name="Location", default=(TR_LOC_X_DFLT, TR_LOC_Y_DFLT, TR_LOC_Z_DFLT), update=upd_tr)
rt_tr: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_tr)
v_tr_mj: bpy.props.FloatProperty(name="Major Radius", default=TR_MJ_RD_DFLT, update=upd_tr)
v_tr_mn: bpy.props.FloatProperty(name="Minor Radius", default=TR_MN_RD_DFLT, update=upd_tr)
cl_tr: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_TR_DFLT, min=0, max=1, update=upd_tr)
b_cn: bpy.props.BoolProperty(default=False)
lc_cn: bpy.props.FloatVectorProperty(name="Location", default=(CN_LOC_X_DFLT, CN_LOC_Y_DFLT, CN_LOC_Z_DFLT), update=upd_cn)
rt_cn: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_cn)
v_cn_tp: bpy.props.FloatProperty(name="Top Radius", default=CN_RD_TP_DFLT, update=upd_cn)
v_cn_bt: bpy.props.FloatProperty(name="Bot Radius", default=CN_RD_BT_DFLT, update=upd_cn)
v_cn_ht: bpy.props.FloatProperty(name="Height", default=CN_HT_DFLT, update=upd_cn)
b_cn_cp: bpy.props.BoolProperty(name="No Caps", default=False, update=upd_cn)
b_cn_fl: bpy.props.BoolProperty(name="Flip Norm", default=False, update=upd_cn)
cl_cn_fr: bpy.props.FloatVectorProperty(name="Front Col", subtype='COLOR', size=4, default=COLOR_CN_FR_DFLT, min=0, max=1, update=upd_cn)
cl_cn_bk: bpy.props.FloatVectorProperty(name="Back Col", subtype='COLOR', size=4, default=COLOR_CN_BK_DFLT, min=0, max=1, update=upd_cn)
b_sp: bpy.props.BoolProperty(default=False)
lc_sp: bpy.props.FloatVectorProperty(name="Location", default=(SP_LOC_X_DFLT, SP_LOC_Y_DFLT, SP_LOC_Z_DFLT), update=upd_sp)
rt_sp: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_sp)
v_sp_rd: bpy.props.FloatProperty(name="Radius", default=SP_RD_DFLT, update=upd_sp)
cl_sp: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_SP_DFLT, min=0, max=1, update=upd_sp)
b_aw: bpy.props.BoolProperty(default=False)
v_aw_cn: bpy.props.IntProperty(name="Count", default=AW_CNT_DFLT, min=1, max=100, update=upd_aw)
lc_aw_tg: bpy.props.FloatVectorProperty(name="Target Loc", default=(0,0,10), update=upd_aw)
v_aw_of: bpy.props.FloatProperty(name="Offset %", default=100.0, min=1, max=100, update=upd_aw)
v_aw_th: bpy.props.FloatProperty(name="Thickness", default=AW_THK_DFLT, update=upd_aw)
cl_aw: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_AW_DFLT, min=0, max=1, update=upd_aw)
b_dm: bpy.props.BoolProperty(default=False)
s_dm_md: bpy.props.EnumProperty(
items=[('HEMI', "半球 (Dome)", ""), ('FULL', "球体 (Sphere)", "")],
default=DM_MODE_DFLT, update=upd_dm
)
lc_dm: bpy.props.FloatVectorProperty(name="Location", default=(DM_LOC_X_DFLT, DM_LOC_Y_DFLT, DM_LOC_Z_DFLT), update=upd_dm)
rt_dm: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_dm)
v_dm_rd: bpy.props.FloatProperty(name="Radius", default=DM_RD_DFLT, update=upd_dm)
v_dm_th: bpy.props.FloatProperty(name="Thickness", default=DM_THK_DFLT, update=upd_dm)
v_dm_ln: bpy.props.IntProperty(name="Longt Count", default=12, update=upd_dm)
v_dm_lt: bpy.props.IntProperty(name="Lat Count (per Hemi)", default=6, update=upd_dm)
v_dm_sg: bpy.props.IntProperty(name="Segments", default=32, update=upd_dm)
cl_dm: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_DM_DFLT, min=0, max=1, update=upd_dm)
b_tx: bpy.props.BoolProperty(default=False)
s_tx_bd: bpy.props.StringProperty(name="Text", default=TEXT_BODY_DFLT, update=upd_tx)
v_tx_sz: bpy.props.FloatProperty(name="Size", default=TEXT_SIZE_DFLT, update=upd_tx)
lc_tx: bpy.props.FloatVectorProperty(name="Location", default=(TEXT_OFFSET_X_DFLT, TEXT_OFFSET_Y_DFLT, TEXT_OFFSET_Z_DFLT), update=upd_tx)
rt_tx: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(1.5708,0,0), update=upd_tx)
cl_tx: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_TX_DFLT, min=0, max=1, update=upd_tx)
# =========================================================
# 10. オペレーター群
# =========================================================
class Op_Cr_Tr(bpy.types.Operator):
bl_idname = f"{PREFIX}.cr_tr"
bl_label = "トーラス生成"
def execute(self, cx):
cx.scene.tc_g5_prp.b_tr = True
upd_tr(cx.scene.tc_g5_prp, cx)
return {'FINISHED'}
class Op_Cr_Cn(bpy.types.Operator):
bl_idname = f"{PREFIX}.cr_cn"
bl_label = "円錐台生成"
def execute(self, cx):
cx.scene.tc_g5_prp.b_cn = True
upd_cn(cx.scene.tc_g5_prp, cx)
return {'FINISHED'}
class Op_Cr_Sp(bpy.types.Operator):
bl_idname = f"{PREFIX}.cr_sp"
bl_label = "球体生成"
def execute(self, cx):
cx.scene.tc_g5_prp.b_sp = True
upd_sp(cx.scene.tc_g5_prp, cx)
return {'FINISHED'}
class Op_Cr_Aw(bpy.types.Operator):
bl_idname = f"{PREFIX}.cr_aw"
bl_label = "集中矢印生成"
def execute(self, cx):
cx.scene.tc_g5_prp.b_aw = True
upd_aw(cx.scene.tc_g5_prp, cx)
return {'FINISHED'}
class Op_Cr_Dm(bpy.types.Operator):
bl_idname = f"{PREFIX}.cr_dm"
bl_label = "骨格生成"
def execute(self, cx):
cx.scene.tc_g5_prp.b_dm = True
upd_dm(cx.scene.tc_g5_prp, cx)
return {'FINISHED'}
class Op_Cr_Tx(bpy.types.Operator):
bl_idname = f"{PREFIX}.cr_tx"
bl_label = "テキスト生成"
def execute(self, cx):
cx.scene.tc_g5_prp.b_tx = True
upd_tx(cx.scene.tc_g5_prp, cx)
return {'FINISHED'}
class Op_Dt_Tr(bpy.types.Operator):
bl_idname = f"{PREFIX}.dt_tr"
bl_label = "独立"
def execute(self, cx):
dtch_ob(cx, f"{PREFIX_VAL}_Ob_Tr", "Detached_Torus", "b_tr")
return {'FINISHED'}
class Op_Dt_Cn(bpy.types.Operator):
bl_idname = f"{PREFIX}.dt_cn"
bl_label = "独立"
def execute(self, cx):
dtch_ob(cx, f"{PREFIX_VAL}_Ob_Cn", "Detached_Cone", "b_cn")
return {'FINISHED'}
class Op_Dt_Sp(bpy.types.Operator):
bl_idname = f"{PREFIX}.dt_sp"
bl_label = "独立"
def execute(self, cx):
dtch_ob(cx, f"{PREFIX_VAL}_Ob_Sp", "Detached_Sphere", "b_sp")
return {'FINISHED'}
class Op_Dt_Aw(bpy.types.Operator):
bl_idname = f"{PREFIX}.dt_aw"
bl_label = "独立"
def execute(self, cx):
dtch_ob(cx, f"{PREFIX_VAL}_Ob_Aw", "Detached_Arrows", "b_aw")
return {'FINISHED'}
class Op_Dt_Dm(bpy.types.Operator):
bl_idname = f"{PREFIX}.dt_dm"
bl_label = "独立"
def execute(self, cx):
dtch_ob(cx, f"{PREFIX_VAL}_Ob_Dm", "Detached_Bone", "b_dm")
return {'FINISHED'}
class Op_Dt_Tx(bpy.types.Operator):
bl_idname = f"{PREFIX}.dt_tx"
bl_label = "独立"
def execute(self, cx):
dtch_ob(cx, f"{PREFIX_VAL}_Ob_Tx", "Detached_Text", "b_tx")
return {'FINISHED'}
class Op_Op_Ur(bpy.types.Operator):
bl_idname = f"{PREFIX}.op_ur"
bl_label = "Open URL"
url: bpy.props.StringProperty()
def execute(self, cx):
webbrowser.open(self.url)
return {'FINISHED'}
def dly_unrg():
try: addon_utils.disable(__name__)
except Exception: pass
try: unregister()
except Exception: pass
return None
class Op_Rm_Ad(bpy.types.Operator):
bl_idname = f"{PREFIX}.rm_ad"
bl_label = "Clean up & Uninstall"
def execute(self, cx):
c_dt = bpy.data.collections.get(f"{PREFIX_VAL}_Col")
if c_dt:
for ob in list(c_dt.objects): bpy.data.objects.remove(ob, do_unlink=True)
bpy.data.collections.remove(c_dt)
for m in list(bpy.data.meshes):
if m.name.startswith(PREFIX_VAL): bpy.data.meshes.remove(m, do_unlink=True)
for c in list(bpy.data.curves):
if c.name.startswith(PREFIX_VAL): bpy.data.curves.remove(c, do_unlink=True)
for mt in list(bpy.data.materials):
if mt.name.startswith(PREFIX_VAL): bpy.data.materials.remove(mt, do_unlink=True)
bpy.app.timers.register(dly_unrg, first_interval=0.2)
return {'FINISHED'}
class Op_Xp_Tx(bpy.types.Operator):
bl_idname = f"{PREFIX}.xp_tx"
bl_label = "設定をコードに書き出し"
def execute(self, cx):
if not hasattr(cx.scene, "tc_g5_prp"): return {'CANCELLED'}
p = cx.scene.tc_g5_prp
ss = ""
fp = globals().get("__file__")
if fp and fp.endswith(".pyc"): fp = fp[:-1]
if fp and os.path.exists(fp):
try:
with open(fp, "r", encoding="utf-8") as f: ss = f.read()
except Exception: pass
if not ss:
for tx in bpy.data.texts:
bd = tx.as_string()
if ("# " + "--- DEFAULTS_BLOCK_START ---") in bd:
ss = bd
break
if not ss: return {'CANCELLED'}
# 初期設定ブロックと「完全に同じ形式」で出力文字列を構築
ds = "# " + "--- DEFAULTS_BLOCK_START ---\n\n"
ds += "# 数値 (半径・高さ・太さ・本数など)\n"
ds += f"TR_MJ_RD_DFLT = {p.v_tr_mj}\n"
ds += f"TR_MN_RD_DFLT = {p.v_tr_mn}\n"
ds += f"CN_RD_TP_DFLT = {p.v_cn_tp}\n"
ds += f"CN_RD_BT_DFLT = {p.v_cn_bt}\n"
ds += f"CN_HT_DFLT = {p.v_cn_ht}\n"
ds += f"SP_RD_DFLT = {p.v_sp_rd}\n"
ds += f"AW_CNT_DFLT = {p.v_aw_cn}\n"
ds += f"AW_THK_DFLT = {p.v_aw_th}\n"
ds += f"DM_RD_DFLT = {p.v_dm_rd}\n"
ds += f"DM_THK_DFLT = {p.v_dm_th}\n\n"
ds += "# ドームの形状 ( 'HEMI': 半球, 'FULL': 球体 )\n"
ds += f"DM_MODE_DFLT = '{p.s_dm_md}'\n\n"
ds += "# XYZ (各オブジェクトの初期位置)\n"
ds += f"TR_LOC_X_DFLT = {p.lc_tr[0]:.4f}\n"
ds += f"TR_LOC_Y_DFLT = {p.lc_tr[1]:.4f}\n"
ds += f"TR_LOC_Z_DFLT = {p.lc_tr[2]:.4f}\n\n"
ds += f"CN_LOC_X_DFLT = {p.lc_cn[0]:.4f}\n"
ds += f"CN_LOC_Y_DFLT = {p.lc_cn[1]:.4f}\n"
ds += f"CN_LOC_Z_DFLT = {p.lc_cn[2]:.4f}\n\n"
ds += f"SP_LOC_X_DFLT = {p.lc_sp[0]:.4f}\n"
ds += f"SP_LOC_Y_DFLT = {p.lc_sp[1]:.4f}\n"
ds += f"SP_LOC_Z_DFLT = {p.lc_sp[2]:.4f}\n\n"
ds += f"DM_LOC_X_DFLT = {p.lc_dm[0]:.4f}\n"
ds += f"DM_LOC_Y_DFLT = {p.lc_dm[1]:.4f}\n"
ds += f"DM_LOC_Z_DFLT = {p.lc_dm[2]:.4f}\n\n"
ds += "# 透明度\n"
ds += f"ALPHA_FRONT_DFLT = {p.cl_cn_fr[3]:.4f}\n"
ds += f"ALPHA_BACK_DFLT = {p.cl_cn_bk[3]:.4f}\n\n"
ds += "# テキスト設定\n"
ds += f"TEXT_BODY_DFLT = \"{p.s_tx_bd}\"\n"
ds += f"TEXT_SIZE_DFLT = {p.v_tx_sz}\n"
ds += f"TEXT_OFFSET_X_DFLT = {p.lc_tx[0]:.4f}\n"
ds += f"TEXT_OFFSET_Y_DFLT = {p.lc_tx[1]:.4f}\n"
ds += f"TEXT_OFFSET_Z_DFLT = {p.lc_tx[2]:.4f}\n\n"
ds += "# 色 (R, G, B, A)\n"
ds += f"COLOR_TR_DFLT = ({p.cl_tr[0]:.4f}, {p.cl_tr[1]:.4f}, {p.cl_tr[2]:.4f}, {p.cl_tr[3]:.4f})\n"
ds += f"COLOR_CN_FR_DFLT = ({p.cl_cn_fr[0]:.4f}, {p.cl_cn_fr[1]:.4f}, {p.cl_cn_fr[2]:.4f}, {p.cl_cn_fr[3]:.4f})\n"
ds += f"COLOR_CN_BK_DFLT = ({p.cl_cn_bk[0]:.4f}, {p.cl_cn_bk[1]:.4f}, {p.cl_cn_bk[2]:.4f}, {p.cl_cn_bk[3]:.4f})\n"
ds += f"COLOR_SP_DFLT = ({p.cl_sp[0]:.4f}, {p.cl_sp[1]:.4f}, {p.cl_sp[2]:.4f}, {p.cl_sp[3]:.4f})\n"
ds += f"COLOR_AW_DFLT = ({p.cl_aw[0]:.4f}, {p.cl_aw[1]:.4f}, {p.cl_aw[2]:.4f}, {p.cl_aw[3]:.4f})\n"
ds += f"COLOR_DM_DFLT = ({p.cl_dm[0]:.4f}, {p.cl_dm[1]:.4f}, {p.cl_dm[2]:.4f}, {p.cl_dm[3]:.4f})\n"
ds += f"COLOR_TX_DFLT = ({p.cl_tx[0]:.4f}, {p.cl_tx[1]:.4f}, {p.cl_tx[2]:.4f}, {p.cl_tx[3]:.4f})\n\n"
m1 = "# " + "--- DEFAULTS_BLOCK_START ---"
m2 = "# " + "--- DEFAULTS_BLOCK_END ---"
pr = r"(" + m1 + r"\n)(.*?)(" + m2 + r")"
ns = re.sub(pr, r"\g<1>\n" + ds + r"\g<3>", ss, flags=re.DOTALL)
tn = "Exp_TCS_G5.py"
nb = bpy.data.texts.get(tn)
if nb: nb.clear()
else: nb = bpy.data.texts.new(tn)
nb.write(ns)
sf = False
for a in cx.screen.areas:
if a.type == 'TEXT_EDITOR':
a.spaces.active.text = nb
sf = True
break
if sf: self.report({'INFO'}, f"書き出し成功: {tn}")
else: self.report({'INFO'}, f"内部データに保存: {tn}")
return {'FINISHED'}
# =========================================================
# 11. UIパネル
# =========================================================
class Pn_Bs(bpy.types.Panel):
bl_space_type, bl_region_type, bl_category = 'VIEW_3D', 'UI', ADDON_CATEGORY
class Pn_Tr(Pn_Bs):
bl_label, bl_idname, bl_order = "1. トーラス", f"{PREFIX_VAL}_P_Tr", 1
def draw(self, cx):
ly, p = self.layout, cx.scene.tc_g5_prp
c = ly.column(align=True)
c.prop(p, "lc_tr"); c.prop(p, "rt_tr"); c.separator()
c.prop(p, "v_tr_mj"); c.prop(p, "v_tr_mn"); c.prop(p, "cl_tr"); ly.separator()
r = ly.row(align=True)
if not p.b_tr: r.operator(Op_Cr_Tr.bl_idname, icon='MESH_TORUS')
else:
r.operator(Op_Cr_Tr.bl_idname, text="更新", icon='FILE_REFRESH')
r.operator(Op_Dt_Tr.bl_idname, icon='UNLINKED')
class Pn_Cn(Pn_Bs):
bl_label, bl_idname, bl_order = "2. 円錐台", f"{PREFIX_VAL}_P_Cn", 2
def draw(self, cx):
ly, p = self.layout, cx.scene.tc_g5_prp
c = ly.column(align=True)
c.prop(p, "lc_cn"); c.prop(p, "rt_cn"); c.separator()
r1 = c.row(align=True); r1.prop(p, "v_cn_tp"); r1.prop(p, "v_cn_bt")
c.prop(p, "v_cn_ht"); c.separator()
r2 = c.row(align=True); r2.prop(p, "b_cn_cp"); r2.prop(p, "b_cn_fl"); c.separator()
r3 = c.row(align=True); r3.prop(p, "cl_cn_fr", text="表面"); r3.prop(p, "cl_cn_bk", text="裏面"); ly.separator()
r = ly.row(align=True)
if not p.b_cn: r.operator(Op_Cr_Cn.bl_idname, icon='MESH_CONE')
else:
r.operator(Op_Cr_Cn.bl_idname, text="更新", icon='FILE_REFRESH')
r.operator(Op_Dt_Cn.bl_idname, icon='UNLINKED')
class Pn_Sp(Pn_Bs):
bl_label, bl_idname, bl_order = "3. 球体", f"{PREFIX_VAL}_P_Sp", 3
def draw(self, cx):
ly, p = self.layout, cx.scene.tc_g5_prp
c = ly.column(align=True)
c.prop(p, "lc_sp"); c.prop(p, "rt_sp"); c.separator()
c.prop(p, "v_sp_rd"); c.prop(p, "cl_sp"); ly.separator()
r = ly.row(align=True)
if not p.b_sp: r.operator(Op_Cr_Sp.bl_idname, icon='MESH_UVSPHERE')
else:
r.operator(Op_Cr_Sp.bl_idname, text="更新", icon='FILE_REFRESH')
r.operator(Op_Dt_Sp.bl_idname, icon='UNLINKED')
class Pn_Aw(Pn_Bs):
bl_label, bl_idname, bl_order = "4. 集中矢印", f"{PREFIX_VAL}_P_Aw", 4
def draw(self, cx):
ly, p = self.layout, cx.scene.tc_g5_prp
c = ly.column(align=True)
c.prop(p, "v_aw_cn"); c.prop(p, "lc_aw_tg"); c.prop(p, "v_aw_of"); c.separator()
c.prop(p, "v_aw_th"); c.prop(p, "cl_aw"); ly.separator()
r = ly.row(align=True)
if not p.b_aw: r.operator(Op_Cr_Aw.bl_idname, icon='TRACKING')
else:
r.operator(Op_Cr_Aw.bl_idname, text="更新", icon='FILE_REFRESH')
r.operator(Op_Dt_Aw.bl_idname, icon='UNLINKED')
class Pn_Dm(Pn_Bs):
bl_label, bl_idname, bl_order = "5. 骨格形状", f"{PREFIX_VAL}_P_Dm", 5
def draw(self, cx):
ly, p = self.layout, cx.scene.tc_g5_prp
c = ly.column(align=True)
c.prop(p, "s_dm_md", text="形状")
c.separator()
c.prop(p, "lc_dm"); c.prop(p, "rt_dm"); c.separator()
c.prop(p, "v_dm_rd"); c.prop(p, "v_dm_th"); c.separator()
r1 = c.row(align=True); r1.prop(p, "v_dm_ln"); r1.prop(p, "v_dm_lt")
c.prop(p, "v_dm_sg"); c.separator(); c.prop(p, "cl_dm"); ly.separator()
r = ly.row(align=True)
if not p.b_dm: r.operator(Op_Cr_Dm.bl_idname, icon='MESH_UVSPHERE')
else:
r.operator(Op_Cr_Dm.bl_idname, text="更新", icon='FILE_REFRESH')
r.operator(Op_Dt_Dm.bl_idname, icon='UNLINKED')
class Pn_Tx(Pn_Bs):
bl_label, bl_idname, bl_order = "6. 3Dテキスト", f"{PREFIX_VAL}_P_Tx", 6
def draw(self, cx):
ly, p = self.layout, cx.scene.tc_g5_prp
c = ly.column(align=True)
c.prop(p, "s_tx_bd"); c.prop(p, "v_tx_sz"); c.separator()
c.prop(p, "lc_tx"); c.prop(p, "rt_tx"); c.separator(); c.prop(p, "cl_tx"); ly.separator()
r = ly.row(align=True)
if not p.b_tx: r.operator(Op_Cr_Tx.bl_idname, icon='FONT_DATA')
else:
r.operator(Op_Cr_Tx.bl_idname, text="更新", icon='FILE_REFRESH')
r.operator(Op_Dt_Tx.bl_idname, icon='UNLINKED')
class Pn_Lnk(Pn_Bs):
bl_label, bl_idname, bl_order = "Links", f"{PREFIX_VAL}_P_Lnk", 10
def draw(self, cx):
b = self.layout.box()
if LNK_NM_1: b.operator(Op_Op_Ur.bl_idname, text=LNK_NM_1, icon='URL').url = LNK_UR_1
if LNK_NM_2: b.operator(Op_Op_Ur.bl_idname, text=LNK_NM_2, icon='URL').url = LNK_UR_2
if LNK_NM_3: b.operator(Op_Op_Ur.bl_idname, text=LNK_NM_3, icon='URL').url = LNK_UR_3
if LNK_NM_4: b.operator(Op_Op_Ur.bl_idname, text=LNK_NM_4, icon='URL').url = LNK_UR_4
class Pn_Rm(Pn_Bs):
bl_label, bl_idname, bl_order, bl_options = "管理 & エクスポート", f"{PREFIX_VAL}_P_Rm", 11, {'DEFAULT_CLOSED'}
def draw(self, cx):
bx = self.layout.box()
bx.operator(Op_Xp_Tx.bl_idname, icon='TEXT')
bx.separator()
bx.operator(Op_Rm_Ad.bl_idname, icon='CANCEL')
# =========================================================
# 12. 起動時自動展開処理
# =========================================================
def ui_st_up():
try:
cx = bpy.context
if not cx or not cx.window_manager.windows: return 0.2
for w in cx.window_manager.windows:
for a in w.screen.areas:
if a.type == 'VIEW_3D':
for s in a.spaces:
if s.type == 'VIEW_3D':
s.shading.type = 'MATERIAL'
s.show_region_ui = True
except Exception: pass
return None
cls_ls = [
TC_G5_Prp, Op_Cr_Tr, Op_Cr_Cn, Op_Cr_Sp, Op_Cr_Aw, Op_Cr_Dm, Op_Cr_Tx, Op_Op_Ur,
Op_Dt_Tr, Op_Dt_Cn, Op_Dt_Sp, Op_Dt_Aw, Op_Dt_Dm, Op_Dt_Tx,
Op_Rm_Ad, Op_Xp_Tx,
Pn_Tr, Pn_Cn, Pn_Sp, Pn_Aw, Pn_Dm, Pn_Tx, Pn_Lnk, Pn_Rm
]
def register():
for c in cls_ls: bpy.utils.register_class(c)
bpy.types.Scene.tc_g5_prp = bpy.props.PointerProperty(type=TC_G5_Prp)
if not bpy.app.timers.is_registered(ui_st_up): bpy.app.timers.register(ui_st_up, first_interval=0.2)
def unregister():
try: del bpy.types.Scene.tc_g5_prp
except Exception: pass
for c in reversed(cls_ls):
try: bpy.utils.unregister_class(c)
except RuntimeError: pass
if bpy.app.timers.is_registered(ui_st_up): bpy.app.timers.unregister(ui_st_up)
if __name__ == "__main__": register()
# =========================================================
# 1. bl_info
# =========================================================
bl_info = {
"name": "zionad 308 光円錐",
"author": "zionadchat",
"version": (3, 1, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar",
"description": "Blender5専用: 自動UI展開・文字化け回避・半球/全球切替対応版",
"category": "308光円錐"
}
# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0725_1730
# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import time
import re
import os
import webbrowser
import addon_utils
from mathutils import Vector, Euler
# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "zionad 308 光円錐"
PREFIX_VAL = "TCS_ARR_G5"
ADDON_CATEGORY = "308光円錐"
PREFIX = PREFIX_VAL.lower()
# =========================================================
# 5. リンク定義
# =========================================================
LNK_NM_1 = "308 トーラス等間隔 矢印"
LNK_UR_1 = "<https://app.notion.com/p/308-3a8347b3454a8055b265fd41eea1dcea>"
LNK_NM_2 = "コンテナ builder"
LNK_UR_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"
LNK_NM_3 = "posfie"
LNK_UR_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LNK_NM_4 = "zionadchat"
LNK_UR_4 = "<https://x.com/zionadchat>"
# =========================================================
# 6. 初期設定
# =========================================================
# --- DEFAULTS_BLOCK_START ---
# 数値 (半径・高さ・太さ・本数など)
TR_MJ_RD_DFLT = 3.0
TR_MN_RD_DFLT = 0.2
CN_RD_TP_DFLT = 0.0
CN_RD_BT_DFLT = 2.0
CN_HT_DFLT = 4.0
SP_RD_DFLT = 1.5
AW_CNT_DFLT = 8
AW_THK_DFLT = 0.1
DM_RD_DFLT = 3.0
DM_THK_DFLT = 0.1
# ドームの形状 ( 'HEMI': 半球, 'FULL': 球体 )
DM_MODE_DFLT = 'HEMI'
# XYZ (各オブジェクトの初期位置)
TR_LOC_X_DFLT = 0.0
TR_LOC_Y_DFLT = 0.0
TR_LOC_Z_DFLT = 0.0
CN_LOC_X_DFLT = 5.0
CN_LOC_Y_DFLT = 0.0
CN_LOC_Z_DFLT = 0.0
SP_LOC_X_DFLT = -5.0
SP_LOC_Y_DFLT = 0.0
SP_LOC_Z_DFLT = 0.0
DM_LOC_X_DFLT = 0.0
DM_LOC_Y_DFLT = 5.0
DM_LOC_Z_DFLT = 0.0
# 透明度
ALPHA_FRONT_DFLT = 1.0
ALPHA_BACK_DFLT = 1.0
# テキスト設定
TEXT_BODY_DFLT = "Sample Text"
TEXT_SIZE_DFLT = 1.5
TEXT_OFFSET_X_DFLT = 0.0
TEXT_OFFSET_Y_DFLT = 0.0
TEXT_OFFSET_Z_DFLT = 3.0
# 色 (R, G, B, A)
COLOR_TR_DFLT = (0.2000, 0.6000, 1.0000, 1.0000)
COLOR_CN_FR_DFLT = (1.0000, 0.5000, 0.1000, 1.0000)
COLOR_CN_BK_DFLT = (0.1000, 0.5000, 1.0000, 1.0000)
COLOR_SP_DFLT = (0.2000, 0.9000, 0.4000, 1.0000)
COLOR_AW_DFLT = (1.0000, 0.1000, 0.1000, 1.0000)
COLOR_DM_DFLT = (0.5000, 0.3000, 0.8000, 1.0000)
COLOR_TX_DFLT = (1.0000, 1.0000, 1.0000, 1.0000)
# --- DEFAULTS_BLOCK_END ---
# =========================================================
# 7. コアユーティリティ (文字化け回避対応済)
# =========================================================
def gt_cr_clc(nm_st):
c_dt = bpy.data.collections.get(nm_st)
if not c_dt:
c_dt = bpy.data.collections.new(nm_st)
bpy.context.scene.collection.children.link(c_dt)
return c_dt
def gt_cr_mt(nm_st, cl_tp):
m_dt = bpy.data.materials.get(nm_st)
if not m_dt:
m_dt = bpy.data.materials.new(nm_st)
m_dt.use_nodes = True
try:
m_dt.blend_method = 'BLEND'
m_dt.shadow_method = 'NONE'
except Exception: pass
if m_dt.use_nodes:
bs_nd = m_dt.node_tree.nodes.get("Principled BSDF")
if bs_nd:
if "Base Color" in bs_nd.inputs: bs_nd.inputs["Base Color"].default_value = (cl_tp[0], cl_tp[1], cl_tp[2], 1.0)
if "Alpha" in bs_nd.inputs: bs_nd.inputs["Alpha"].default_value = cl_tp[3]
m_dt.diffuse_color = cl_tp
return m_dt
def gt_cr_mt_dbl(nm_st, c_fr, c_bk):
m_dt = bpy.data.materials.get(nm_st)
if not m_dt:
m_dt = bpy.data.materials.new(nm_st)
m_dt.use_nodes = True
try:
m_dt.blend_method = 'BLEND'
m_dt.shadow_method = 'NONE'
except Exception: pass
if m_dt.use_nodes:
tr_dt = m_dt.node_tree
bs_fr = tr_dt.nodes.get("Pr_Fr")
if not bs_fr:
tr_dt.nodes.clear()
out_nd = tr_dt.nodes.new('ShaderNodeOutputMaterial')
out_nd.location = (400, 0)
mx_nd = tr_dt.nodes.new('ShaderNodeMixShader')
mx_nd.location = (200, 0)
bs_fr = tr_dt.nodes.new('ShaderNodeBsdfPrincipled')
bs_fr.name = "Pr_Fr"
bs_fr.location = (0, 150)
bs_bk = tr_dt.nodes.new('ShaderNodeBsdfPrincipled')
bs_bk.name = "Pr_Bk"
bs_bk.location = (0, -200)
gm_nd = tr_dt.nodes.new('ShaderNodeNewGeometry')
gm_nd.location = (0, 400)
tr_dt.links.new(gm_nd.outputs["Backfacing"], mx_nd.inputs[0])
tr_dt.links.new(bs_fr.outputs["BSDF"], mx_nd.inputs[1])
tr_dt.links.new(bs_bk.outputs["BSDF"], mx_nd.inputs[2])
tr_dt.links.new(mx_nd.outputs["Shader"], out_nd.inputs["Surface"])
bs_fr = tr_dt.nodes.get("Pr_Fr")
bs_bk = tr_dt.nodes.get("Pr_Bk")
if bs_fr:
if "Base Color" in bs_fr.inputs: bs_fr.inputs["Base Color"].default_value = (c_fr[0], c_fr[1], c_fr[2], 1.0)
if "Alpha" in bs_fr.inputs: bs_fr.inputs["Alpha"].default_value = c_fr[3]
if bs_bk:
if "Base Color" in bs_bk.inputs: bs_bk.inputs["Base Color"].default_value = (c_bk[0], c_bk[1], c_bk[2], 1.0)
if "Alpha" in bs_bk.inputs: bs_bk.inputs["Alpha"].default_value = c_bk[3]
m_dt.diffuse_color = c_fr
return m_dt
def aply_dt(ob_nm, dt_fn, m_dt, dt_tp='MESH'):
c_dt = gt_cr_clc(f"{PREFIX_VAL}_Col")
ob_dt = bpy.data.objects.get(ob_nm)
if ob_dt:
od_dt = ob_dt.data
ob_dt.data = dt_fn()
if od_dt.users == 0:
if dt_tp == 'MESH': bpy.data.meshes.remove(od_dt, do_unlink=True)
elif dt_tp == 'CURVE': bpy.data.curves.remove(od_dt, do_unlink=True)
else:
nd_dt = dt_fn()
ob_dt = bpy.data.objects.new(ob_nm, nd_dt)
c_dt.objects.link(ob_dt)
if not ob_dt.data.materials: ob_dt.data.materials.append(m_dt)
else: ob_dt.data.materials[0] = m_dt
return ob_dt
def dtch_ob(ctx, ob_nm, nw_nm, flg_st):
ob_dt = bpy.data.objects.get(ob_nm)
if not ob_dt: return False
ts = time.strftime("%H%M%S")
rn_nm = f"{nw_nm}_{ts}"
ob_dt.name = rn_nm
if ob_dt.data: ob_dt.data.name = f"{rn_nm}_Dt"
for sl in ob_dt.material_slots:
if sl.material and sl.material.name.startswith(PREFIX_VAL):
nw_mt = sl.material.copy()
nw_mt.name = f"{rn_nm}_Mt"
sl.material = nw_mt
m_cl = ctx.scene.collection
if ob_dt.name not in m_cl.objects: m_cl.objects.link(ob_dt)
a_cl = bpy.data.collections.get(f"{PREFIX_VAL}_Col")
if a_cl and ob_dt.name in a_cl.objects: a_cl.objects.unlink(ob_dt)
setattr(ctx.scene.tc_g5_prp, flg_st, False)
return True
# =========================================================
# 8. 更新処理 (Update)
# =========================================================
def upd_tr(sf, cx):
if not sf.b_tr: return
m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Tr", sf.cl_tr)
def mk_tr():
v, f = [], []
S, MS = 48, 16
for i in range(S):
a1 = (i/S)*2*math.pi
c1, s1 = math.cos(a1), math.sin(a1)
for j in range(MS):
a2 = (j/MS)*2*math.pi
c2, s2 = math.cos(a2), math.sin(a2)
v.append(((sf.v_tr_mj + sf.v_tr_mn*c2)*c1, (sf.v_tr_mj + sf.v_tr_mn*c2)*s1, sf.v_tr_mn*s2))
for i in range(S):
for j in range(MS):
i_n, j_n = (i+1)%S, (j+1)%MS
f.append((i*MS+j, i_n*MS+j, i_n*MS+j_n, i*MS+j_n))
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Tr")
me.from_pydata(v, [], f)
me.update()
return me
ob = aply_dt(f"{PREFIX_VAL}_Ob_Tr", mk_tr, m_dt)
ob.location, ob.rotation_euler = sf.lc_tr, sf.rt_tr
if sf.b_aw: upd_aw(sf, cx)
def upd_cn(sf, cx):
if not sf.b_cn: return
m_dt = gt_cr_mt_dbl(f"{PREFIX_VAL}_Mt_Cn", sf.cl_cn_fr, sf.cl_cn_bk)
def mk_cn():
bm = bmesh.new()
ah = max(abs(sf.v_cn_ht), 0.001)
r1, r2, zo = (sf.v_cn_bt, sf.v_cn_tp, ah/2) if sf.v_cn_ht >= 0 else (sf.v_cn_tp, sf.v_cn_bt, -ah/2)
bmesh.ops.create_cone(bm, cap_ends=not sf.b_cn_cp, cap_tris=True, segments=48, radius1=r1, radius2=r2, depth=ah)
bmesh.ops.translate(bm, verts=bm.verts, vec=(0,0,zo))
if sf.b_cn_fl: bmesh.ops.reverse_faces(bm, faces=bm.faces)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Cn")
bm.to_mesh(me)
bm.free()
return me
ob = aply_dt(f"{PREFIX_VAL}_Ob_Cn", mk_cn, m_dt)
ob.location, ob.rotation_euler = sf.lc_cn, sf.rt_cn
def upd_sp(sf, cx):
if not sf.b_sp: return
m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Sp", sf.cl_sp)
def mk_sp():
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=sf.v_sp_rd)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Sp")
bm.to_mesh(me)
bm.free()
return me
ob = aply_dt(f"{PREFIX_VAL}_Ob_Sp", mk_sp, m_dt)
ob.location, ob.rotation_euler = sf.lc_sp, sf.rt_sp
def upd_aw(sf, cx):
if not sf.b_aw: return
m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Aw", sf.cl_aw)
def mk_aw():
bm = bmesh.new()
tl, tm = Vector(sf.lc_tr), Euler(sf.rt_tr).to_matrix()
for i in range(sf.v_aw_cn):
th = i*(2*math.pi/sf.v_aw_cn)
sp = tl + tm @ Vector((sf.v_tr_mj*math.cos(th), sf.v_tr_mj*math.sin(th), 0))
vc = Vector(sf.lc_aw_tg) - sp
ds = vc.length * (sf.v_aw_of/100.0)
if ds < 0.001: continue
hl = min(sf.v_aw_th*4.0, ds*0.5)
sl = ds - hl
rq = Vector((0,0,1)).rotation_difference(vc.normalized())
gs = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12, radius1=sf.v_aw_th, radius2=sf.v_aw_th, depth=sl)
bmesh.ops.translate(bm, verts=gs['verts'], vec=(0,0,sl/2))
gh = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12, radius1=sf.v_aw_th*2.5, radius2=0, depth=hl)
bmesh.ops.translate(bm, verts=gh['verts'], vec=(0,0,sl+hl/2))
va = gs['verts'] + gh['verts']
bmesh.ops.transform(bm, verts=va, matrix=rq.to_matrix().to_4x4())
bmesh.ops.translate(bm, verts=va, vec=sp)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Aw")
bm.to_mesh(me)
bm.free()
return me
ob = aply_dt(f"{PREFIX_VAL}_Ob_Aw", mk_aw, m_dt)
ob.location, ob.rotation_euler = (0,0,0), (0,0,0)
def upd_dm(sf, cx):
if not sf.b_dm: return
m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Dm", sf.cl_dm)
def mk_dm():
cv = bpy.data.curves.new(f"{PREFIX_VAL}_Cv_Dm", 'CURVE')
cv.dimensions, cv.fill_mode = '3D', 'FULL'
cv.bevel_depth, cv.bevel_resolution = sf.v_dm_th, 4
is_fl = (sf.s_dm_md == 'FULL')
ph_mx = math.pi if is_fl else (math.pi / 2)
lt_ed = (sf.v_dm_lt * 2) if is_fl else (sf.v_dm_lt + 1)
# 経度 (縦線)
for i in range(sf.v_dm_ln):
th = (2*math.pi/sf.v_dm_ln)*i
sl = cv.splines.new('POLY')
sl.points.add(sf.v_dm_sg - 1)
for j in range(sf.v_dm_sg):
ph = ph_mx * (j/(sf.v_dm_sg - 1))
sl.points[j].co = (sf.v_dm_rd*math.sin(ph)*math.cos(th),
sf.v_dm_rd*math.sin(ph)*math.sin(th),
sf.v_dm_rd*math.cos(ph), 1)
# 緯度 (横線)
for i in range(1, lt_ed):
ph = (math.pi/2)*(i/sf.v_dm_lt)
sl = cv.splines.new('POLY')
sl.points.add(sf.v_dm_sg - 1)
for j in range(sf.v_dm_sg):
th = (2*math.pi)*(j/(sf.v_dm_sg - 1))
sl.points[j].co = (sf.v_dm_rd*math.sin(ph)*math.cos(th),
sf.v_dm_rd*math.sin(ph)*math.sin(th),
sf.v_dm_rd*math.cos(ph), 1)
sl.use_cyclic_u = True
return cv
ob = aply_dt(f"{PREFIX_VAL}_Ob_Dm", mk_dm, m_dt, 'CURVE')
ob.location, ob.rotation_euler = sf.lc_dm, sf.rt_dm
def upd_tx(sf, cx):
if not sf.b_tx: return
m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Tx", sf.cl_tx)
def mk_tx():
cv = bpy.data.curves.new(f"{PREFIX_VAL}_Cv_Tx", 'FONT')
cv.body = sf.s_tx_bd
cv.size = sf.v_tx_sz
cv.extrude = 0.05
return cv
ob = aply_dt(f"{PREFIX_VAL}_Ob_Tx", mk_tx, m_dt, 'CURVE')
ob.location = sf.lc_tx
ob.rotation_euler = sf.rt_tx
# =========================================================
# 9. プロパティ定義
# =========================================================
class TC_G5_Prp(bpy.types.PropertyGroup):
b_tr: bpy.props.BoolProperty(default=False)
lc_tr: bpy.props.FloatVectorProperty(name="Location", default=(TR_LOC_X_DFLT, TR_LOC_Y_DFLT, TR_LOC_Z_DFLT), update=upd_tr)
rt_tr: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_tr)
v_tr_mj: bpy.props.FloatProperty(name="Major Radius", default=TR_MJ_RD_DFLT, update=upd_tr)
v_tr_mn: bpy.props.FloatProperty(name="Minor Radius", default=TR_MN_RD_DFLT, update=upd_tr)
cl_tr: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_TR_DFLT, min=0, max=1, update=upd_tr)
b_cn: bpy.props.BoolProperty(default=False)
lc_cn: bpy.props.FloatVectorProperty(name="Location", default=(CN_LOC_X_DFLT, CN_LOC_Y_DFLT, CN_LOC_Z_DFLT), update=upd_cn)
rt_cn: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_cn)
v_cn_tp: bpy.props.FloatProperty(name="Top Radius", default=CN_RD_TP_DFLT, update=upd_cn)
v_cn_bt: bpy.props.FloatProperty(name="Bot Radius", default=CN_RD_BT_DFLT, update=upd_cn)
v_cn_ht: bpy.props.FloatProperty(name="Height", default=CN_HT_DFLT, update=upd_cn)
b_cn_cp: bpy.props.BoolProperty(name="No Caps", default=False, update=upd_cn)
b_cn_fl: bpy.props.BoolProperty(name="Flip Norm", default=False, update=upd_cn)
cl_cn_fr: bpy.props.FloatVectorProperty(name="Front Col", subtype='COLOR', size=4, default=COLOR_CN_FR_DFLT, min=0, max=1, update=upd_cn)
cl_cn_bk: bpy.props.FloatVectorProperty(name="Back Col", subtype='COLOR', size=4, default=COLOR_CN_BK_DFLT, min=0, max=1, update=upd_cn)
b_sp: bpy.props.BoolProperty(default=False)
lc_sp: bpy.props.FloatVectorProperty(name="Location", default=(SP_LOC_X_DFLT, SP_LOC_Y_DFLT, SP_LOC_Z_DFLT), update=upd_sp)
rt_sp: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_sp)
v_sp_rd: bpy.props.FloatProperty(name="Radius", default=SP_RD_DFLT, update=upd_sp)
cl_sp: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_SP_DFLT, min=0, max=1, update=upd_sp)
b_aw: bpy.props.BoolProperty(default=False)
v_aw_cn: bpy.props.IntProperty(name="Count", default=AW_CNT_DFLT, min=1, max=100, update=upd_aw)
lc_aw_tg: bpy.props.FloatVectorProperty(name="Target Loc", default=(0,0,10), update=upd_aw)
v_aw_of: bpy.props.FloatProperty(name="Offset %", default=100.0, min=1, max=100, update=upd_aw)
v_aw_th: bpy.props.FloatProperty(name="Thickness", default=AW_THK_DFLT, update=upd_aw)
cl_aw: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_AW_DFLT, min=0, max=1, update=upd_aw)
b_dm: bpy.props.BoolProperty(default=False)
s_dm_md: bpy.props.EnumProperty(
items=[('HEMI', "半球 (Dome)", ""), ('FULL', "球体 (Sphere)", "")],
default=DM_MODE_DFLT, update=upd_dm
)
lc_dm: bpy.props.FloatVectorProperty(name="Location", default=(DM_LOC_X_DFLT, DM_LOC_Y_DFLT, DM_LOC_Z_DFLT), update=upd_dm)
rt_dm: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_dm)
v_dm_rd: bpy.props.FloatProperty(name="Radius", default=DM_RD_DFLT, update=upd_dm)
v_dm_th: bpy.props.FloatProperty(name="Thickness", default=DM_THK_DFLT, update=upd_dm)
v_dm_ln: bpy.props.IntProperty(name="Longt Count", default=12, update=upd_dm)
v_dm_lt: bpy.props.IntProperty(name="Lat Count (per Hemi)", default=6, update=upd_dm)
v_dm_sg: bpy.props.IntProperty(name="Segments", default=32, update=upd_dm)
cl_dm: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_DM_DFLT, min=0, max=1, update=upd_dm)
b_tx: bpy.props.BoolProperty(default=False)
s_tx_bd: bpy.props.StringProperty(name="Text", default=TEXT_BODY_DFLT, update=upd_tx)
v_tx_sz: bpy.props.FloatProperty(name="Size", default=TEXT_SIZE_DFLT, update=upd_tx)
lc_tx: bpy.props.FloatVectorProperty(name="Location", default=(TEXT_OFFSET_X_DFLT, TEXT_OFFSET_Y_DFLT, TEXT_OFFSET_Z_DFLT), update=upd_tx)
rt_tx: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(1.5708,0,0), update=upd_tx)
cl_tx: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_TX_DFLT, min=0, max=1, update=upd_tx)
# =========================================================
# 10. オペレーター群
# =========================================================
class Op_Cr_Tr(bpy.types.Operator):
bl_idname = f"{PREFIX}.cr_tr"
bl_label = "トーラス生成"
def execute(self, cx):
cx.scene.tc_g5_prp.b_tr = True
upd_tr(cx.scene.tc_g5_prp, cx)
return {'FINISHED'}
class Op_Cr_Cn(bpy.types.Operator):
bl_idname = f"{PREFIX}.cr_cn"
bl_label = "円錐台生成"
def execute(self, cx):
cx.scene.tc_g5_prp.b_cn = True
upd_cn(cx.scene.tc_g5_prp, cx)
return {'FINISHED'}
class Op_Cr_Sp(bpy.types.Operator):
bl_idname = f"{PREFIX}.cr_sp"
bl_label = "球体生成"
def execute(self, cx):
cx.scene.tc_g5_prp.b_sp = True
upd_sp(cx.scene.tc_g5_prp, cx)
return {'FINISHED'}
class Op_Cr_Aw(bpy.types.Operator):
bl_idname = f"{PREFIX}.cr_aw"
bl_label = "集中矢印生成"
def execute(self, cx):
cx.scene.tc_g5_prp.b_aw = True
upd_aw(cx.scene.tc_g5_prp, cx)
return {'FINISHED'}
class Op_Cr_Dm(bpy.types.Operator):
bl_idname = f"{PREFIX}.cr_dm"
bl_label = "骨格生成"
def execute(self, cx):
cx.scene.tc_g5_prp.b_dm = True
upd_dm(cx.scene.tc_g5_prp, cx)
return {'FINISHED'}
class Op_Cr_Tx(bpy.types.Operator):
bl_idname = f"{PREFIX}.cr_tx"
bl_label = "テキスト生成"
def execute(self, cx):
cx.scene.tc_g5_prp.b_tx = True
upd_tx(cx.scene.tc_g5_prp, cx)
return {'FINISHED'}
class Op_Dt_Tr(bpy.types.Operator):
bl_idname = f"{PREFIX}.dt_tr"
bl_label = "独立"
def execute(self, cx):
dtch_ob(cx, f"{PREFIX_VAL}_Ob_Tr", "Detached_Torus", "b_tr")
return {'FINISHED'}
class Op_Dt_Cn(bpy.types.Operator):
bl_idname = f"{PREFIX}.dt_cn"
bl_label = "独立"
def execute(self, cx):
dtch_ob(cx, f"{PREFIX_VAL}_Ob_Cn", "Detached_Cone", "b_cn")
return {'FINISHED'}
class Op_Dt_Sp(bpy.types.Operator):
bl_idname = f"{PREFIX}.dt_sp"
bl_label = "独立"
def execute(self, cx):
dtch_ob(cx, f"{PREFIX_VAL}_Ob_Sp", "Detached_Sphere", "b_sp")
return {'FINISHED'}
class Op_Dt_Aw(bpy.types.Operator):
bl_idname = f"{PREFIX}.dt_aw"
bl_label = "独立"
def execute(self, cx):
dtch_ob(cx, f"{PREFIX_VAL}_Ob_Aw", "Detached_Arrows", "b_aw")
return {'FINISHED'}
class Op_Dt_Dm(bpy.types.Operator):
bl_idname = f"{PREFIX}.dt_dm"
bl_label = "独立"
def execute(self, cx):
dtch_ob(cx, f"{PREFIX_VAL}_Ob_Dm", "Detached_Bone", "b_dm")
return {'FINISHED'}
class Op_Dt_Tx(bpy.types.Operator):
bl_idname = f"{PREFIX}.dt_tx"
bl_label = "独立"
def execute(self, cx):
dtch_ob(cx, f"{PREFIX_VAL}_Ob_Tx", "Detached_Text", "b_tx")
return {'FINISHED'}
class Op_Op_Ur(bpy.types.Operator):
bl_idname = f"{PREFIX}.op_ur"
bl_label = "Open URL"
url: bpy.props.StringProperty()
def execute(self, cx):
webbrowser.open(self.url)
return {'FINISHED'}
def dly_unrg():
try: addon_utils.disable(__name__)
except Exception: pass
try: unregister()
except Exception: pass
return None
class Op_Rm_Ad(bpy.types.Operator):
bl_idname = f"{PREFIX}.rm_ad"
bl_label = "Clean up & Uninstall"
def execute(self, cx):
c_dt = bpy.data.collections.get(f"{PREFIX_VAL}_Col")
if c_dt:
for ob in list(c_dt.objects): bpy.data.objects.remove(ob, do_unlink=True)
bpy.data.collections.remove(c_dt)
for m in list(bpy.data.meshes):
if m.name.startswith(PREFIX_VAL): bpy.data.meshes.remove(m, do_unlink=True)
for c in list(bpy.data.curves):
if c.name.startswith(PREFIX_VAL): bpy.data.curves.remove(c, do_unlink=True)
for mt in list(bpy.data.materials):
if mt.name.startswith(PREFIX_VAL): bpy.data.materials.remove(mt, do_unlink=True)
bpy.app.timers.register(dly_unrg, first_interval=0.2)
return {'FINISHED'}
class Op_Xp_Tx(bpy.types.Operator):
bl_idname = f"{PREFIX}.xp_tx"
bl_label = "設定をコードに書き出し"
def execute(self, cx):
if not hasattr(cx.scene, "tc_g5_prp"): return {'CANCELLED'}
p = cx.scene.tc_g5_prp
ss = ""
fp = globals().get("__file__")
if fp and fp.endswith(".pyc"): fp = fp[:-1]
if fp and os.path.exists(fp):
try:
with open(fp, "r", encoding="utf-8") as f: ss = f.read()
except Exception: pass
if not ss:
for tx in bpy.data.texts:
bd = tx.as_string()
if ("# " + "--- DEFAULTS_BLOCK_START ---") in bd:
ss = bd
break
if not ss: return {'CANCELLED'}
ds = "# " + "--- DEFAULTS_BLOCK_START ---\n\n"
ds += f"TR_MJ_RD_DFLT = {p.v_tr_mj}\nTR_MN_RD_DFLT = {p.v_tr_mn}\nCN_RD_TP_DFLT = {p.v_cn_tp}\nCN_RD_BT_DFLT = {p.v_cn_bt}\n"
ds += f"CN_HT_DFLT = {p.v_cn_ht}\nSP_RD_DFLT = {p.v_sp_rd}\nAW_CNT_DFLT = {p.v_aw_cn}\nAW_THK_DFLT = {p.v_aw_th}\n"
ds += f"DM_RD_DFLT = {p.v_dm_rd}\nDM_THK_DFLT = {p.v_dm_th}\n\n"
ds += f"DM_MODE_DFLT = '{p.s_dm_md}'\n\n"
ds += f"TR_LOC_X_DFLT, TR_LOC_Y_DFLT, TR_LOC_Z_DFLT = {p.lc_tr[0]:.2f}, {p.lc_tr[1]:.2f}, {p.lc_tr[2]:.2f}\n"
ds += f"CN_LOC_X_DFLT, CN_LOC_Y_DFLT, CN_LOC_Z_DFLT = {p.lc_cn[0]:.2f}, {p.lc_cn[1]:.2f}, {p.lc_cn[2]:.2f}\n"
ds += f"SP_LOC_X_DFLT, SP_LOC_Y_DFLT, SP_LOC_Z_DFLT = {p.lc_sp[0]:.2f}, {p.lc_sp[1]:.2f}, {p.lc_sp[2]:.2f}\n"
ds += f"DM_LOC_X_DFLT, DM_LOC_Y_DFLT, DM_LOC_Z_DFLT = {p.lc_dm[0]:.2f}, {p.lc_dm[1]:.2f}, {p.lc_dm[2]:.2f}\n\n"
ds += f"TEXT_BODY_DFLT = \"{p.s_tx_bd}\"\nTEXT_SIZE_DFLT = {p.v_tx_sz}\n"
ds += f"TEXT_OFFSET_X_DFLT, TEXT_OFFSET_Y_DFLT, TEXT_OFFSET_Z_DFLT = {p.lc_tx[0]:.2f}, {p.lc_tx[1]:.2f}, {p.lc_tx[2]:.2f}\n\n"
ds += f"COLOR_TR_DFLT = ({p.cl_tr[0]:.4f}, {p.cl_tr[1]:.4f}, {p.cl_tr[2]:.4f}, {p.cl_tr[3]:.4f})\n"
ds += f"COLOR_CN_FR_DFLT = ({p.cl_cn_fr[0]:.4f}, {p.cl_cn_fr[1]:.4f}, {p.cl_cn_fr[2]:.4f}, {p.cl_cn_fr[3]:.4f})\n"
ds += f"COLOR_CN_BK_DFLT = ({p.cl_cn_bk[0]:.4f}, {p.cl_cn_bk[1]:.4f}, {p.cl_cn_bk[2]:.4f}, {p.cl_cn_bk[3]:.4f})\n"
ds += f"COLOR_SP_DFLT = ({p.cl_sp[0]:.4f}, {p.cl_sp[1]:.4f}, {p.cl_sp[2]:.4f}, {p.cl_sp[3]:.4f})\n"
ds += f"COLOR_AW_DFLT = ({p.cl_aw[0]:.4f}, {p.cl_aw[1]:.4f}, {p.cl_aw[2]:.4f}, {p.cl_aw[3]:.4f})\n"
ds += f"COLOR_DM_DFLT = ({p.cl_dm[0]:.4f}, {p.cl_dm[1]:.4f}, {p.cl_dm[2]:.4f}, {p.cl_dm[3]:.4f})\n"
ds += f"COLOR_TX_DFLT = ({p.cl_tx[0]:.4f}, {p.cl_tx[1]:.4f}, {p.cl_tx[2]:.4f}, {p.cl_tx[3]:.4f})\n\n"
m1 = "# " + "--- DEFAULTS_BLOCK_START ---"
m2 = "# " + "--- DEFAULTS_BLOCK_END ---"
pr = r"(" + m1 + r"\n)(.*?)(" + m2 + r")"
ns = re.sub(pr, r"\g<1>" + ds + r"\g<3>", ss, flags=re.DOTALL)
tn = "Exp_TCS_G5.py"
nb = bpy.data.texts.get(tn)
if nb: nb.clear()
else: nb = bpy.data.texts.new(tn)
nb.write(ns)
sf = False
for a in cx.screen.areas:
if a.type == 'TEXT_EDITOR':
a.spaces.active.text = nb
sf = True
break
if sf: self.report({'INFO'}, f"書き出し成功: {tn}")
else: self.report({'INFO'}, f"内部データに保存: {tn}")
return {'FINISHED'}
# =========================================================
# 11. UIパネル
# =========================================================
class Pn_Bs(bpy.types.Panel):
bl_space_type, bl_region_type, bl_category = 'VIEW_3D', 'UI', ADDON_CATEGORY
class Pn_Tr(Pn_Bs):
bl_label, bl_idname, bl_order = "1. トーラス", f"{PREFIX_VAL}_P_Tr", 1
def draw(self, cx):
ly, p = self.layout, cx.scene.tc_g5_prp
c = ly.column(align=True)
c.prop(p, "lc_tr"); c.prop(p, "rt_tr"); c.separator()
c.prop(p, "v_tr_mj"); c.prop(p, "v_tr_mn"); c.prop(p, "cl_tr"); ly.separator()
r = ly.row(align=True)
if not p.b_tr: r.operator(Op_Cr_Tr.bl_idname, icon='MESH_TORUS')
else:
r.operator(Op_Cr_Tr.bl_idname, text="更新", icon='FILE_REFRESH')
r.operator(Op_Dt_Tr.bl_idname, icon='UNLINKED')
class Pn_Cn(Pn_Bs):
bl_label, bl_idname, bl_order = "2. 円錐台", f"{PREFIX_VAL}_P_Cn", 2
def draw(self, cx):
ly, p = self.layout, cx.scene.tc_g5_prp
c = ly.column(align=True)
c.prop(p, "lc_cn"); c.prop(p, "rt_cn"); c.separator()
r1 = c.row(align=True); r1.prop(p, "v_cn_tp"); r1.prop(p, "v_cn_bt")
c.prop(p, "v_cn_ht"); c.separator()
r2 = c.row(align=True); r2.prop(p, "b_cn_cp"); r2.prop(p, "b_cn_fl"); c.separator()
r3 = c.row(align=True); r3.prop(p, "cl_cn_fr", text="表面"); r3.prop(p, "cl_cn_bk", text="裏面"); ly.separator()
r = ly.row(align=True)
if not p.b_cn: r.operator(Op_Cr_Cn.bl_idname, icon='MESH_CONE')
else:
r.operator(Op_Cr_Cn.bl_idname, text="更新", icon='FILE_REFRESH')
r.operator(Op_Dt_Cn.bl_idname, icon='UNLINKED')
class Pn_Sp(Pn_Bs):
bl_label, bl_idname, bl_order = "3. 球体", f"{PREFIX_VAL}_P_Sp", 3
def draw(self, cx):
ly, p = self.layout, cx.scene.tc_g5_prp
c = ly.column(align=True)
c.prop(p, "lc_sp"); c.prop(p, "rt_sp"); c.separator()
c.prop(p, "v_sp_rd"); c.prop(p, "cl_sp"); ly.separator()
r = ly.row(align=True)
if not p.b_sp: r.operator(Op_Cr_Sp.bl_idname, icon='MESH_UVSPHERE')
else:
r.operator(Op_Cr_Sp.bl_idname, text="更新", icon='FILE_REFRESH')
r.operator(Op_Dt_Sp.bl_idname, icon='UNLINKED')
class Pn_Aw(Pn_Bs):
bl_label, bl_idname, bl_order = "4. 集中矢印", f"{PREFIX_VAL}_P_Aw", 4
def draw(self, cx):
ly, p = self.layout, cx.scene.tc_g5_prp
c = ly.column(align=True)
c.prop(p, "v_aw_cn"); c.prop(p, "lc_aw_tg"); c.prop(p, "v_aw_of"); c.separator()
c.prop(p, "v_aw_th"); c.prop(p, "cl_aw"); ly.separator()
r = ly.row(align=True)
if not p.b_aw: r.operator(Op_Cr_Aw.bl_idname, icon='TRACKING')
else:
r.operator(Op_Cr_Aw.bl_idname, text="更新", icon='FILE_REFRESH')
r.operator(Op_Dt_Aw.bl_idname, icon='UNLINKED')
class Pn_Dm(Pn_Bs):
bl_label, bl_idname, bl_order = "5. 骨格形状", f"{PREFIX_VAL}_P_Dm", 5
def draw(self, cx):
ly, p = self.layout, cx.scene.tc_g5_prp
c = ly.column(align=True)
c.prop(p, "s_dm_md", text="形状")
c.separator()
c.prop(p, "lc_dm"); c.prop(p, "rt_dm"); c.separator()
c.prop(p, "v_dm_rd"); c.prop(p, "v_dm_th"); c.separator()
r1 = c.row(align=True); r1.prop(p, "v_dm_ln"); r1.prop(p, "v_dm_lt")
c.prop(p, "v_dm_sg"); c.separator(); c.prop(p, "cl_dm"); ly.separator()
r = ly.row(align=True)
if not p.b_dm: r.operator(Op_Cr_Dm.bl_idname, icon='MESH_UVSPHERE')
else:
r.operator(Op_Cr_Dm.bl_idname, text="更新", icon='FILE_REFRESH')
r.operator(Op_Dt_Dm.bl_idname, icon='UNLINKED')
class Pn_Tx(Pn_Bs):
bl_label, bl_idname, bl_order = "6. 3Dテキスト", f"{PREFIX_VAL}_P_Tx", 6
def draw(self, cx):
ly, p = self.layout, cx.scene.tc_g5_prp
c = ly.column(align=True)
c.prop(p, "s_tx_bd"); c.prop(p, "v_tx_sz"); c.separator()
c.prop(p, "lc_tx"); c.prop(p, "rt_tx"); c.separator(); c.prop(p, "cl_tx"); ly.separator()
r = ly.row(align=True)
if not p.b_tx: r.operator(Op_Cr_Tx.bl_idname, icon='FONT_DATA')
else:
r.operator(Op_Cr_Tx.bl_idname, text="更新", icon='FILE_REFRESH')
r.operator(Op_Dt_Tx.bl_idname, icon='UNLINKED')
class Pn_Lnk(Pn_Bs):
bl_label, bl_idname, bl_order = "Links", f"{PREFIX_VAL}_P_Lnk", 10
def draw(self, cx):
b = self.layout.box()
if LNK_NM_1: b.operator(Op_Op_Ur.bl_idname, text=LNK_NM_1, icon='URL').url = LNK_UR_1
if LNK_NM_2: b.operator(Op_Op_Ur.bl_idname, text=LNK_NM_2, icon='URL').url = LNK_UR_2
if LNK_NM_3: b.operator(Op_Op_Ur.bl_idname, text=LNK_NM_3, icon='URL').url = LNK_UR_3
if LNK_NM_4: b.operator(Op_Op_Ur.bl_idname, text=LNK_NM_4, icon='URL').url = LNK_UR_4
class Pn_Rm(Pn_Bs):
bl_label, bl_idname, bl_order, bl_options = "管理 & エクスポート", f"{PREFIX_VAL}_P_Rm", 11, {'DEFAULT_CLOSED'}
def draw(self, cx):
bx = self.layout.box()
bx.operator(Op_Xp_Tx.bl_idname, icon='TEXT')
bx.separator()
bx.operator(Op_Rm_Ad.bl_idname, icon='CANCEL')
# =========================================================
# 12. 起動時自動展開処理
# =========================================================
def ui_st_up():
try:
cx = bpy.context
if not cx or not cx.window_manager.windows: return 0.2
for w in cx.window_manager.windows:
for a in w.screen.areas:
if a.type == 'VIEW_3D':
for s in a.spaces:
if s.type == 'VIEW_3D':
s.shading.type = 'MATERIAL'
s.show_region_ui = True
except Exception: pass
return None
cls_ls = [
TC_G5_Prp, Op_Cr_Tr, Op_Cr_Cn, Op_Cr_Sp, Op_Cr_Aw, Op_Cr_Dm, Op_Cr_Tx, Op_Op_Ur,
Op_Dt_Tr, Op_Dt_Cn, Op_Dt_Sp, Op_Dt_Aw, Op_Dt_Dm, Op_Dt_Tx,
Op_Rm_Ad, Op_Xp_Tx,
Pn_Tr, Pn_Cn, Pn_Sp, Pn_Aw, Pn_Dm, Pn_Tx, Pn_Lnk, Pn_Rm
]
def register():
for c in cls_ls: bpy.utils.register_class(c)
bpy.types.Scene.tc_g5_prp = bpy.props.PointerProperty(type=TC_G5_Prp)
if not bpy.app.timers.is_registered(ui_st_up): bpy.app.timers.register(ui_st_up, first_interval=0.2)
def unregister():
try: del bpy.types.Scene.tc_g5_prp
except Exception: pass
for c in reversed(cls_ls):
try: bpy.utils.unregister_class(c)
except RuntimeError: pass
if bpy.app.timers.is_registered(ui_st_up): bpy.app.timers.unregister(ui_st_up)
if __name__ == "__main__": register()
# =========================================================
# 1. bl_info
# =========================================================
bl_info = {
"name": "TCS & Arrows Generator 5.0",
"author": "zionadchat",
"version": (3, 0, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar",
"description": "Blender5専用: 自動UI展開・文字化け回避・エクスポートバグ対策版",
"category": "3D View"
}
# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0725_1700
# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import time
import re
import os
import webbrowser
import addon_utils
from mathutils import Vector, Euler
# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "TCS & Arrows 5"
PREFIX_VAL = "TCS_ARR_G5"
ADDON_CATEGORY = "TCS Gen 5.0"
PREFIX = PREFIX_VAL.lower()
# =========================================================
# 5. リンク定義
# =========================================================
LNK_NM_1 = "マニュアルページ"
LNK_UR_1 = "<https://example.com/manual>"
LNK_NM_2 = "コンテナ builder"
LNK_UR_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"
LNK_NM_3 = "posfie"
LNK_UR_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LNK_NM_4 = "zionadchat"
LNK_UR_4 = "<https://x.com/zionadchat>"
# =========================================================
# 6. 初期設定
# =========================================================
# --- DEFAULTS_BLOCK_START ---
# --- DEFAULTS_BLOCK_START ---
TR_MJ_RD_DFLT = 3.0
TR_MN_RD_DFLT = 0.20000000298023224
CN_RD_TP_DFLT = 0.0
CN_RD_BT_DFLT = 2.0
CN_HT_DFLT = 4.0
SP_RD_DFLT = 1.5
AW_CNT_DFLT = 8
AW_THK_DFLT = 0.10000000149011612
DM_RD_DFLT = 3.0
DM_THK_DFLT = 0.10000000149011612
TR_LOC_X_DFLT, TR_LOC_Y_DFLT, TR_LOC_Z_DFLT = 0.00, 0.00, 0.00
CN_LOC_X_DFLT, CN_LOC_Y_DFLT, CN_LOC_Z_DFLT = 5.00, 0.00, 0.00
SP_LOC_X_DFLT, SP_LOC_Y_DFLT, SP_LOC_Z_DFLT = -5.00, 0.00, 0.00
DM_LOC_X_DFLT, DM_LOC_Y_DFLT, DM_LOC_Z_DFLT = 0.00, 5.00, 0.00
TEXT_BODY_DFLT = "Sample Text"
TEXT_SIZE_DFLT = 1.5
TEXT_OFFSET_X_DFLT, TEXT_OFFSET_Y_DFLT, TEXT_OFFSET_Z_DFLT = 0.00, 0.00, 3.00
COLOR_TR_DFLT = (0.2000, 0.6000, 1.0000, 1.0000)
COLOR_CN_FR_DFLT = (1.0000, 0.5000, 0.1000, 1.0000)
COLOR_CN_BK_DFLT = (0.1000, 0.5000, 1.0000, 1.0000)
COLOR_SP_DFLT = (0.2000, 0.9000, 0.4000, 1.0000)
COLOR_AW_DFLT = (1.0000, 0.1000, 0.1000, 1.0000)
COLOR_DM_DFLT = (0.5729, 0.1036, 0.7038, 1.0000)
COLOR_TX_DFLT = (1.0000, 0.0376, 0.1255, 1.0000)
# --- DEFAULTS_BLOCK_END ---
# =========================================================
# 7. コアユーティリティ (文字化け回避対応済)
# =========================================================
def gt_cr_clc(nm_st):
c_dt = bpy.data.collections.get(nm_st)
if not c_dt:
c_dt = bpy.data.collections.new(nm_st)
bpy.context.scene.collection.children.link(c_dt)
return c_dt
def gt_cr_mt(nm_st, cl_tp):
m_dt = bpy.data.materials.get(nm_st)
if not m_dt:
m_dt = bpy.data.materials.new(nm_st)
m_dt.use_nodes = True
try:
m_dt.blend_method = 'BLEND'
m_dt.shadow_method = 'NONE'
except Exception: pass
if m_dt.use_nodes:
bs_nd = m_dt.node_tree.nodes.get("Principled BSDF")
if bs_nd:
if "Base Color" in bs_nd.inputs: bs_nd.inputs["Base Color"].default_value = (cl_tp[0], cl_tp[1], cl_tp[2], 1.0)
if "Alpha" in bs_nd.inputs: bs_nd.inputs["Alpha"].default_value = cl_tp[3]
m_dt.diffuse_color = cl_tp
return m_dt
def gt_cr_mt_dbl(nm_st, c_fr, c_bk):
m_dt = bpy.data.materials.get(nm_st)
if not m_dt:
m_dt = bpy.data.materials.new(nm_st)
m_dt.use_nodes = True
try:
m_dt.blend_method = 'BLEND'
m_dt.shadow_method = 'NONE'
except Exception: pass
if m_dt.use_nodes:
tr_dt = m_dt.node_tree
bs_fr = tr_dt.nodes.get("Pr_Fr")
if not bs_fr:
tr_dt.nodes.clear()
out_nd = tr_dt.nodes.new('ShaderNodeOutputMaterial')
out_nd.location = (400, 0)
mx_nd = tr_dt.nodes.new('ShaderNodeMixShader')
mx_nd.location = (200, 0)
bs_fr = tr_dt.nodes.new('ShaderNodeBsdfPrincipled')
bs_fr.name = "Pr_Fr"
bs_fr.location = (0, 150)
bs_bk = tr_dt.nodes.new('ShaderNodeBsdfPrincipled')
bs_bk.name = "Pr_Bk"
bs_bk.location = (0, -200)
gm_nd = tr_dt.nodes.new('ShaderNodeNewGeometry')
gm_nd.location = (0, 400)
tr_dt.links.new(gm_nd.outputs["Backfacing"], mx_nd.inputs[0])
tr_dt.links.new(bs_fr.outputs["BSDF"], mx_nd.inputs[1])
tr_dt.links.new(bs_bk.outputs["BSDF"], mx_nd.inputs[2])
tr_dt.links.new(mx_nd.outputs["Shader"], out_nd.inputs["Surface"])
bs_fr = tr_dt.nodes.get("Pr_Fr")
bs_bk = tr_dt.nodes.get("Pr_Bk")
if bs_fr:
if "Base Color" in bs_fr.inputs: bs_fr.inputs["Base Color"].default_value = (c_fr[0], c_fr[1], c_fr[2], 1.0)
if "Alpha" in bs_fr.inputs: bs_fr.inputs["Alpha"].default_value = c_fr[3]
if bs_bk:
if "Base Color" in bs_bk.inputs: bs_bk.inputs["Base Color"].default_value = (c_bk[0], c_bk[1], c_bk[2], 1.0)
if "Alpha" in bs_bk.inputs: bs_bk.inputs["Alpha"].default_value = c_bk[3]
m_dt.diffuse_color = c_fr
return m_dt
def aply_dt(ob_nm, dt_fn, m_dt, dt_tp='MESH'):
c_dt = gt_cr_clc(f"{PREFIX_VAL}_Col")
ob_dt = bpy.data.objects.get(ob_nm)
if ob_dt:
od_dt = ob_dt.data
ob_dt.data = dt_fn()
if od_dt.users == 0:
if dt_tp == 'MESH': bpy.data.meshes.remove(od_dt, do_unlink=True)
elif dt_tp == 'CURVE': bpy.data.curves.remove(od_dt, do_unlink=True)
else:
nd_dt = dt_fn()
ob_dt = bpy.data.objects.new(ob_nm, nd_dt)
c_dt.objects.link(ob_dt)
if not ob_dt.data.materials: ob_dt.data.materials.append(m_dt)
else: ob_dt.data.materials[0] = m_dt
return ob_dt
def dtch_ob(ctx, ob_nm, nw_nm, flg_st):
ob_dt = bpy.data.objects.get(ob_nm)
if not ob_dt: return False
ts = time.strftime("%H%M%S")
rn_nm = f"{nw_nm}_{ts}"
ob_dt.name = rn_nm
if ob_dt.data: ob_dt.data.name = f"{rn_nm}_Dt"
for sl in ob_dt.material_slots:
if sl.material and sl.material.name.startswith(PREFIX_VAL):
nw_mt = sl.material.copy()
nw_mt.name = f"{rn_nm}_Mt"
sl.material = nw_mt
m_cl = ctx.scene.collection
if ob_dt.name not in m_cl.objects: m_cl.objects.link(ob_dt)
a_cl = bpy.data.collections.get(f"{PREFIX_VAL}_Col")
if a_cl and ob_dt.name in a_cl.objects: a_cl.objects.unlink(ob_dt)
setattr(ctx.scene.tc_g5_prp, flg_st, False)
return True
# =========================================================
# 8. 更新処理 (Update)
# =========================================================
def upd_tr(sf, cx):
if not sf.b_tr: return
m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Tr", sf.cl_tr)
def mk_tr():
v, f = [], []
S, MS = 48, 16
for i in range(S):
a1 = (i/S)*2*math.pi
c1, s1 = math.cos(a1), math.sin(a1)
for j in range(MS):
a2 = (j/MS)*2*math.pi
c2, s2 = math.cos(a2), math.sin(a2)
v.append(((sf.v_tr_mj + sf.v_tr_mn*c2)*c1, (sf.v_tr_mj + sf.v_tr_mn*c2)*s1, sf.v_tr_mn*s2))
for i in range(S):
for j in range(MS):
i_n, j_n = (i+1)%S, (j+1)%MS
f.append((i*MS+j, i_n*MS+j, i_n*MS+j_n, i*MS+j_n))
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Tr")
me.from_pydata(v, [], f)
me.update()
return me
ob = aply_dt(f"{PREFIX_VAL}_Ob_Tr", mk_tr, m_dt)
ob.location, ob.rotation_euler = sf.lc_tr, sf.rt_tr
if sf.b_aw: upd_aw(sf, cx)
def upd_cn(sf, cx):
if not sf.b_cn: return
m_dt = gt_cr_mt_dbl(f"{PREFIX_VAL}_Mt_Cn", sf.cl_cn_fr, sf.cl_cn_bk)
def mk_cn():
bm = bmesh.new()
ah = max(abs(sf.v_cn_ht), 0.001)
r1, r2, zo = (sf.v_cn_bt, sf.v_cn_tp, ah/2) if sf.v_cn_ht >= 0 else (sf.v_cn_tp, sf.v_cn_bt, -ah/2)
bmesh.ops.create_cone(bm, cap_ends=not sf.b_cn_cp, cap_tris=True, segments=48, radius1=r1, radius2=r2, depth=ah)
bmesh.ops.translate(bm, verts=bm.verts, vec=(0,0,zo))
if sf.b_cn_fl: bmesh.ops.reverse_faces(bm, faces=bm.faces)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Cn")
bm.to_mesh(me)
bm.free()
return me
ob = aply_dt(f"{PREFIX_VAL}_Ob_Cn", mk_cn, m_dt)
ob.location, ob.rotation_euler = sf.lc_cn, sf.rt_cn
def upd_sp(sf, cx):
if not sf.b_sp: return
m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Sp", sf.cl_sp)
def mk_sp():
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=sf.v_sp_rd)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Sp")
bm.to_mesh(me)
bm.free()
return me
ob = aply_dt(f"{PREFIX_VAL}_Ob_Sp", mk_sp, m_dt)
ob.location, ob.rotation_euler = sf.lc_sp, sf.rt_sp
def upd_aw(sf, cx):
if not sf.b_aw: return
m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Aw", sf.cl_aw)
def mk_aw():
bm = bmesh.new()
tl, tm = Vector(sf.lc_tr), Euler(sf.rt_tr).to_matrix()
for i in range(sf.v_aw_cn):
th = i*(2*math.pi/sf.v_aw_cn)
sp = tl + tm @ Vector((sf.v_tr_mj*math.cos(th), sf.v_tr_mj*math.sin(th), 0))
vc = Vector(sf.lc_aw_tg) - sp
ds = vc.length * (sf.v_aw_of/100.0)
if ds < 0.001: continue
hl = min(sf.v_aw_th*4.0, ds*0.5)
sl = ds - hl
rq = Vector((0,0,1)).rotation_difference(vc.normalized())
gs = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12, radius1=sf.v_aw_th, radius2=sf.v_aw_th, depth=sl)
bmesh.ops.translate(bm, verts=gs['verts'], vec=(0,0,sl/2))
gh = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=12, radius1=sf.v_aw_th*2.5, radius2=0, depth=hl)
bmesh.ops.translate(bm, verts=gh['verts'], vec=(0,0,sl+hl/2))
va = gs['verts'] + gh['verts']
bmesh.ops.transform(bm, verts=va, matrix=rq.to_matrix().to_4x4())
bmesh.ops.translate(bm, verts=va, vec=sp)
me = bpy.data.meshes.new(f"{PREFIX_VAL}_Ms_Aw")
bm.to_mesh(me)
bm.free()
return me
ob = aply_dt(f"{PREFIX_VAL}_Ob_Aw", mk_aw, m_dt)
ob.location, ob.rotation_euler = (0,0,0), (0,0,0)
def upd_dm(sf, cx):
if not sf.b_dm: return
m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Dm", sf.cl_dm)
def mk_dm():
cv = bpy.data.curves.new(f"{PREFIX_VAL}_Cv_Dm", 'CURVE')
cv.dimensions, cv.fill_mode = '3D', 'FULL'
cv.bevel_depth, cv.bevel_resolution = sf.v_dm_th, 4
for i in range(sf.v_dm_ln):
th = (2*math.pi/sf.v_dm_ln)*i
sl = cv.splines.new('POLY')
sl.points.add(sf.v_dm_sg - 1)
for j in range(sf.v_dm_sg):
ph = (math.pi/2)*(j/(sf.v_dm_sg - 1))
sl.points[j].co = (sf.v_dm_rd*math.sin(ph)*math.cos(th), sf.v_dm_rd*math.sin(ph)*math.sin(th), sf.v_dm_rd*math.cos(ph), 1)
for i in range(1, sf.v_dm_lt + 1):
ph = (math.pi/2)*(i/sf.v_dm_lt)
sl = cv.splines.new('POLY')
sl.points.add(sf.v_dm_sg - 1)
for j in range(sf.v_dm_sg):
th = (2*math.pi)*(j/(sf.v_dm_sg - 1))
sl.points[j].co = (sf.v_dm_rd*math.sin(ph)*math.cos(th), sf.v_dm_rd*math.sin(ph)*math.sin(th), sf.v_dm_rd*math.cos(ph), 1)
sl.use_cyclic_u = True
return cv
ob = aply_dt(f"{PREFIX_VAL}_Ob_Dm", mk_dm, m_dt, 'CURVE')
ob.location, ob.rotation_euler = sf.lc_dm, sf.rt_dm
def upd_tx(sf, cx):
if not sf.b_tx: return
m_dt = gt_cr_mt(f"{PREFIX_VAL}_Mt_Tx", sf.cl_tx)
def mk_tx():
cv = bpy.data.curves.new(f"{PREFIX_VAL}_Cv_Tx", 'FONT')
cv.body = sf.s_tx_bd
cv.size = sf.v_tx_sz
cv.extrude = 0.05
return cv
ob = aply_dt(f"{PREFIX_VAL}_Ob_Tx", mk_tx, m_dt, 'CURVE')
ob.location = sf.lc_tx
ob.rotation_euler = sf.rt_tx
# =========================================================
# 9. プロパティ定義
# =========================================================
class TC_G5_Prp(bpy.types.PropertyGroup):
b_tr: bpy.props.BoolProperty(default=False)
lc_tr: bpy.props.FloatVectorProperty(name="Location", default=(TR_LOC_X_DFLT, TR_LOC_Y_DFLT, TR_LOC_Z_DFLT), update=upd_tr)
rt_tr: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_tr)
v_tr_mj: bpy.props.FloatProperty(name="Major Radius", default=TR_MJ_RD_DFLT, update=upd_tr)
v_tr_mn: bpy.props.FloatProperty(name="Minor Radius", default=TR_MN_RD_DFLT, update=upd_tr)
cl_tr: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_TR_DFLT, min=0, max=1, update=upd_tr)
b_cn: bpy.props.BoolProperty(default=False)
lc_cn: bpy.props.FloatVectorProperty(name="Location", default=(CN_LOC_X_DFLT, CN_LOC_Y_DFLT, CN_LOC_Z_DFLT), update=upd_cn)
rt_cn: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_cn)
v_cn_tp: bpy.props.FloatProperty(name="Top Radius", default=CN_RD_TP_DFLT, update=upd_cn)
v_cn_bt: bpy.props.FloatProperty(name="Bot Radius", default=CN_RD_BT_DFLT, update=upd_cn)
v_cn_ht: bpy.props.FloatProperty(name="Height", default=CN_HT_DFLT, update=upd_cn)
b_cn_cp: bpy.props.BoolProperty(name="No Caps", default=False, update=upd_cn)
b_cn_fl: bpy.props.BoolProperty(name="Flip Norm", default=False, update=upd_cn)
cl_cn_fr: bpy.props.FloatVectorProperty(name="Front Col", subtype='COLOR', size=4, default=COLOR_CN_FR_DFLT, min=0, max=1, update=upd_cn)
cl_cn_bk: bpy.props.FloatVectorProperty(name="Back Col", subtype='COLOR', size=4, default=COLOR_CN_BK_DFLT, min=0, max=1, update=upd_cn)
b_sp: bpy.props.BoolProperty(default=False)
lc_sp: bpy.props.FloatVectorProperty(name="Location", default=(SP_LOC_X_DFLT, SP_LOC_Y_DFLT, SP_LOC_Z_DFLT), update=upd_sp)
rt_sp: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_sp)
v_sp_rd: bpy.props.FloatProperty(name="Radius", default=SP_RD_DFLT, update=upd_sp)
cl_sp: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_SP_DFLT, min=0, max=1, update=upd_sp)
b_aw: bpy.props.BoolProperty(default=False)
v_aw_cn: bpy.props.IntProperty(name="Count", default=AW_CNT_DFLT, min=1, max=100, update=upd_aw)
lc_aw_tg: bpy.props.FloatVectorProperty(name="Target Loc", default=(0,0,10), update=upd_aw)
v_aw_of: bpy.props.FloatProperty(name="Offset %", default=100.0, min=1, max=100, update=upd_aw)
v_aw_th: bpy.props.FloatProperty(name="Thickness", default=AW_THK_DFLT, update=upd_aw)
cl_aw: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_AW_DFLT, min=0, max=1, update=upd_aw)
b_dm: bpy.props.BoolProperty(default=False)
lc_dm: bpy.props.FloatVectorProperty(name="Location", default=(DM_LOC_X_DFLT, DM_LOC_Y_DFLT, DM_LOC_Z_DFLT), update=upd_dm)
rt_dm: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', update=upd_dm)
v_dm_rd: bpy.props.FloatProperty(name="Radius", default=DM_RD_DFLT, update=upd_dm)
v_dm_th: bpy.props.FloatProperty(name="Thickness", default=DM_THK_DFLT, update=upd_dm)
v_dm_ln: bpy.props.IntProperty(name="Longt Count", default=12, update=upd_dm)
v_dm_lt: bpy.props.IntProperty(name="Lat Count", default=6, update=upd_dm)
v_dm_sg: bpy.props.IntProperty(name="Segments", default=32, update=upd_dm)
cl_dm: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_DM_DFLT, min=0, max=1, update=upd_dm)
b_tx: bpy.props.BoolProperty(default=False)
s_tx_bd: bpy.props.StringProperty(name="Text", default=TEXT_BODY_DFLT, update=upd_tx)
v_tx_sz: bpy.props.FloatProperty(name="Size", default=TEXT_SIZE_DFLT, update=upd_tx)
lc_tx: bpy.props.FloatVectorProperty(name="Location", default=(TEXT_OFFSET_X_DFLT, TEXT_OFFSET_Y_DFLT, TEXT_OFFSET_Z_DFLT), update=upd_tx)
rt_tx: bpy.props.FloatVectorProperty(name="Rotation", subtype='EULER', default=(1.5708,0,0), update=upd_tx)
cl_tx: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=COLOR_TX_DFLT, min=0, max=1, update=upd_tx)
# =========================================================
# 10. オペレーター群
# =========================================================
class Op_Cr_Tr(bpy.types.Operator):
bl_idname = f"{PREFIX}.cr_tr"
bl_label = "トーラス生成"
def execute(self, cx):
cx.scene.tc_g5_prp.b_tr = True
upd_tr(cx.scene.tc_g5_prp, cx)
return {'FINISHED'}
class Op_Cr_Cn(bpy.types.Operator):
bl_idname = f"{PREFIX}.cr_cn"
bl_label = "円錐台生成"
def execute(self, cx):
cx.scene.tc_g5_prp.b_cn = True
upd_cn(cx.scene.tc_g5_prp, cx)
return {'FINISHED'}
class Op_Cr_Sp(bpy.types.Operator):
bl_idname = f"{PREFIX}.cr_sp"
bl_label = "球体生成"
def execute(self, cx):
cx.scene.tc_g5_prp.b_sp = True
upd_sp(cx.scene.tc_g5_prp, cx)
return {'FINISHED'}
class Op_Cr_Aw(bpy.types.Operator):
bl_idname = f"{PREFIX}.cr_aw"
bl_label = "集中矢印生成"
def execute(self, cx):
cx.scene.tc_g5_prp.b_aw = True
upd_aw(cx.scene.tc_g5_prp, cx)
return {'FINISHED'}
class Op_Cr_Dm(bpy.types.Operator):
bl_idname = f"{PREFIX}.cr_dm"
bl_label = "骨格ドーム生成"
def execute(self, cx):
cx.scene.tc_g5_prp.b_dm = True
upd_dm(cx.scene.tc_g5_prp, cx)
return {'FINISHED'}
class Op_Cr_Tx(bpy.types.Operator):
bl_idname = f"{PREFIX}.cr_tx"
bl_label = "テキスト生成"
def execute(self, cx):
cx.scene.tc_g5_prp.b_tx = True
upd_tx(cx.scene.tc_g5_prp, cx)
return {'FINISHED'}
class Op_Dt_Tr(bpy.types.Operator):
bl_idname = f"{PREFIX}.dt_tr"
bl_label = "独立"
def execute(self, cx):
dtch_ob(cx, f"{PREFIX_VAL}_Ob_Tr", "Detached_Torus", "b_tr")
return {'FINISHED'}
class Op_Dt_Cn(bpy.types.Operator):
bl_idname = f"{PREFIX}.dt_cn"
bl_label = "独立"
def execute(self, cx):
dtch_ob(cx, f"{PREFIX_VAL}_Ob_Cn", "Detached_Cone", "b_cn")
return {'FINISHED'}
class Op_Dt_Sp(bpy.types.Operator):
bl_idname = f"{PREFIX}.dt_sp"
bl_label = "独立"
def execute(self, cx):
dtch_ob(cx, f"{PREFIX_VAL}_Ob_Sp", "Detached_Sphere", "b_sp")
return {'FINISHED'}
class Op_Dt_Aw(bpy.types.Operator):
bl_idname = f"{PREFIX}.dt_aw"
bl_label = "独立"
def execute(self, cx):
dtch_ob(cx, f"{PREFIX_VAL}_Ob_Aw", "Detached_Arrows", "b_aw")
return {'FINISHED'}
class Op_Dt_Dm(bpy.types.Operator):
bl_idname = f"{PREFIX}.dt_dm"
bl_label = "独立"
def execute(self, cx):
dtch_ob(cx, f"{PREFIX_VAL}_Ob_Dm", "Detached_Dome", "b_dm")
return {'FINISHED'}
class Op_Dt_Tx(bpy.types.Operator):
bl_idname = f"{PREFIX}.dt_tx"
bl_label = "独立"
def execute(self, cx):
dtch_ob(cx, f"{PREFIX_VAL}_Ob_Tx", "Detached_Text", "b_tx")
return {'FINISHED'}
def dly_unrg():
try: addon_utils.disable(__name__)
except Exception: pass
try: unregister()
except Exception: pass
return None
class Op_Rm_Ad(bpy.types.Operator):
bl_idname = f"{PREFIX}.rm_ad"
bl_label = "Clean up & Uninstall"
def execute(self, cx):
c_dt = bpy.data.collections.get(f"{PREFIX_VAL}_Col")
if c_dt:
for ob in list(c_dt.objects): bpy.data.objects.remove(ob, do_unlink=True)
bpy.data.collections.remove(c_dt)
for m in list(bpy.data.meshes):
if m.name.startswith(PREFIX_VAL): bpy.data.meshes.remove(m, do_unlink=True)
for c in list(bpy.data.curves):
if c.name.startswith(PREFIX_VAL): bpy.data.curves.remove(c, do_unlink=True)
for mt in list(bpy.data.materials):
if mt.name.startswith(PREFIX_VAL): bpy.data.materials.remove(mt, do_unlink=True)
bpy.app.timers.register(dly_unrg, first_interval=0.2)
return {'FINISHED'}
class Op_Xp_Tx(bpy.types.Operator):
bl_idname = f"{PREFIX}.xp_tx"
bl_label = "設定をコードに書き出し"
def execute(self, cx):
if not hasattr(cx.scene, "tc_g5_prp"): return {'CANCELLED'}
p = cx.scene.tc_g5_prp
ss = ""
fp = globals().get("__file__")
if fp and fp.endswith(".pyc"): fp = fp[:-1]
if fp and os.path.exists(fp):
try:
with open(fp, "r", encoding="utf-8") as f: ss = f.read()
except Exception: pass
if not ss:
for tx in bpy.data.texts:
bd = tx.as_string()
# 誤爆防止のため文字列を分割結合して判定
if ("# " + "--- DEFAULTS_BLOCK_START ---") in bd:
ss = bd
break
if not ss: return {'CANCELLED'}
ds = "# " + "--- DEFAULTS_BLOCK_START ---\n\n"
ds += f"TR_MJ_RD_DFLT = {p.v_tr_mj}\nTR_MN_RD_DFLT = {p.v_tr_mn}\nCN_RD_TP_DFLT = {p.v_cn_tp}\nCN_RD_BT_DFLT = {p.v_cn_bt}\n"
ds += f"CN_HT_DFLT = {p.v_cn_ht}\nSP_RD_DFLT = {p.v_sp_rd}\nAW_CNT_DFLT = {p.v_aw_cn}\nAW_THK_DFLT = {p.v_aw_th}\n"
ds += f"DM_RD_DFLT = {p.v_dm_rd}\nDM_THK_DFLT = {p.v_dm_th}\n\n"
ds += f"TR_LOC_X_DFLT, TR_LOC_Y_DFLT, TR_LOC_Z_DFLT = {p.lc_tr[0]:.2f}, {p.lc_tr[1]:.2f}, {p.lc_tr[2]:.2f}\n"
ds += f"CN_LOC_X_DFLT, CN_LOC_Y_DFLT, CN_LOC_Z_DFLT = {p.lc_cn[0]:.2f}, {p.lc_cn[1]:.2f}, {p.lc_cn[2]:.2f}\n"
ds += f"SP_LOC_X_DFLT, SP_LOC_Y_DFLT, SP_LOC_Z_DFLT = {p.lc_sp[0]:.2f}, {p.lc_sp[1]:.2f}, {p.lc_sp[2]:.2f}\n"
ds += f"DM_LOC_X_DFLT, DM_LOC_Y_DFLT, DM_LOC_Z_DFLT = {p.lc_dm[0]:.2f}, {p.lc_dm[1]:.2f}, {p.lc_dm[2]:.2f}\n\n"
ds += f"TEXT_BODY_DFLT = \"{p.s_tx_bd}\"\nTEXT_SIZE_DFLT = {p.v_tx_sz}\n"
ds += f"TEXT_OFFSET_X_DFLT, TEXT_OFFSET_Y_DFLT, TEXT_OFFSET_Z_DFLT = {p.lc_tx[0]:.2f}, {p.lc_tx[1]:.2f}, {p.lc_tx[2]:.2f}\n\n"
ds += f"COLOR_TR_DFLT = ({p.cl_tr[0]:.4f}, {p.cl_tr[1]:.4f}, {p.cl_tr[2]:.4f}, {p.cl_tr[3]:.4f})\n"
ds += f"COLOR_CN_FR_DFLT = ({p.cl_cn_fr[0]:.4f}, {p.cl_cn_fr[1]:.4f}, {p.cl_cn_fr[2]:.4f}, {p.cl_cn_fr[3]:.4f})\n"
ds += f"COLOR_CN_BK_DFLT = ({p.cl_cn_bk[0]:.4f}, {p.cl_cn_bk[1]:.4f}, {p.cl_cn_bk[2]:.4f}, {p.cl_cn_bk[3]:.4f})\n"
ds += f"COLOR_SP_DFLT = ({p.cl_sp[0]:.4f}, {p.cl_sp[1]:.4f}, {p.cl_sp[2]:.4f}, {p.cl_sp[3]:.4f})\n"
ds += f"COLOR_AW_DFLT = ({p.cl_aw[0]:.4f}, {p.cl_aw[1]:.4f}, {p.cl_aw[2]:.4f}, {p.cl_aw[3]:.4f})\n"
ds += f"COLOR_DM_DFLT = ({p.cl_dm[0]:.4f}, {p.cl_dm[1]:.4f}, {p.cl_dm[2]:.4f}, {p.cl_dm[3]:.4f})\n"
ds += f"COLOR_TX_DFLT = ({p.cl_tx[0]:.4f}, {p.cl_tx[1]:.4f}, {p.cl_tx[2]:.4f}, {p.cl_tx[3]:.4f})\n\n"
# エラー回避のための正規表現パターン分割
m1 = "# " + "--- DEFAULTS_BLOCK_START ---"
m2 = "# " + "--- DEFAULTS_BLOCK_END ---"
pr = r"(" + m1 + r"\n)(.*?)(" + m2 + r")"
ns = re.sub(pr, r"\g<1>" + ds + r"\g<3>", ss, flags=re.DOTALL)
tn = "Exp_TCS_G5.py"
nb = bpy.data.texts.get(tn)
if nb: nb.clear()
else: nb = bpy.data.texts.new(tn)
nb.write(ns)
sf = False
for a in cx.screen.areas:
if a.type == 'TEXT_EDITOR':
a.spaces.active.text = nb
sf = True
break
if sf: self.report({'INFO'}, f"書き出し成功: {tn}")
else: self.report({'INFO'}, f"内部データに保存: {tn}")
return {'FINISHED'}
# =========================================================
# 11. UIパネル
# =========================================================
class Pn_Bs(bpy.types.Panel):
bl_space_type, bl_region_type, bl_category = 'VIEW_3D', 'UI', ADDON_CATEGORY
class Pn_Tr(Pn_Bs):
bl_label, bl_idname, bl_order = "1. トーラス", f"{PREFIX_VAL}_P_Tr", 1
def draw(self, cx):
ly, p = self.layout, cx.scene.tc_g5_prp
c = ly.column(align=True)
c.prop(p, "lc_tr"); c.prop(p, "rt_tr"); c.separator()
c.prop(p, "v_tr_mj"); c.prop(p, "v_tr_mn"); c.prop(p, "cl_tr"); ly.separator()
r = ly.row(align=True)
if not p.b_tr: r.operator(Op_Cr_Tr.bl_idname, icon='MESH_TORUS')
else:
r.operator(Op_Cr_Tr.bl_idname, text="更新", icon='FILE_REFRESH')
r.operator(Op_Dt_Tr.bl_idname, icon='UNLINKED')
class Pn_Cn(Pn_Bs):
bl_label, bl_idname, bl_order = "2. 円錐台", f"{PREFIX_VAL}_P_Cn", 2
def draw(self, cx):
ly, p = self.layout, cx.scene.tc_g5_prp
c = ly.column(align=True)
c.prop(p, "lc_cn"); c.prop(p, "rt_cn"); c.separator()
r1 = c.row(align=True); r1.prop(p, "v_cn_tp"); r1.prop(p, "v_cn_bt")
c.prop(p, "v_cn_ht"); c.separator()
r2 = c.row(align=True); r2.prop(p, "b_cn_cp"); r2.prop(p, "b_cn_fl"); c.separator()
r3 = c.row(align=True); r3.prop(p, "cl_cn_fr", text="表面"); r3.prop(p, "cl_cn_bk", text="裏面"); ly.separator()
r = ly.row(align=True)
if not p.b_cn: r.operator(Op_Cr_Cn.bl_idname, icon='MESH_CONE')
else:
r.operator(Op_Cr_Cn.bl_idname, text="更新", icon='FILE_REFRESH')
r.operator(Op_Dt_Cn.bl_idname, icon='UNLINKED')
class Pn_Sp(Pn_Bs):
bl_label, bl_idname, bl_order = "3. 球体", f"{PREFIX_VAL}_P_Sp", 3
def draw(self, cx):
ly, p = self.layout, cx.scene.tc_g5_prp
c = ly.column(align=True)
c.prop(p, "lc_sp"); c.prop(p, "rt_sp"); c.separator()
c.prop(p, "v_sp_rd"); c.prop(p, "cl_sp"); ly.separator()
r = ly.row(align=True)
if not p.b_sp: r.operator(Op_Cr_Sp.bl_idname, icon='MESH_UVSPHERE')
else:
r.operator(Op_Cr_Sp.bl_idname, text="更新", icon='FILE_REFRESH')
r.operator(Op_Dt_Sp.bl_idname, icon='UNLINKED')
class Pn_Aw(Pn_Bs):
bl_label, bl_idname, bl_order = "4. 集中矢印", f"{PREFIX_VAL}_P_Aw", 4
def draw(self, cx):
ly, p = self.layout, cx.scene.tc_g5_prp
c = ly.column(align=True)
c.prop(p, "v_aw_cn"); c.prop(p, "lc_aw_tg"); c.prop(p, "v_aw_of"); c.separator()
c.prop(p, "v_aw_th"); c.prop(p, "cl_aw"); ly.separator()
r = ly.row(align=True)
if not p.b_aw: r.operator(Op_Cr_Aw.bl_idname, icon='TRACKING')
else:
r.operator(Op_Cr_Aw.bl_idname, text="更新", icon='FILE_REFRESH')
r.operator(Op_Dt_Aw.bl_idname, icon='UNLINKED')
class Pn_Dm(Pn_Bs):
bl_label, bl_idname, bl_order = "5. 骨格ドーム", f"{PREFIX_VAL}_P_Dm", 5
def draw(self, cx):
ly, p = self.layout, cx.scene.tc_g5_prp
c = ly.column(align=True)
c.prop(p, "lc_dm"); c.prop(p, "rt_dm"); c.separator()
c.prop(p, "v_dm_rd"); c.prop(p, "v_dm_th"); c.separator()
r1 = c.row(align=True); r1.prop(p, "v_dm_ln"); r1.prop(p, "v_dm_lt")
c.prop(p, "v_dm_sg"); c.separator(); c.prop(p, "cl_dm"); ly.separator()
r = ly.row(align=True)
if not p.b_dm: r.operator(Op_Cr_Dm.bl_idname, icon='MESH_UVSPHERE')
else:
r.operator(Op_Cr_Dm.bl_idname, text="更新", icon='FILE_REFRESH')
r.operator(Op_Dt_Dm.bl_idname, icon='UNLINKED')
class Pn_Tx(Pn_Bs):
bl_label, bl_idname, bl_order = "6. 3Dテキスト", f"{PREFIX_VAL}_P_Tx", 6
def draw(self, cx):
ly, p = self.layout, cx.scene.tc_g5_prp
c = ly.column(align=True)
c.prop(p, "s_tx_bd"); c.prop(p, "v_tx_sz"); c.separator()
c.prop(p, "lc_tx"); c.prop(p, "rt_tx"); c.separator(); c.prop(p, "cl_tx"); ly.separator()
r = ly.row(align=True)
if not p.b_tx: r.operator(Op_Cr_Tx.bl_idname, icon='FONT_DATA')
else:
r.operator(Op_Cr_Tx.bl_idname, text="更新", icon='FILE_REFRESH')
r.operator(Op_Dt_Tx.bl_idname, icon='UNLINKED')
class Pn_Rm(Pn_Bs):
bl_label, bl_idname, bl_order, bl_options = "管理 & エクスポート", f"{PREFIX_VAL}_P_Rm", 10, {'DEFAULT_CLOSED'}
def draw(self, cx):
bx = self.layout.box()
bx.operator(Op_Xp_Tx.bl_idname, icon='TEXT')
bx.separator()
bx.operator(Op_Rm_Ad.bl_idname, icon='CANCEL')
# =========================================================
# 12. 起動時自動展開処理
# =========================================================
def ui_st_up():
try:
cx = bpy.context
if not cx or not cx.window_manager.windows: return 0.2
for w in cx.window_manager.windows:
for a in w.screen.areas:
if a.type == 'VIEW_3D':
for s in a.spaces:
if s.type == 'VIEW_3D':
s.shading.type = 'MATERIAL'
s.show_region_ui = True
except Exception: pass
return None
cls_ls = [
TC_G5_Prp, Op_Cr_Tr, Op_Cr_Cn, Op_Cr_Sp, Op_Cr_Aw, Op_Cr_Dm, Op_Cr_Tx,
Op_Dt_Tr, Op_Dt_Cn, Op_Dt_Sp, Op_Dt_Aw, Op_Dt_Dm, Op_Dt_Tx,
Op_Rm_Ad, Op_Xp_Tx,
Pn_Tr, Pn_Cn, Pn_Sp, Pn_Aw, Pn_Dm, Pn_Tx, Pn_Rm
]
def register():
for c in cls_ls: bpy.utils.register_class(c)
bpy.types.Scene.tc_g5_prp = bpy.props.PointerProperty(type=TC_G5_Prp)
if not bpy.app.timers.is_registered(ui_st_up): bpy.app.timers.register(ui_st_up, first_interval=0.2)
def unregister():
try: del bpy.types.Scene.tc_g5_prp
except Exception: pass
for c in reversed(cls_ls):
try: bpy.utils.unregister_class(c)
except RuntimeError: pass
if bpy.app.timers.is_registered(ui_st_up): bpy.app.timers.unregister(ui_st_up)
if __name__ == "__main__": register()
# =========================================================
# 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()