blender 5以降 専用
20261003 円錐頂点矢印 と 球体

bl_info = {
"name": "Tangent Cone Generator",
"author": "AI",
"version": (1, 8, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar > Tangent Cone",
"description": "円錐と矢印(1〜12本)の切り替え機能を搭載",
"category": "Add Mesh",
}
import bpy
import bmesh
import math
import webbrowser
import random
from mathutils import Vector, Matrix
from bpy.app.handlers import persistent
# ==========================================
# ユーザー設定用パラメーター (ここを書き換えるだけで初期値やリンクを変更できます)
# ==========================================
# ▼ リンク一覧(辞書化)
ADDON_LINKS = {
"338 Sphere Cam": "<https://app.notion.com/p/338-Sphere-Cam-3c0347b3454a80abbb61ff2cd585792c>",
"posfie": "<https://posfie.com/@timekagura/t/zionad2022?sort=0>",
"zionadchat": "<https://x.com/zionadchat>",
"背後霊カメラ 最新版リンク": "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>",
"blender コンテナ 20260713": "<https://note.com/zionadmillion/n/n9a47d88fe7b4>",
"骨格立方体": "<https://app.notion.com/p/351-2017-3cb347b3454a8086b15fec2f69408bd5>",
}
# ▼ 形状の初期値
INIT_APEX_LOC = (0.0, 0.0, 10.0)
INIT_RADIUS = 1.0
INIT_SEGMENTS = 128
INIT_DRAW_MODE = 'CONE' # 'CONE' (円錐) または 'ARROWS' (矢印)
INIT_ARROW_CNT = 4 # 矢印の初期本数 (1〜12)
# ▼ マテリアルの初期値
INIT_SPHERE_COLOR = (0.2, 0.6, 1.0)
INIT_SPHERE_ALPHA = 0.35
INIT_CONE_COLOR = (1.0, 0.5, 0.1)
INIT_CONE_ALPHA = 0.25
# ==========================================
# 起動時の自動セットアップ
# ==========================================
@persistent
def setup_workspace_handler(dummy=None):
def setup():
if not bpy.context.window_manager:
return 0.1
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.show_region_ui = True
space.shading.type = 'MATERIAL'
return None
bpy.app.timers.register(setup, first_interval=0.1)
# ==========================================
# ユーティリティ関数
# ==========================================
def get_or_create_collection(name):
coll = bpy.data.collections.get(name)
if not coll:
coll = bpy.data.collections.new(name)
bpy.context.scene.collection.children.link(coll)
return coll
def get_or_create_object(name, coll):
obj = bpy.data.objects.get(name)
if not obj:
mesh = bpy.data.meshes.new(f"{name}_Mesh")
obj = bpy.data.objects.new(name, mesh)
coll.objects.link(obj)
else:
if obj.name not in coll.objects:
coll.objects.link(obj)
if not obj.data or not isinstance(obj.data, bpy.types.Mesh):
obj.data = bpy.data.meshes.new(f"{name}_Mesh")
return obj
def assign_material(obj, name, color, alpha, blend='BLEND'):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
mat.blend_method = blend
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = (*color[:3], 1.0)
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = alpha
if "Transmission Weight" in bsdf.inputs:
bsdf.inputs["Transmission Weight"].default_value = 0.1
if not obj.data.materials:
obj.data.materials.append(mat)
else:
obj.data.materials[0] = mat
def get_plane_eq(N, Cc):
nx, ny, nz = N.x, N.y, N.z
d = N.dot(Cc)
terms = []
if abs(nx) > 1e-4: terms.append(f"{nx:.2f}x")
if abs(ny) > 1e-4: terms.append(f"{ny:.2f}y")
if abs(nz) > 1e-4: terms.append(f"{nz:.2f}z")
if not terms: terms.append("0")
return " + ".join(terms).replace("+ -", "- ") + f" = {d:.2f}"
def get_sphere_eq(Cc, rc):
def fmt_term(axis, val):
if abs(val) < 1e-4: return f"{axis}^2"
if val < 0: return f"({axis} + {abs(val):.2f})^2"
return f"({axis} - {val:.2f})^2"
return f"{fmt_term('x', Cc.x)} + {fmt_term('y', Cc.y)} + {fmt_term('z', Cc.z)} = {(rc*rc):.2f}"
# ==========================================
# リアルタイム生成コアロジック
# ==========================================
def update_tangent_cone_logic(self, context=None):
if not getattr(self, "is_calculated", False):
return False
P = Vector(self.apex_loc)
C = Vector((0.0, 0.0, 0.0))
R = self.radius
segments = self.segments
V = P - C
d = V.length
if d <= R:
self.res_error = "エラー: 頂点が球の内部または表面にあります"
return False
self.res_error = ""
zc = (R * R) / d
rc = R * math.sqrt(d * d - R * R) / d
N = V.normalized()
Cc = C + N * zc
cone_h = d - zc
tmp = Vector((1, 0, 0)) if abs(N.x) < 0.9 else Vector((0, 1, 0))
U = N.cross(tmp).normalized()
W = N.cross(U).normalized()
circle_verts = []
for i in range(segments):
theta = 2 * math.pi * i / segments
circle_verts.append(Cc + rc * (U * math.cos(theta) + W * math.sin(theta)))
coll = get_or_create_collection("Tangent Cone Setup")
# 1. Base Sphere
obj_sphere = get_or_create_object("Sphere_Base", coll)
bm_sphere = bmesh.new()
bmesh.ops.create_uvsphere(bm_sphere, u_segments=64, v_segments=32, radius=R)
for f in bm_sphere.faces: f.smooth = True
bm_sphere.to_mesh(obj_sphere.data)
bm_sphere.free()
assign_material(obj_sphere, "Mat_Sphere", self.sphere_color, self.sphere_alpha)
# 2. Tangent Cone または 矢印(Arrows)
obj_cone = get_or_create_object("Cone_Tangent", coll)
bm_cone = bmesh.new()
if self.draw_mode == 'CONE':
# --- 通常の円錐スカート ---
apex_v = bm_cone.verts.new(P)
cv = [bm_cone.verts.new(p) for p in circle_verts]
bm_cone.verts.ensure_lookup_table()
for i in range(segments):
bm_cone.faces.new((apex_v, cv[i], cv[(i+1)%segments]))
for f in bm_cone.faces: f.smooth = True
else:
# --- 矢印の生成 ---
arrows = self.arrow_count
r_shaft = R * 0.02
r_head = R * 0.06
for i in range(arrows):
theta = 2 * math.pi * i / arrows
v_target = Cc + rc * (U * math.cos(theta) + W * math.sin(theta))
vec = v_target - P
L = vec.length
if L > 1e-4:
dir = vec.normalized()
rot_mat = Vector((0,0,1)).rotation_difference(dir).to_matrix().to_4x4()
# 軸 (Cylinder)
L_shaft = L * 0.85
ret = bmesh.ops.create_cone(bm_cone, cap_ends=True, cap_tris=False, segments=16, radius1=r_shaft, radius2=r_shaft, depth=L_shaft)
bmesh.ops.transform(bm_cone, matrix=rot_mat, verts=ret['verts'])
bmesh.ops.transform(bm_cone, matrix=Matrix.Translation(P + dir*(L_shaft/2)), verts=ret['verts'])
# 先端 (Cone)
L_head = L * 0.15
ret_head = bmesh.ops.create_cone(bm_cone, cap_ends=True, cap_tris=False, segments=16, radius1=r_head, radius2=0.0, depth=L_head)
bmesh.ops.transform(bm_cone, matrix=rot_mat, verts=ret_head['verts'])
bmesh.ops.transform(bm_cone, matrix=Matrix.Translation(P + dir*(L_shaft + L_head/2)), verts=ret_head['verts'])
for f in bm_cone.faces: f.smooth = True
bm_cone.to_mesh(obj_cone.data)
bm_cone.free()
assign_material(obj_cone, "Mat_Cone", self.cone_color, self.cone_alpha)
# 3. Contact Circle
obj_circle = get_or_create_object("ContactCircle", coll)
bm_circle = bmesh.new()
cv_circ = [bm_circle.verts.new(p) for p in circle_verts]
bm_circle.verts.ensure_lookup_table()
for i in range(segments):
bm_circle.edges.new((cv_circ[i], cv_circ[(i+1)%segments]))
bm_circle.to_mesh(obj_circle.data)
bm_circle.free()
obj_circle.show_wire = True
assign_material(obj_circle, "Mat_Circle", (1.0, 0.05, 0.05), 1.0, blend='OPAQUE')
# 4. Spherical Cap
obj_cap = get_or_create_object("SphericalCap", coll)
bm_cap = bmesh.new()
bmesh.ops.create_uvsphere(bm_cap, u_segments=64, v_segments=32, radius=R)
bm_cap.verts.ensure_lookup_table()
to_remove = [v for v in bm_cap.verts if (v.co - Cc).dot(N) < -1e-6]
bmesh.ops.delete(bm_cap, geom=to_remove, context='VERTS')
for f in bm_cap.faces: f.smooth = True
bm_cap.to_mesh(obj_cap.data)
bm_cap.free()
assign_material(obj_cap, "Mat_Cap", self.sphere_color, self.sphere_alpha)
for obj in [obj_sphere, obj_cone, obj_circle, obj_cap]:
obj.data.update()
self.res_apex_z = f"{P.z:.2f}"
self.res_cone_h = f"{cone_h:.2f}"
self.res_circle_z = f"{Cc.z:.2f}"
self.res_circle_r = f"{rc:.2f}"
self.res_circle_circum = f"{(2 * math.pi * rc):.2f}"
self.res_plane_eq = get_plane_eq(N, Cc)
self.res_sphere_eq = get_sphere_eq(Cc, rc)
return True
# ==========================================
# オペレーター
# ==========================================
class TANGENTCONE_OT_init(bpy.types.Operator):
bl_idname = "mesh.tangent_cone_init"
bl_label = "作成 (新規生成)"
def execute(self, context):
props = context.scene.tangent_cone_props
props.is_calculated = True
success = update_tangent_cone_logic(props, context)
if not success:
props.is_calculated = False
self.report({'WARNING'}, props.res_error)
return {'FINISHED'}
class TANGENTCONE_OT_detach(bpy.types.Operator):
bl_idname = "mesh.tangent_cone_detach"
bl_label = "切り離し (独立化)"
bl_description = "現在の生成物を独立させ、マテリアルの色なども固定して保持します"
def execute(self, context):
props = context.scene.tangent_cone_props
suffix = f"_Fixed_{random.randint(1000, 9999)}"
names = ["Sphere_Base", "Cone_Tangent", "ContactCircle", "SphericalCap"]
mat_names = ["Mat_Sphere", "Mat_Cone", "Mat_Circle", "Mat_Cap"]
coll = bpy.data.collections.get("Tangent Cone Setup")
if not coll:
self.report({'WARNING'}, "切り離す対象が見つかりません。")
props.is_calculated = False
return {'CANCELLED'}
fixed_coll = bpy.data.collections.get("Tangent Cone Fixed")
if not fixed_coll:
fixed_coll = bpy.data.collections.new("Tangent Cone Fixed")
context.scene.collection.children.link(fixed_coll)
for name in names:
obj = bpy.data.objects.get(name)
if obj:
if coll.name in [c.name for c in obj.users_collection]:
coll.objects.unlink(obj)
if fixed_coll.name not in [c.name for c in obj.users_collection]:
fixed_coll.objects.link(obj)
obj.name = name + suffix
if obj.data:
obj.data.name = obj.data.name + suffix
if obj.data.materials:
for slot in obj.material_slots:
if slot.material and slot.material.name in mat_names:
slot.material.name = slot.material.name + suffix
props.is_calculated = False
self.report({'INFO'}, f"切り離し完了。設定を変えて新しく「作成」できます。")
return {'FINISHED'}
class TANGENTCONE_OT_copy_text(bpy.types.Operator):
bl_idname = "wm.copy_tangent_cone_text"
bl_label = "Copy"
text_to_copy: bpy.props.StringProperty()
def execute(self, context):
context.window_manager.clipboard = self.text_to_copy
self.report({'INFO'}, f"コピーしました: {self.text_to_copy}")
return {'FINISHED'}
class TANGENTCONE_OT_copy_all(bpy.types.Operator):
bl_idname = "wm.copy_tangent_cone_all"
bl_label = "全結果をコピー"
def execute(self, context):
props = context.scene.tangent_cone_props
lines = [
f"【計算結果】",
f"頂点のZ座標: {props.res_apex_z}",
f"円錐の高さ: {props.res_cone_h}",
f"接円のZ座標: {props.res_circle_z}",
f"接円の半径: {props.res_circle_r}",
f"接円の円周: {props.res_circle_circum}",
f"",
f"【接する円の方程式 (連立)】",
f"平面: {props.res_plane_eq}",
f"球面: {props.res_sphere_eq}",
]
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "すべての結果をコピーしました")
return {'FINISHED'}
class TANGENTCONE_OT_open_url(bpy.types.Operator):
bl_idname = "wm.tangent_cone_open_url"
bl_label = "リンクを開く"
url: bpy.props.StringProperty()
def execute(self, context):
if self.url: webbrowser.open(self.url)
return {'FINISHED'}
class TANGENTCONE_OT_remove_addon(bpy.types.Operator):
bl_idname = "wm.tangent_cone_remove_addon"
bl_label = "アドオン削除"
def execute(self, context):
unregister()
return {'FINISHED'}
# ==========================================
# プロパティ設定 (初期値に定数を代入)
# ==========================================
class TangentConeProperties(bpy.types.PropertyGroup):
apex_loc: bpy.props.FloatVectorProperty(name="Apex (X,Y,Z)", default=INIT_APEX_LOC, subtype='XYZ', update=update_tangent_cone_logic)
radius: bpy.props.FloatProperty(name="Sphere Radius", default=INIT_RADIUS, min=0.001, update=update_tangent_cone_logic)
segments: bpy.props.IntProperty(name="Segments", default=INIT_SEGMENTS, min=16, max=512, update=update_tangent_cone_logic)
# ▼ 追加:円錐と矢印の選択プロパティ
draw_mode: bpy.props.EnumProperty(
name="描画モード",
items=[
('CONE', "円錐スカート", "従来の円錐を生成"),
('ARROWS', "矢印 (放射状)", "頂点から接円へ向かう矢印を生成")
],
default=INIT_DRAW_MODE,
update=update_tangent_cone_logic
)
arrow_count: bpy.props.IntProperty(
name="矢印の本数", default=INIT_ARROW_CNT, min=1, max=12, update=update_tangent_cone_logic
)
sphere_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=INIT_SPHERE_COLOR, min=0.0, max=1.0, update=update_tangent_cone_logic)
sphere_alpha: bpy.props.FloatProperty(name="透明度", default=INIT_SPHERE_ALPHA, min=0.0, max=1.0, update=update_tangent_cone_logic)
cone_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=INIT_CONE_COLOR, min=0.0, max=1.0, update=update_tangent_cone_logic)
cone_alpha: bpy.props.FloatProperty(name="透明度", default=INIT_CONE_ALPHA, min=0.0, max=1.0, update=update_tangent_cone_logic)
is_calculated: bpy.props.BoolProperty(default=False)
res_error: bpy.props.StringProperty(default="")
res_apex_z: bpy.props.StringProperty()
res_cone_h: bpy.props.StringProperty()
res_circle_z: bpy.props.StringProperty()
res_circle_r: bpy.props.StringProperty()
res_circle_circum: bpy.props.StringProperty()
res_plane_eq: bpy.props.StringProperty()
res_sphere_eq: bpy.props.StringProperty()
# ==========================================
# UIパネル群
# ==========================================
class VIEW3D_PT_tangent_cone(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Tangent Cone'
bl_label = "Tangent Cone Generator"
def draw(self, context):
layout = self.layout
props = context.scene.tangent_cone_props
# 形状設定
col = layout.column(align=True)
col.prop(props, "apex_loc")
col.prop(props, "radius")
col.prop(props, "segments")
layout.separator()
# 描画モードの切り替え (ボタンスタイル)
box_mode = layout.box()
box_mode.label(text="生成形状の選択", icon='MODIFIER')
box_mode.prop(props, "draw_mode", expand=True)
if props.draw_mode == 'ARROWS':
box_mode.prop(props, "arrow_count")
layout.separator()
# マテリアル設定
box_mat = layout.box()
box_mat.label(text="マテリアル (動的反映)", icon='MATERIAL')
row_sphere = box_mat.row(align=True)
row_sphere.label(text="球体:")
row_sphere.prop(props, "sphere_color", text="")
row_sphere.prop(props, "sphere_alpha", slider=True)
row_cone = box_mat.row(align=True)
row_cone.label(text="円錐/矢印:")
row_cone.prop(props, "cone_color", text="")
row_cone.prop(props, "cone_alpha", slider=True)
layout.separator()
# 作成 / 切り離し ボタン
if not props.is_calculated:
layout.operator("mesh.tangent_cone_init", text="作成 (新規生成)", icon='MESH_CONE')
else:
layout.operator("mesh.tangent_cone_detach", text="切り離し (独立化)", icon='UNLINKED')
if props.res_error:
layout.separator()
layout.label(text=props.res_error, icon='ERROR')
return
# 計算結果
if props.is_calculated:
layout.separator()
box = layout.box()
box.label(text="【計算結果】", icon='INFO')
def draw_result(parent, label_text, val_text):
row = parent.row(align=True)
split = row.split(factor=0.45)
split.label(text=label_text)
sub = split.row(align=True)
sub.label(text=val_text)
op = sub.operator("wm.copy_tangent_cone_text", text="", icon='COPYDOWN')
op.text_to_copy = val_text
col = box.column(align=True)
draw_result(col, "頂点の Z座標", props.res_apex_z)
draw_result(col, "高さ (頂点〜円)", props.res_cone_h)
draw_result(col, "接円の Z座標", props.res_circle_z)
draw_result(col, "接円の半径", props.res_circle_r)
draw_result(col, "接円の円周長", props.res_circle_circum)
col.separator()
col.label(text="【接する円の方程式】")
draw_result(col, "平面", props.res_plane_eq)
draw_result(col, "球面", props.res_sphere_eq)
col.separator()
col.operator("wm.copy_tangent_cone_all", text="すべての結果をコピー", icon='DOCUMENTS')
class VIEW3D_PT_tangent_cone_links(bpy.types.Panel):
bl_label = "Links"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Tangent Cone'
def draw(self, context):
layout = self.layout
for name, url in ADDON_LINKS.items():
if name and url:
layout.operator("wm.tangent_cone_open_url", text=name, icon='URL').url = url
class VIEW3D_PT_tangent_cone_remove(bpy.types.Panel):
bl_label = "Addon Remove"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Tangent Cone'
def draw(self, context):
layout = self.layout
layout.operator("wm.tangent_cone_remove_addon", icon='CANCEL')
# ==========================================
# 登録処理
# ==========================================
classes = (
TANGENTCONE_OT_init,
TANGENTCONE_OT_detach,
TANGENTCONE_OT_copy_text,
TANGENTCONE_OT_copy_all,
TANGENTCONE_OT_open_url,
TANGENTCONE_OT_remove_addon,
TangentConeProperties,
VIEW3D_PT_tangent_cone,
VIEW3D_PT_tangent_cone_links,
VIEW3D_PT_tangent_cone_remove,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.tangent_cone_props = bpy.props.PointerProperty(type=TangentConeProperties)
bpy.app.handlers.load_post.append(setup_workspace_handler)
setup_workspace_handler()
def unregister():
for cls in reversed(classes):
try:
bpy.utils.unregister_class(cls)
except:
pass
if hasattr(bpy.types.Scene, "tangent_cone_props"):
del bpy.types.Scene.tangent_cone_props
if setup_workspace_handler in bpy.app.handlers.load_post:
bpy.app.handlers.load_post.remove(setup_workspace_handler)
if __name__ == "__main__":
try: unregister()
except: pass
register()