Sidebar > Crystal Torus", "description": "初期値反映版: トーラスと球体マーカーの個別表示/非表示機能付き", "category": "3D View", } # ========================================================= # 2. コード作成日時 # ========================================================= # Code Created : 2026_0723_1920 # ========================================================= # 3. import # ========================================================= import bpy import bmesh import math import mathutils import webbrowser # ========================================================= # 4. 共通定義 # ========================================================= ADDON_TITLE = "Crystal Torus System" PREFIX_VAL = "CRYSTAL_TORUS" PREFIX = PREFIX_VAL"> Sidebar > Crystal Torus", "description": "初期値反映版: トーラスと球体マーカーの個別表示/非表示機能付き", "category": "3D View", } # ========================================================= # 2. コード作成日時 # ========================================================= # Code Created : 2026_0723_1920 # ========================================================= # 3. import # ========================================================= import bpy import bmesh import math import mathutils import webbrowser # ========================================================= # 4. 共通定義 # ========================================================= ADDON_TITLE = "Crystal Torus System" PREFIX_VAL = "CRYSTAL_TORUS" PREFIX = PREFIX_VAL"> Sidebar > Crystal Torus", "description": "初期値反映版: トーラスと球体マーカーの個別表示/非表示機能付き", "category": "3D View", } # ========================================================= # 2. コード作成日時 # ========================================================= # Code Created : 2026_0723_1920 # ========================================================= # 3. import # ========================================================= import bpy import bmesh import math import mathutils import webbrowser # ========================================================= # 4. 共通定義 # ========================================================= ADDON_TITLE = "Crystal Torus System" PREFIX_VAL = "CRYSTAL_TORUS" PREFIX = PREFIX_VAL">
# =========================================================
# 1. bl_info
# =========================================================
bl_info = {
"name": "Crystal Torus & Center Markers (Ver 1.8)",
"author": "ChatGPT",
"version": (1, 8, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar > Crystal Torus",
"description": "初期値反映版: トーラスと球体マーカーの個別表示/非表示機能付き",
"category": "3D View",
}
# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0723_1920
# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import mathutils
import webbrowser
# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "Crystal Torus System"
PREFIX_VAL = "CRYSTAL_TORUS"
PREFIX = PREFIX_VAL.lower()
ADDON_CATEGORY = "Crystal Torus"
# =========================================================
# 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. 初期設定
# =========================================================
SCALE_FACTOR_DEFAULT = 1.0
CRYSTAL_MAJOR_DEFAULT = 0.10
CRYSTAL_MINOR_DEFAULT = 0.01
CRYSTAL_LOC_DEFAULT = (0.0, 0.0, 0.0)
COLOR_CRYSTAL_DEFAULT = (0.6, 0.9, 1.0, 0.5)
CRYSTAL_SHOW_TORUS = True
CRYSTAL_SHOW_CENTER = True
CRYSTAL_CENTER_RADIUS_DEFAULT = 0.02
COLOR_CRYSTAL_CENTER_DEFAULT = (0.4, 0.5, 1.0, 1.0)
RETINA_MAJOR_DEFAULT = 1.00
RETINA_MINOR_DEFAULT = 0.01
RETINA_LOC_DEFAULT = (1.10, 0.0, 0.0)
COLOR_RETINA_DEFAULT = (1.0, 0.2, 0.5, 1.0)
RETINA_SHOW_TORUS = True
RETINA_SHOW_CENTER = True
RETINA_CENTER_RADIUS_DEFAULT = 0.20
COLOR_RETINA_CENTER_DEFAULT = (1.0, 0.3, 0.5, 1.0)
SURFACE_MAJOR_DEFAULT = 1.00
SURFACE_MINOR_DEFAULT = 0.01
COLOR_SURFACE_DEFAULT = (0.5, 1.0, 0.5, 0.5)
SURFACE_SHOW_TORUS = True
SURFACE_SHOW_CENTER = True
SURFACE_CENTER_RADIUS_DEFAULT = 0.02
COLOR_SURFACE_CENTER_DEFAULT = (0.8, 0.0, 1.0, 1.0)
GAMMA_MAJOR_DEFAULT = 10.00
GAMMA_MINOR_DEFAULT = 0.05
GAMMA_LOC_DEFAULT = (0.0, 0.0, 0.0)
COLOR_GAMMA_DEFAULT = (0.0, 0.0, 1.0, 0.5)
GAMMA_SHOW_TORUS = True
GAMMA_SHOW_CENTER = True
GAMMA_CENTER_RADIUS_DEFAULT = 0.00
COLOR_GAMMA_CENTER_DEFAULT = (0.2, 0.4, 1.0, 1.0)
# =========================================================
# 7. 共通クラス
# =========================================================
class CRYSTAL_OT_OpenUrl(bpy.types.Operator):
bl_idname = f"{PREFIX}.open_url"
bl_label = "リンクを開く"
url: bpy.props.StringProperty()
def execute(self, context):
if self.url: webbrowser.open(self.url)
return {'FINISHED'}
class CRYSTAL_OT_RemoveAddon(bpy.types.Operator):
bl_idname = f"{PREFIX}.remove_addon"
bl_label = "Unregister Addon"
bl_description = "このアドオンを無効化し、UIから消去します"
def execute(self, context):
bpy.app.timers.register(unregister, first_interval=0.01)
self.report({'INFO'}, f"{ADDON_TITLE} を UI から消去しました")
return {'FINISHED'}
# =========================================================
# 8. アドオン固有機能 (リアルタイム・生成・切り離し)
# =========================================================
def create_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
if color[3] < 1.0:
bsdf.inputs["Alpha"].default_value = color[3]
mat.blend_method = 'BLEND' if bpy.app.version < (4, 2, 0) else mat.blend_method
else:
bsdf.inputs["Alpha"].default_value = 1.0
return mat
def apply_torus_bmesh(obj, major, minor):
bm = bmesh.new()
scaled_major = major * SCALE_FACTOR_DEFAULT
scaled_minor = minor * SCALE_FACTOR_DEFAULT
if scaled_major <= 0.0001: scaled_major = 0.0001
if scaled_minor <= 0.0001: scaled_minor = 0.0001
segments, rings = 48, 24
verts = []
for i in range(segments):
theta = 2.0 * math.pi * i / segments
cos_theta = math.cos(theta)
sin_theta = math.sin(theta)
row = []
for j in range(rings):
phi = 2.0 * math.pi * j / rings
x = (scaled_major + scaled_minor * math.cos(phi)) * cos_theta
z = (scaled_major + scaled_minor * math.cos(phi)) * sin_theta
y = scaled_minor * math.sin(phi)
row.append(bm.verts.new((x, y, z)))
verts.append(row)
for i in range(segments):
next_i = (i + 1) % segments
for j in range(rings):
next_j = (j + 1) % rings
f = bm.faces.new((verts[i][j], verts[i][next_j], verts[next_i][next_j], verts[next_i][j]))
f.smooth = True
bm.to_mesh(obj.data)
bm.free()
def apply_center_bmesh(obj, radius):
bm = bmesh.new()
scaled_radius = radius * SCALE_FACTOR_DEFAULT
if scaled_radius > 0.0001:
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=scaled_radius)
for f in bm.faces: f.smooth = True
bm.to_mesh(obj.data)
bm.free()
def update_torus(obj_name, loc, major, minor, color, show):
obj = bpy.data.objects.get(obj_name)
if not obj: return
obj.hide_viewport = not show
obj.hide_render = not show
if show:
apply_torus_bmesh(obj, major, minor)
obj.location = (loc[0] * SCALE_FACTOR_DEFAULT, loc[1] * SCALE_FACTOR_DEFAULT, loc[2] * SCALE_FACTOR_DEFAULT)
mat = create_material(f"Mat_{obj_name}", color)
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
def update_center(obj_name, loc, radius, color, show):
obj = bpy.data.objects.get(obj_name)
if not obj: return
obj.hide_viewport = not show
obj.hide_render = not show
if show:
apply_center_bmesh(obj, radius)
obj.location = (loc[0] * SCALE_FACTOR_DEFAULT, loc[1] * SCALE_FACTOR_DEFAULT, loc[2] * SCALE_FACTOR_DEFAULT)
mat = create_material(f"Mat_{obj_name}", color)
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
def get_surface_loc(props):
C = mathutils.Vector(props.cr_crystal_loc)
R = mathutils.Vector(props.cr_retina_loc)
r = props.cr_crystal_major
V = R - C
dist = V.length
if dist > 0.0001:
return C + (V / dist) * r
return C.copy()
# --- コールバック ---
def cb_update_crystal(self, context):
props = context.scene
update_torus("Crystal_Torus", props.cr_crystal_loc, props.cr_crystal_major, props.cr_crystal_minor, props.cr_crystal_color, props.cr_crystal_show_torus)
update_center("Crystal_Center", props.cr_crystal_loc, props.cr_crystal_center_radius, props.cr_crystal_center_color, props.cr_crystal_show_center)
# 表面交点の追従更新
P = get_surface_loc(props)
update_torus("Surface_Torus", P, props.cr_surface_major, props.cr_surface_minor, props.cr_surface_color, props.cr_surface_show_torus)
update_center("Surface_Center", P, props.cr_surface_center_radius, props.cr_surface_center_color, props.cr_surface_show_center)
def cb_update_retina(self, context):
props = context.scene
update_torus("Retina_Torus", props.cr_retina_loc, props.cr_retina_major, props.cr_retina_minor, props.cr_retina_color, props.cr_retina_show_torus)
update_center("Retina_Center", props.cr_retina_loc, props.cr_retina_center_radius, props.cr_retina_center_color, props.cr_retina_show_center)
# 表面交点の追従更新
P = get_surface_loc(props)
update_torus("Surface_Torus", P, props.cr_surface_major, props.cr_surface_minor, props.cr_surface_color, props.cr_surface_show_torus)
update_center("Surface_Center", P, props.cr_surface_center_radius, props.cr_surface_center_color, props.cr_surface_show_center)
def cb_update_surface(self, context):
props = context.scene
P = get_surface_loc(props)
update_torus("Surface_Torus", P, props.cr_surface_major, props.cr_surface_minor, props.cr_surface_color, props.cr_surface_show_torus)
update_center("Surface_Center", P, props.cr_surface_center_radius, props.cr_surface_center_color, props.cr_surface_show_center)
def cb_update_gamma(self, context):
props = context.scene
update_torus("Gamma_Ref_Torus", props.cr_gamma_loc, props.cr_gamma_major, props.cr_gamma_minor, props.cr_gamma_color, props.cr_gamma_show_torus)
update_center("Gamma_Ref_Center", props.cr_gamma_loc, props.cr_gamma_center_radius, props.cr_gamma_center_color, props.cr_gamma_show_center)
# --- 生成 ---
def create_base_obj(name, col):
existing = bpy.data.objects.get(name)
if existing:
mesh = existing.data
bpy.data.objects.remove(existing)
if mesh.users == 0: bpy.data.meshes.remove(mesh)
mesh = bpy.data.meshes.new(f"Mesh_{name}")
obj = bpy.data.objects.new(name, mesh)
col.objects.link(obj)
return obj
class CRYSTAL_OT_generate(bpy.types.Operator):
bl_idname = f"{PREFIX}.generate"
bl_label = "4つのトーラスと中心マーカーを生成"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col = bpy.data.collections.get(ADDON_TITLE)
if not col:
col = bpy.data.collections.new(ADDON_TITLE)
context.scene.collection.children.link(col)
for name in ["Crystal_Torus", "Crystal_Center", "Retina_Torus", "Retina_Center",
"Surface_Torus", "Surface_Center", "Gamma_Ref_Torus", "Gamma_Ref_Center"]:
create_base_obj(name, col)
cb_update_crystal(context.scene, context)
cb_update_retina(context.scene, context)
cb_update_surface(context.scene, context)
cb_update_gamma(context.scene, context)
self.report({'INFO'}, "要素の生成が完了しました")
return {'FINISHED'}
class CRYSTAL_OT_detach(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach"
bl_label = "切り離し (独立化して確定)"
def execute(self, context):
names = ["Crystal_Torus", "Crystal_Center", "Retina_Torus", "Retina_Center",
"Surface_Torus", "Surface_Center", "Gamma_Ref_Torus", "Gamma_Ref_Center"]
count = 0
for name in names:
obj = bpy.data.objects.get(name)
if obj:
new_name = f"{name}_Baked"
obj.name = new_name
obj.data.name = f"Mesh_{new_name}"
if obj.data.materials:
new_mat = obj.data.materials[0].copy()
new_mat.name = f"Mat_{new_name}"
obj.data.materials[0] = new_mat
count += 1
if count > 0: self.report({'INFO'}, "現在の状態を切り離して確定しました")
return {'FINISHED'}
class CRYSTAL_OT_copy_info(bpy.types.Operator):
bl_idname = f"{PREFIX}.copy_info"
bl_label = "情報をクリップボードにコピー"
def execute(self, context):
props = context.scene
P = get_surface_loc(props)
info_text = (
f"【1.水晶玉】\n 中心: ({props.cr_crystal_loc[0]:.2f}, {props.cr_crystal_loc[1]:.2f}, {props.cr_crystal_loc[2]:.2f})\n"
f" 半径: {props.cr_crystal_major:.2f} / 太さ: {props.cr_crystal_minor:.2f}\n\n"
f"【2.網膜点】\n 中心: ({props.cr_retina_loc[0]:.2f}, {props.cr_retina_loc[1]:.2f}, {props.cr_retina_loc[2]:.2f})\n"
f" 半径: {props.cr_retina_major:.2f} / 太さ: {props.cr_retina_minor:.2f}\n\n"
f"【3.表面交点】\n 中心: ({P.x:.2f}, {P.y:.2f}, {P.z:.2f})\n"
f" 半径: {props.cr_surface_major:.2f} / 太さ: {props.cr_surface_minor:.2f}\n\n"
f"【4.ガンマ基準点】\n 中心: ({props.cr_gamma_loc[0]:.2f}, {props.cr_gamma_loc[1]:.2f}, {props.cr_gamma_loc[2]:.2f})\n"
f" 半径: {props.cr_gamma_major:.2f} / 太さ: {props.cr_gamma_minor:.2f}\n"
)
context.window_manager.clipboard = info_text
self.report({'INFO'}, "パラメータ情報をコピーしました")
return {'FINISHED'}
def setup_workspace():
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
return None
# =========================================================
# 9. パネル構成
# =========================================================
def draw_torus_box(layout, props, title, icon, p_loc, p_major, p_minor, p_color, p_show_torus, p_show_center, p_radius, p_center_color, is_surface=False):
box = layout.box()
# トーラス自体の表示トグルとタイトル
row = box.row()
row.prop(props, p_show_torus, text="", icon='HIDE_OFF' if getattr(props, p_show_torus) else 'HIDE_ON')
row.label(text=title, icon=icon)
if getattr(props, p_show_torus):
if is_surface:
P = get_surface_loc(props)
box.label(text=f"中心 (自動計算): ({P.x:.3f}, {P.y:.3f}, {P.z:.3f})")
else:
box.prop(props, p_loc, text="中心")
row = box.row()
row.prop(props, p_major, text="半径")
row.prop(props, p_minor, text="太さ")
box.prop(props, p_color, text="色")
# センターマーカーの表示トグル
box.prop(props, p_show_center, text="中心に球体マーカーを表示", toggle=True)
if getattr(props, p_show_center):
row = box.row()
row.prop(props, p_radius, text="球の半径")
row.prop(props, p_center_color, text="球の色")
class CRYSTAL_PT_main(bpy.types.Panel):
bl_label = ADDON_TITLE
bl_idname = f"{PREFIX_VAL}_PT_main"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
props = context.scene
layout.label(text="生成後にスライダーでリアルタイム変更", icon='INFO')
draw_torus_box(layout, props, "1. 水晶玉", 'SPHERE', "cr_crystal_loc", "cr_crystal_major", "cr_crystal_minor", "cr_crystal_color", "cr_crystal_show_torus", "cr_crystal_show_center", "cr_crystal_center_radius", "cr_crystal_center_color")
draw_torus_box(layout, props, "2. 網膜点", 'TRACKING', "cr_retina_loc", "cr_retina_major", "cr_retina_minor", "cr_retina_color", "cr_retina_show_torus", "cr_retina_show_center", "cr_retina_center_radius", "cr_retina_center_color")
draw_torus_box(layout, props, "3. 表面交点 (線分と球面の交差位置)", 'VERTEXSEL', None, "cr_surface_major", "cr_surface_minor", "cr_surface_color", "cr_surface_show_torus", "cr_surface_show_center", "cr_surface_center_radius", "cr_surface_center_color", is_surface=True)
draw_torus_box(layout, props, "4. ガンマ基準点", 'EMPTY_DATA', "cr_gamma_loc", "cr_gamma_major", "cr_gamma_minor", "cr_gamma_color", "cr_gamma_show_torus", "cr_gamma_show_center", "cr_gamma_center_radius", "cr_gamma_center_color")
layout.separator()
layout.operator(CRYSTAL_OT_generate.bl_idname, icon='PLAY')
layout.operator(CRYSTAL_OT_detach.bl_idname, icon='UNLINKED')
layout.operator(CRYSTAL_OT_copy_info.bl_idname, icon='COPYDOWN')
class CRYSTAL_PT_LinksPanel(bpy.types.Panel):
bl_label = "リンク"
bl_idname = f"{PREFIX_VAL}_PT_links_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_options = {'DEFAULT_CLOSED'}
bl_order = 100
def draw(self, context):
layout = self.layout
box = layout.box()
links = [(LINK_NAME_1, LINK_URL_1, 'HELP'), (LINK_NAME_2, LINK_URL_2, 'URL'), (LINK_NAME_3, LINK_URL_3, 'URL'), (LINK_NAME_4, LINK_URL_4, 'URL')]
for name, url, icon in links:
if name and url: box.operator(CRYSTAL_OT_OpenUrl.bl_idname, text=name, icon=icon).url = url
class CRYSTAL_PT_RemovePanel(bpy.types.Panel):
bl_label = "アドオン削除"
bl_idname = f"{PREFIX_VAL}_PT_remove_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_options = {'DEFAULT_CLOSED'}
bl_order = 101
def draw(self, context):
layout = self.layout
box = layout.box()
box.operator(CRYSTAL_OT_RemoveAddon.bl_idname, icon='CANCEL')
# =========================================================
# 10. register()
# =========================================================
classes = (
CRYSTAL_OT_OpenUrl, CRYSTAL_OT_RemoveAddon, CRYSTAL_OT_generate, CRYSTAL_OT_detach, CRYSTAL_OT_copy_info,
CRYSTAL_PT_main, CRYSTAL_PT_LinksPanel, CRYSTAL_PT_RemovePanel,
)
def register():
for cls in classes: bpy.utils.register_class(cls)
Scene = bpy.types.Scene
# 1. 水晶玉
Scene.cr_crystal_loc = bpy.props.FloatVectorProperty(name="位置", default=CRYSTAL_LOC_DEFAULT, subtype='XYZ', update=cb_update_crystal)
Scene.cr_crystal_major = bpy.props.FloatProperty(name="半径", default=CRYSTAL_MAJOR_DEFAULT, min=0.0, update=cb_update_crystal)
Scene.cr_crystal_minor = bpy.props.FloatProperty(name="太さ", default=CRYSTAL_MINOR_DEFAULT, min=0.0, update=cb_update_crystal)
Scene.cr_crystal_color = bpy.props.FloatVectorProperty(name="色", default=COLOR_CRYSTAL_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_crystal)
Scene.cr_crystal_show_torus = bpy.props.BoolProperty(name="トーラス表示", default=CRYSTAL_SHOW_TORUS, update=cb_update_crystal)
Scene.cr_crystal_show_center = bpy.props.BoolProperty(name="マーカー表示", default=CRYSTAL_SHOW_CENTER, update=cb_update_crystal)
Scene.cr_crystal_center_radius = bpy.props.FloatProperty(name="球半径", default=CRYSTAL_CENTER_RADIUS_DEFAULT, min=0.0, update=cb_update_crystal)
Scene.cr_crystal_center_color = bpy.props.FloatVectorProperty(name="球色", default=COLOR_CRYSTAL_CENTER_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_crystal)
# 2. 網膜点
Scene.cr_retina_loc = bpy.props.FloatVectorProperty(name="位置", default=RETINA_LOC_DEFAULT, subtype='XYZ', update=cb_update_retina)
Scene.cr_retina_major = bpy.props.FloatProperty(name="半径", default=RETINA_MAJOR_DEFAULT, min=0.0, update=cb_update_retina)
Scene.cr_retina_minor = bpy.props.FloatProperty(name="太さ", default=RETINA_MINOR_DEFAULT, min=0.0, update=cb_update_retina)
Scene.cr_retina_color = bpy.props.FloatVectorProperty(name="色", default=COLOR_RETINA_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_retina)
Scene.cr_retina_show_torus = bpy.props.BoolProperty(name="トーラス表示", default=RETINA_SHOW_TORUS, update=cb_update_retina)
Scene.cr_retina_show_center = bpy.props.BoolProperty(name="マーカー表示", default=RETINA_SHOW_CENTER, update=cb_update_retina)
Scene.cr_retina_center_radius = bpy.props.FloatProperty(name="球半径", default=RETINA_CENTER_RADIUS_DEFAULT, min=0.0, update=cb_update_retina)
Scene.cr_retina_center_color = bpy.props.FloatVectorProperty(name="球色", default=COLOR_RETINA_CENTER_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_retina)
# 3. 表面交点
Scene.cr_surface_major = bpy.props.FloatProperty(name="半径", default=SURFACE_MAJOR_DEFAULT, min=0.0, update=cb_update_surface)
Scene.cr_surface_minor = bpy.props.FloatProperty(name="太さ", default=SURFACE_MINOR_DEFAULT, min=0.0, update=cb_update_surface)
Scene.cr_surface_color = bpy.props.FloatVectorProperty(name="色", default=COLOR_SURFACE_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_surface)
Scene.cr_surface_show_torus = bpy.props.BoolProperty(name="トーラス表示", default=SURFACE_SHOW_TORUS, update=cb_update_surface)
Scene.cr_surface_show_center = bpy.props.BoolProperty(name="マーカー表示", default=SURFACE_SHOW_CENTER, update=cb_update_surface)
Scene.cr_surface_center_radius = bpy.props.FloatProperty(name="球半径", default=SURFACE_CENTER_RADIUS_DEFAULT, min=0.0, update=cb_update_surface)
Scene.cr_surface_center_color = bpy.props.FloatVectorProperty(name="球色", default=COLOR_SURFACE_CENTER_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_surface)
# 4. ガンマ基準点
Scene.cr_gamma_loc = bpy.props.FloatVectorProperty(name="位置", default=GAMMA_LOC_DEFAULT, subtype='XYZ', update=cb_update_gamma)
Scene.cr_gamma_major = bpy.props.FloatProperty(name="半径", default=GAMMA_MAJOR_DEFAULT, min=0.0, update=cb_update_gamma)
Scene.cr_gamma_minor = bpy.props.FloatProperty(name="太さ", default=GAMMA_MINOR_DEFAULT, min=0.0, update=cb_update_gamma)
Scene.cr_gamma_color = bpy.props.FloatVectorProperty(name="色", default=COLOR_GAMMA_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_gamma)
Scene.cr_gamma_show_torus = bpy.props.BoolProperty(name="トーラス表示", default=GAMMA_SHOW_TORUS, update=cb_update_gamma)
Scene.cr_gamma_show_center = bpy.props.BoolProperty(name="マーカー表示", default=GAMMA_SHOW_CENTER, update=cb_update_gamma)
Scene.cr_gamma_center_radius = bpy.props.FloatProperty(name="球半径", default=GAMMA_CENTER_RADIUS_DEFAULT, min=0.0, update=cb_update_gamma)
Scene.cr_gamma_center_color = bpy.props.FloatVectorProperty(name="球色", default=COLOR_GAMMA_CENTER_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_gamma)
bpy.app.timers.register(setup_workspace, first_interval=0.1)
# =========================================================
# 11. unregister()
# =========================================================
def unregister():
props = [
"cr_crystal_loc", "cr_crystal_major", "cr_crystal_minor", "cr_crystal_color", "cr_crystal_show_torus", "cr_crystal_show_center", "cr_crystal_center_radius", "cr_crystal_center_color",
"cr_retina_loc", "cr_retina_major", "cr_retina_minor", "cr_retina_color", "cr_retina_show_torus", "cr_retina_show_center", "cr_retina_center_radius", "cr_retina_center_color",
"cr_surface_major", "cr_surface_minor", "cr_surface_color", "cr_surface_show_torus", "cr_surface_show_center", "cr_surface_center_radius", "cr_surface_center_color",
"cr_gamma_loc", "cr_gamma_major", "cr_gamma_minor", "cr_gamma_color", "cr_gamma_show_torus", "cr_gamma_show_center", "cr_gamma_center_radius", "cr_gamma_center_color"
]
for p in props:
if hasattr(bpy.types.Scene, p): delattr(bpy.types.Scene, p)
for cls in reversed(classes):
try: bpy.utils.unregister_class(cls)
except: pass
if __name__ == "__main__":
register()
# =========================================================
# 1. bl_info
# =========================================================
bl_info = {
"name": "Crystal Torus & Center Markers",
"author": "ChatGPT",
"version": (1, 7, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar > Crystal Torus",
"description": "表面交点の自動追従、および各トーラス中心の球体マーカーに対応",
"category": "3D View",
}
# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0723_1610
# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import mathutils
import webbrowser
# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "Crystal Torus System"
PREFIX_VAL = "CRYSTAL_TORUS"
PREFIX = PREFIX_VAL.lower()
ADDON_CATEGORY = "Crystal Torus"
# =========================================================
# 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. 初期設定
# =========================================================
SCALE_FACTOR_DEFAULT = 1.0
CRYSTAL_MAJOR_DEFAULT = 0.10
CRYSTAL_MINOR_DEFAULT = 0.01
CRYSTAL_LOC_DEFAULT = (0.0, 0.0, 0.0)
COLOR_CRYSTAL_DEFAULT = (0.4, 0.8, 1.0, 0.5)
RETINA_MAJOR_DEFAULT = 1.00
RETINA_MINOR_DEFAULT = 0.01
RETINA_LOC_DEFAULT = (1.10, 0.0, 0.0)
COLOR_RETINA_DEFAULT = (1.0, 0.5, 0.5, 1.0)
SURFACE_MAJOR_DEFAULT = 0.05
SURFACE_MINOR_DEFAULT = 0.01
COLOR_SURFACE_DEFAULT = (0.2, 1.0, 0.2, 0.8)
GAMMA_MAJOR_DEFAULT = 1.10
GAMMA_MINOR_DEFAULT = 0.05
GAMMA_LOC_DEFAULT = (0.0, 0.0, 0.0)
COLOR_GAMMA_DEFAULT = (1.0, 0.9, 0.4, 0.5)
CENTER_RADIUS_DEFAULT = 0.02
COLOR_CENTER_DEFAULT = (1.0, 1.0, 1.0, 1.0)
# =========================================================
# 7. 共通クラス
# =========================================================
class CRYSTAL_OT_OpenUrl(bpy.types.Operator):
bl_idname = f"{PREFIX}.open_url"
bl_label = "リンクを開く"
url: bpy.props.StringProperty()
def execute(self, context):
if self.url: webbrowser.open(self.url)
return {'FINISHED'}
class CRYSTAL_OT_RemoveAddon(bpy.types.Operator):
bl_idname = f"{PREFIX}.remove_addon"
bl_label = "Unregister Addon"
bl_description = "このアドオンを無効化し、UIから消去します"
def execute(self, context):
bpy.app.timers.register(unregister, first_interval=0.01)
self.report({'INFO'}, f"{ADDON_TITLE} を UI から消去しました")
return {'FINISHED'}
# =========================================================
# 8. アドオン固有機能 (リアルタイム・生成・切り離し)
# =========================================================
def create_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
if color[3] < 1.0:
bsdf.inputs["Alpha"].default_value = color[3]
mat.blend_method = 'BLEND' if bpy.app.version < (4, 2, 0) else mat.blend_method
else:
bsdf.inputs["Alpha"].default_value = 1.0
return mat
def apply_torus_bmesh(obj, major, minor):
"""ZX平面のトーラスを構築"""
bm = bmesh.new()
scaled_major = major * SCALE_FACTOR_DEFAULT
scaled_minor = minor * SCALE_FACTOR_DEFAULT
if scaled_major <= 0.0001: scaled_major = 0.0001
if scaled_minor <= 0.0001: scaled_minor = 0.0001
segments, rings = 48, 24
verts = []
for i in range(segments):
theta = 2.0 * math.pi * i / segments
cos_theta = math.cos(theta)
sin_theta = math.sin(theta)
row = []
for j in range(rings):
phi = 2.0 * math.pi * j / rings
x = (scaled_major + scaled_minor * math.cos(phi)) * cos_theta
z = (scaled_major + scaled_minor * math.cos(phi)) * sin_theta
y = scaled_minor * math.sin(phi)
row.append(bm.verts.new((x, y, z)))
verts.append(row)
for i in range(segments):
next_i = (i + 1) % segments
for j in range(rings):
next_j = (j + 1) % rings
f = bm.faces.new((verts[i][j], verts[i][next_j], verts[next_i][next_j], verts[next_i][j]))
f.smooth = True
bm.to_mesh(obj.data)
bm.free()
def apply_center_bmesh(obj, radius):
"""中心球体のメッシュを構築"""
bm = bmesh.new()
scaled_radius = radius * SCALE_FACTOR_DEFAULT
if scaled_radius > 0.0001:
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=scaled_radius)
for f in bm.faces: f.smooth = True
bm.to_mesh(obj.data)
bm.free()
def update_torus(obj_name, loc, major, minor, color):
obj = bpy.data.objects.get(obj_name)
if not obj: return
apply_torus_bmesh(obj, major, minor)
obj.location = (loc[0] * SCALE_FACTOR_DEFAULT, loc[1] * SCALE_FACTOR_DEFAULT, loc[2] * SCALE_FACTOR_DEFAULT)
mat = create_material(f"Mat_{obj_name}", color)
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
def update_center(obj_name, loc, radius, color, show):
obj = bpy.data.objects.get(obj_name)
if not obj: return
obj.hide_viewport = not show
obj.hide_render = not show
if show:
apply_center_bmesh(obj, radius)
obj.location = (loc[0] * SCALE_FACTOR_DEFAULT, loc[1] * SCALE_FACTOR_DEFAULT, loc[2] * SCALE_FACTOR_DEFAULT)
mat = create_material(f"Mat_{obj_name}", color)
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
# 表面交点 (水晶玉中心と網膜点を結ぶ線分と球面の交点) の座標計算
def get_surface_loc(props):
C = mathutils.Vector(props.cr_crystal_loc)
R = mathutils.Vector(props.cr_retina_loc)
r = props.cr_crystal_major
V = R - C
dist = V.length
if dist > 0.0001:
return C + (V / dist) * r
return C.copy()
# --- コールバック ---
def cb_update_crystal(self, context):
props = context.scene
update_torus("Crystal_Torus", props.cr_crystal_loc, props.cr_crystal_major, props.cr_crystal_minor, props.cr_crystal_color)
update_center("Crystal_Center", props.cr_crystal_loc, props.cr_crystal_center_radius, props.cr_crystal_center_color, props.cr_crystal_show_center)
# 表面交点の追従更新
P = get_surface_loc(props)
update_torus("Surface_Torus", P, props.cr_surface_major, props.cr_surface_minor, props.cr_surface_color)
update_center("Surface_Center", P, props.cr_surface_center_radius, props.cr_surface_center_color, props.cr_surface_show_center)
def cb_update_retina(self, context):
props = context.scene
update_torus("Retina_Torus", props.cr_retina_loc, props.cr_retina_major, props.cr_retina_minor, props.cr_retina_color)
update_center("Retina_Center", props.cr_retina_loc, props.cr_retina_center_radius, props.cr_retina_center_color, props.cr_retina_show_center)
# 表面交点の追従更新
P = get_surface_loc(props)
update_torus("Surface_Torus", P, props.cr_surface_major, props.cr_surface_minor, props.cr_surface_color)
update_center("Surface_Center", P, props.cr_surface_center_radius, props.cr_surface_center_color, props.cr_surface_show_center)
def cb_update_surface(self, context):
props = context.scene
P = get_surface_loc(props)
update_torus("Surface_Torus", P, props.cr_surface_major, props.cr_surface_minor, props.cr_surface_color)
update_center("Surface_Center", P, props.cr_surface_center_radius, props.cr_surface_center_color, props.cr_surface_show_center)
def cb_update_gamma(self, context):
props = context.scene
update_torus("Gamma_Ref_Torus", props.cr_gamma_loc, props.cr_gamma_major, props.cr_gamma_minor, props.cr_gamma_color)
update_center("Gamma_Ref_Center", props.cr_gamma_loc, props.cr_gamma_center_radius, props.cr_gamma_center_color, props.cr_gamma_show_center)
# --- 生成 ---
def create_base_obj(name, col):
existing = bpy.data.objects.get(name)
if existing:
mesh = existing.data
bpy.data.objects.remove(existing)
if mesh.users == 0: bpy.data.meshes.remove(mesh)
mesh = bpy.data.meshes.new(f"Mesh_{name}")
obj = bpy.data.objects.new(name, mesh)
col.objects.link(obj)
return obj
class CRYSTAL_OT_generate(bpy.types.Operator):
bl_idname = f"{PREFIX}.generate"
bl_label = "4つのトーラスと中心マーカーを生成"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col = bpy.data.collections.get(ADDON_TITLE)
if not col:
col = bpy.data.collections.new(ADDON_TITLE)
context.scene.collection.children.link(col)
# オブジェクトの枠だけ作成
for name in ["Crystal_Torus", "Crystal_Center", "Retina_Torus", "Retina_Center",
"Surface_Torus", "Surface_Center", "Gamma_Ref_Torus", "Gamma_Ref_Center"]:
create_base_obj(name, col)
# 全てを一斉にアップデート(コールバック関数を手動で呼ぶのと同じ効果)
cb_update_crystal(context.scene, context)
cb_update_retina(context.scene, context)
cb_update_surface(context.scene, context)
cb_update_gamma(context.scene, context)
self.report({'INFO'}, "要素の生成が完了しました")
return {'FINISHED'}
class CRYSTAL_OT_detach(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach"
bl_label = "切り離し (独立化して確定)"
def execute(self, context):
names = ["Crystal_Torus", "Crystal_Center", "Retina_Torus", "Retina_Center",
"Surface_Torus", "Surface_Center", "Gamma_Ref_Torus", "Gamma_Ref_Center"]
count = 0
for name in names:
obj = bpy.data.objects.get(name)
if obj:
new_name = f"{name}_Baked"
obj.name = new_name
obj.data.name = f"Mesh_{new_name}"
if obj.data.materials:
new_mat = obj.data.materials[0].copy()
new_mat.name = f"Mat_{new_name}"
obj.data.materials[0] = new_mat
count += 1
if count > 0: self.report({'INFO'}, "現在の状態を切り離して確定しました")
return {'FINISHED'}
class CRYSTAL_OT_copy_info(bpy.types.Operator):
bl_idname = f"{PREFIX}.copy_info"
bl_label = "情報をクリップボードにコピー"
def execute(self, context):
props = context.scene
P = get_surface_loc(props)
info_text = (
f"【1.水晶玉】\n 中心: ({props.cr_crystal_loc[0]:.2f}, {props.cr_crystal_loc[1]:.2f}, {props.cr_crystal_loc[2]:.2f})\n"
f" 半径: {props.cr_crystal_major:.2f} / 太さ: {props.cr_crystal_minor:.2f}\n\n"
f"【2.網膜点】\n 中心: ({props.cr_retina_loc[0]:.2f}, {props.cr_retina_loc[1]:.2f}, {props.cr_retina_loc[2]:.2f})\n"
f" 半径: {props.cr_retina_major:.2f} / 太さ: {props.cr_retina_minor:.2f}\n\n"
f"【3.表面交点】(自動計算)\n 中心: ({P.x:.2f}, {P.y:.2f}, {P.z:.2f})\n"
f" 半径: {props.cr_surface_major:.2f} / 太さ: {props.cr_surface_minor:.2f}\n\n"
f"【4.ガンマ基準点】\n 中心: ({props.cr_gamma_loc[0]:.2f}, {props.cr_gamma_loc[1]:.2f}, {props.cr_gamma_loc[2]:.2f})\n"
f" 半径: {props.cr_gamma_major:.2f} / 太さ: {props.cr_gamma_minor:.2f}\n"
)
context.window_manager.clipboard = info_text
self.report({'INFO'}, "パラメータ情報をコピーしました")
return {'FINISHED'}
def setup_workspace():
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
return None
# =========================================================
# 9. パネル構成
# =========================================================
def draw_torus_box(layout, props, title, icon, p_loc, p_major, p_minor, p_color, p_show, p_radius, p_center_color, is_surface=False):
box = layout.box()
box.label(text=title, icon=icon)
if is_surface:
P = get_surface_loc(props)
box.label(text=f"中心 (自動計算): ({P.x:.3f}, {P.y:.3f}, {P.z:.3f})")
else:
box.prop(props, p_loc, text="中心")
row = box.row()
row.prop(props, p_major, text="半径")
row.prop(props, p_minor, text="太さ")
box.prop(props, p_color, text="色")
# センターマーカーの表示
box.prop(props, p_show, text="中心に球体マーカーを表示", icon='MESH_UVSPHERE')
if getattr(props, p_show):
row = box.row()
row.prop(props, p_radius, text="球の半径")
row.prop(props, p_center_color, text="球の色")
class CRYSTAL_PT_main(bpy.types.Panel):
bl_label = ADDON_TITLE
bl_idname = f"{PREFIX_VAL}_PT_main"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
props = context.scene
layout.label(text="生成後にスライダーでリアルタイム変更", icon='INFO')
draw_torus_box(layout, props, "1. 水晶玉", 'SPHERE', "cr_crystal_loc", "cr_crystal_major", "cr_crystal_minor", "cr_crystal_color", "cr_crystal_show_center", "cr_crystal_center_radius", "cr_crystal_center_color")
draw_torus_box(layout, props, "2. 網膜点", 'TRACKING', "cr_retina_loc", "cr_retina_major", "cr_retina_minor", "cr_retina_color", "cr_retina_show_center", "cr_retina_center_radius", "cr_retina_center_color")
draw_torus_box(layout, props, "3. 表面交点 (線分と球面の交差位置)", 'VERTEXSEL', None, "cr_surface_major", "cr_surface_minor", "cr_surface_color", "cr_surface_show_center", "cr_surface_center_radius", "cr_surface_center_color", is_surface=True)
draw_torus_box(layout, props, "4. ガンマ基準点", 'EMPTY_DATA', "cr_gamma_loc", "cr_gamma_major", "cr_gamma_minor", "cr_gamma_color", "cr_gamma_show_center", "cr_gamma_center_radius", "cr_gamma_center_color")
layout.separator()
layout.operator(CRYSTAL_OT_generate.bl_idname, icon='PLAY')
layout.operator(CRYSTAL_OT_detach.bl_idname, icon='UNLINKED')
layout.operator(CRYSTAL_OT_copy_info.bl_idname, icon='COPYDOWN')
class CRYSTAL_PT_LinksPanel(bpy.types.Panel):
bl_label = "リンク"
bl_idname = f"{PREFIX_VAL}_PT_links_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_options = {'DEFAULT_CLOSED'}
bl_order = 100
def draw(self, context):
layout = self.layout
box = layout.box()
links = [(LINK_NAME_1, LINK_URL_1, 'HELP'), (LINK_NAME_2, LINK_URL_2, 'URL'), (LINK_NAME_3, LINK_URL_3, 'URL'), (LINK_NAME_4, LINK_URL_4, 'URL')]
for name, url, icon in links:
if name and url: box.operator(CRYSTAL_OT_OpenUrl.bl_idname, text=name, icon=icon).url = url
class CRYSTAL_PT_RemovePanel(bpy.types.Panel):
bl_label = "アドオン削除"
bl_idname = f"{PREFIX_VAL}_PT_remove_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_options = {'DEFAULT_CLOSED'}
bl_order = 101
def draw(self, context):
layout = self.layout
box = layout.box()
box.operator(CRYSTAL_OT_RemoveAddon.bl_idname, icon='CANCEL')
# =========================================================
# 10. register()
# =========================================================
classes = (
CRYSTAL_OT_OpenUrl, CRYSTAL_OT_RemoveAddon, CRYSTAL_OT_generate, CRYSTAL_OT_detach, CRYSTAL_OT_copy_info,
CRYSTAL_PT_main, CRYSTAL_PT_LinksPanel, CRYSTAL_PT_RemovePanel,
)
def register():
for cls in classes: bpy.utils.register_class(cls)
Scene = bpy.types.Scene
# 1. 水晶玉
Scene.cr_crystal_loc = bpy.props.FloatVectorProperty(name="位置", default=CRYSTAL_LOC_DEFAULT, subtype='XYZ', update=cb_update_crystal)
Scene.cr_crystal_major = bpy.props.FloatProperty(name="半径", default=CRYSTAL_MAJOR_DEFAULT, min=0.0, update=cb_update_crystal)
Scene.cr_crystal_minor = bpy.props.FloatProperty(name="太さ", default=CRYSTAL_MINOR_DEFAULT, min=0.0, update=cb_update_crystal)
Scene.cr_crystal_color = bpy.props.FloatVectorProperty(name="色", default=COLOR_CRYSTAL_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_crystal)
Scene.cr_crystal_show_center = bpy.props.BoolProperty(name="マーカー表示", default=False, update=cb_update_crystal)
Scene.cr_crystal_center_radius = bpy.props.FloatProperty(name="球半径", default=CENTER_RADIUS_DEFAULT, min=0.0, update=cb_update_crystal)
Scene.cr_crystal_center_color = bpy.props.FloatVectorProperty(name="球色", default=COLOR_CENTER_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_crystal)
# 2. 網膜点
Scene.cr_retina_loc = bpy.props.FloatVectorProperty(name="位置", default=RETINA_LOC_DEFAULT, subtype='XYZ', update=cb_update_retina)
Scene.cr_retina_major = bpy.props.FloatProperty(name="半径", default=RETINA_MAJOR_DEFAULT, min=0.0, update=cb_update_retina)
Scene.cr_retina_minor = bpy.props.FloatProperty(name="太さ", default=RETINA_MINOR_DEFAULT, min=0.0, update=cb_update_retina)
Scene.cr_retina_color = bpy.props.FloatVectorProperty(name="色", default=COLOR_RETINA_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_retina)
Scene.cr_retina_show_center = bpy.props.BoolProperty(name="マーカー表示", default=False, update=cb_update_retina)
Scene.cr_retina_center_radius = bpy.props.FloatProperty(name="球半径", default=CENTER_RADIUS_DEFAULT, min=0.0, update=cb_update_retina)
Scene.cr_retina_center_color = bpy.props.FloatVectorProperty(name="球色", default=COLOR_CENTER_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_retina)
# 3. 表面交点 (位置のPropertyは不要:自動計算のため)
Scene.cr_surface_major = bpy.props.FloatProperty(name="半径", default=SURFACE_MAJOR_DEFAULT, min=0.0, update=cb_update_surface)
Scene.cr_surface_minor = bpy.props.FloatProperty(name="太さ", default=SURFACE_MINOR_DEFAULT, min=0.0, update=cb_update_surface)
Scene.cr_surface_color = bpy.props.FloatVectorProperty(name="色", default=COLOR_SURFACE_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_surface)
Scene.cr_surface_show_center = bpy.props.BoolProperty(name="マーカー表示", default=False, update=cb_update_surface)
Scene.cr_surface_center_radius = bpy.props.FloatProperty(name="球半径", default=CENTER_RADIUS_DEFAULT, min=0.0, update=cb_update_surface)
Scene.cr_surface_center_color = bpy.props.FloatVectorProperty(name="球色", default=COLOR_CENTER_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_surface)
# 4. ガンマ基準点
Scene.cr_gamma_loc = bpy.props.FloatVectorProperty(name="位置", default=GAMMA_LOC_DEFAULT, subtype='XYZ', update=cb_update_gamma)
Scene.cr_gamma_major = bpy.props.FloatProperty(name="半径", default=GAMMA_MAJOR_DEFAULT, min=0.0, update=cb_update_gamma)
Scene.cr_gamma_minor = bpy.props.FloatProperty(name="太さ", default=GAMMA_MINOR_DEFAULT, min=0.0, update=cb_update_gamma)
Scene.cr_gamma_color = bpy.props.FloatVectorProperty(name="色", default=COLOR_GAMMA_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_gamma)
Scene.cr_gamma_show_center = bpy.props.BoolProperty(name="マーカー表示", default=False, update=cb_update_gamma)
Scene.cr_gamma_center_radius = bpy.props.FloatProperty(name="球半径", default=CENTER_RADIUS_DEFAULT, min=0.0, update=cb_update_gamma)
Scene.cr_gamma_center_color = bpy.props.FloatVectorProperty(name="球色", default=COLOR_CENTER_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_gamma)
bpy.app.timers.register(setup_workspace, first_interval=0.1)
# =========================================================
# 11. unregister()
# =========================================================
def unregister():
props = [
"cr_crystal_loc", "cr_crystal_major", "cr_crystal_minor", "cr_crystal_color", "cr_crystal_show_center", "cr_crystal_center_radius", "cr_crystal_center_color",
"cr_retina_loc", "cr_retina_major", "cr_retina_minor", "cr_retina_color", "cr_retina_show_center", "cr_retina_center_radius", "cr_retina_center_color",
"cr_surface_major", "cr_surface_minor", "cr_surface_color", "cr_surface_show_center", "cr_surface_center_radius", "cr_surface_center_color",
"cr_gamma_loc", "cr_gamma_major", "cr_gamma_minor", "cr_gamma_color", "cr_gamma_show_center", "cr_gamma_center_radius", "cr_gamma_center_color"
]
for p in props:
if hasattr(bpy.types.Scene, p): delattr(bpy.types.Scene, p)
for cls in reversed(classes):
try: bpy.utils.unregister_class(cls)
except: pass
if __name__ == "__main__":
register()
# =========================================================
# 1. bl_info
# =========================================================
bl_info = {
"name": "Crystal Torus Realtime System",
"author": "ChatGPT",
"version": (1, 6, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar > Crystal Torus",
"description": "リアルタイム変更・情報コピー・切り離し機能付き トーラス生成アドオン",
"category": "3D View",
}
# =========================================================
# 2. コード作成日時
# =========================================================
# Code Created : 2026_0723_1545
# =========================================================
# 3. import
# =========================================================
import bpy
import bmesh
import math
import webbrowser
# =========================================================
# 4. 共通定義
# =========================================================
ADDON_TITLE = "Crystal Torus System"
PREFIX_VAL = "CRYSTAL_TORUS"
PREFIX = PREFIX_VAL.lower()
ADDON_CATEGORY = "Crystal Torus"
# =========================================================
# 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. 初期設定
# =========================================================
SCALE_FACTOR_DEFAULT = 1.0
# 1. 水晶玉
CRYSTAL_MAJOR_DEFAULT = 0.10
CRYSTAL_MINOR_DEFAULT = 0.01
CRYSTAL_LOC_DEFAULT = (0.0, 0.0, 0.0)
COLOR_CRYSTAL_DEFAULT = (0.4, 0.8, 1.0, 0.5)
# 2. 網膜点
RETINA_MAJOR_DEFAULT = 1.00
RETINA_MINOR_DEFAULT = 0.01
RETINA_LOC_DEFAULT = (1.10, 0.0, 0.0)
COLOR_RETINA_DEFAULT = (1.0, 0.5, 0.5, 1.0)
# 3. ガンマ基準点
GAMMA_MAJOR_DEFAULT = 1.10
GAMMA_MINOR_DEFAULT = 0.05
GAMMA_LOC_DEFAULT = (0.0, 0.0, 0.0)
COLOR_GAMMA_DEFAULT = (1.0, 0.9, 0.4, 0.5)
# =========================================================
# 7. 共通クラス
# =========================================================
class CRYSTAL_OT_OpenUrl(bpy.types.Operator):
bl_idname = f"{PREFIX}.open_url"
bl_label = "リンクを開く"
url: bpy.props.StringProperty()
def execute(self, context):
if self.url: webbrowser.open(self.url)
return {'FINISHED'}
class CRYSTAL_OT_RemoveAddon(bpy.types.Operator):
bl_idname = f"{PREFIX}.remove_addon"
bl_label = "Unregister Addon"
bl_description = "このアドオンを無効化し、UIから消去します"
def execute(self, context):
bpy.app.timers.register(unregister, first_interval=0.01)
self.report({'INFO'}, f"{ADDON_TITLE} を UI から消去しました")
return {'FINISHED'}
# =========================================================
# 8. アドオン固有機能 (リアルタイム・生成・切り離し)
# =========================================================
def create_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
if color[3] < 1.0:
bsdf.inputs["Alpha"].default_value = color[3]
else:
bsdf.inputs["Alpha"].default_value = 1.0
return mat
def apply_torus_bmesh(obj, major, minor):
"""数学的に頂点を計算してZX平面のトーラスを構築する(エラー回避とリアルタイム対応用)"""
bm = bmesh.new()
scaled_major = major * SCALE_FACTOR_DEFAULT
scaled_minor = minor * SCALE_FACTOR_DEFAULT
segments = 48
rings = 24
verts = []
# 太さや半径が0の時はクラッシュ防止のため微小値を設定するか、処理をスキップ
if scaled_major <= 0.0001: scaled_major = 0.0001
if scaled_minor <= 0.0001: scaled_minor = 0.0001
for i in range(segments):
theta = 2.0 * math.pi * i / segments
cos_theta = math.cos(theta)
sin_theta = math.sin(theta)
row = []
for j in range(rings):
phi = 2.0 * math.pi * j / rings
cos_phi = math.cos(phi)
sin_phi = math.sin(phi)
x = (scaled_major + scaled_minor * cos_phi) * cos_theta
z = (scaled_major + scaled_minor * cos_phi) * sin_theta
y = scaled_minor * sin_phi
v = bm.verts.new((x, y, z))
row.append(v)
verts.append(row)
for i in range(segments):
next_i = (i + 1) % segments
for j in range(rings):
next_j = (j + 1) % rings
v1 = verts[i][j]
v2 = verts[i][next_j]
v3 = verts[next_i][next_j]
v4 = verts[next_i][j]
f = bm.faces.new((v1, v2, v3, v4))
f.smooth = True
bm.to_mesh(obj.data)
bm.free()
def update_torus(obj_name, loc, major, minor, color):
obj = bpy.data.objects.get(obj_name)
if not obj:
return
apply_torus_bmesh(obj, major, minor)
obj.location = (
loc[0] * SCALE_FACTOR_DEFAULT,
loc[1] * SCALE_FACTOR_DEFAULT,
loc[2] * SCALE_FACTOR_DEFAULT
)
create_material(f"Mat_{obj_name}", color)
def cb_update_crystal(self, context):
update_torus("Crystal_Torus", self.cr_crystal_loc, self.cr_crystal_major, self.cr_crystal_minor, self.cr_crystal_color)
def cb_update_retina(self, context):
update_torus("Retina_Torus", self.cr_retina_loc, self.cr_retina_major, self.cr_retina_minor, self.cr_retina_color)
def cb_update_gamma(self, context):
update_torus("Gamma_Ref_Torus", self.cr_gamma_loc, self.cr_gamma_major, self.cr_gamma_minor, self.cr_gamma_color)
def create_torus_obj(name, loc, major, minor, color, col):
existing = bpy.data.objects.get(name)
if existing:
mesh = existing.data
bpy.data.objects.remove(existing)
if mesh.users == 0:
bpy.data.meshes.remove(mesh)
mesh = bpy.data.meshes.new(f"Mesh_{name}")
obj = bpy.data.objects.new(name, mesh)
col.objects.link(obj)
apply_torus_bmesh(obj, major, minor)
obj.location = (
loc[0] * SCALE_FACTOR_DEFAULT,
loc[1] * SCALE_FACTOR_DEFAULT,
loc[2] * SCALE_FACTOR_DEFAULT
)
mat = create_material(f"Mat_{name}", color)
obj.data.materials.append(mat)
return obj
class CRYSTAL_OT_generate(bpy.types.Operator):
bl_idname = f"{PREFIX}.generate"
bl_label = "3つのトーラスを生成"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene
col = bpy.data.collections.get(ADDON_TITLE)
if not col:
col = bpy.data.collections.new(ADDON_TITLE)
context.scene.collection.children.link(col)
create_torus_obj("Crystal_Torus", props.cr_crystal_loc, props.cr_crystal_major, props.cr_crystal_minor, props.cr_crystal_color, col)
create_torus_obj("Retina_Torus", props.cr_retina_loc, props.cr_retina_major, props.cr_retina_minor, props.cr_retina_color, col)
create_torus_obj("Gamma_Ref_Torus", props.cr_gamma_loc, props.cr_gamma_major, props.cr_gamma_minor, props.cr_gamma_color, col)
self.report({'INFO'}, "ZX平面にトーラスを生成しました(以降スライダーでリアルタイム変更可能)")
return {'FINISHED'}
class CRYSTAL_OT_detach(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach"
bl_label = "切り離し (独立化して確定)"
bl_description = "現在のトーラスをアドオン管理から切り離し、マテリアルも独立させます"
def execute(self, context):
names = ["Crystal_Torus", "Retina_Torus", "Gamma_Ref_Torus"]
detached_count = 0
for name in names:
obj = bpy.data.objects.get(name)
if obj:
new_name = f"{name}_Baked"
obj.name = new_name
obj.data.name = f"Mesh_{new_name}"
if obj.data.materials:
old_mat = obj.data.materials[0]
new_mat = old_mat.copy()
new_mat.name = f"Mat_{new_name}"
obj.data.materials[0] = new_mat
detached_count += 1
if detached_count > 0:
self.report({'INFO'}, f"{detached_count}個のトーラスを切り離しました。(再生成が可能です)")
else:
self.report({'WARNING'}, "切り離す対象のトーラスが見つかりません。")
return {'FINISHED'}
class CRYSTAL_OT_copy_info(bpy.types.Operator):
bl_idname = f"{PREFIX}.copy_info"
bl_label = "情報をクリップボードにコピー"
def execute(self, context):
props = context.scene
info_text = (
f"【1.水晶玉】\n 中心: ({props.cr_crystal_loc[0]:.2f}, {props.cr_crystal_loc[1]:.2f}, {props.cr_crystal_loc[2]:.2f})\n"
f" 半径: {props.cr_crystal_major:.2f} / 太さ: {props.cr_crystal_minor:.2f}\n\n"
f"【2.網膜点】\n 中心: ({props.cr_retina_loc[0]:.2f}, {props.cr_retina_loc[1]:.2f}, {props.cr_retina_loc[2]:.2f})\n"
f" 半径: {props.cr_retina_major:.2f} / 太さ: {props.cr_retina_minor:.2f}\n\n"
f"【3.ガンマ基準点】\n 中心: ({props.cr_gamma_loc[0]:.2f}, {props.cr_gamma_loc[1]:.2f}, {props.cr_gamma_loc[2]:.2f})\n"
f" 半径: {props.cr_gamma_major:.2f} / 太さ: {props.cr_gamma_minor:.2f}\n"
)
context.window_manager.clipboard = info_text
self.report({'INFO'}, "パラメータ情報をコピーしました")
return {'FINISHED'}
def setup_workspace():
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
return None
# =========================================================
# 9. パネル構成
# =========================================================
class CRYSTAL_PT_main(bpy.types.Panel):
bl_label = ADDON_TITLE
bl_idname = f"{PREFIX_VAL}_PT_main"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
props = context.scene
layout.label(text="生成後にスライダーでリアルタイム変更", icon='INFO')
box = layout.box()
box.prop(props, "cr_crystal_loc", text="1.水晶玉 中心")
row = box.row()
row.prop(props, "cr_crystal_major", text="半径")
row.prop(props, "cr_crystal_minor", text="太さ")
box.prop(props, "cr_crystal_color", text="色")
box = layout.box()
box.prop(props, "cr_retina_loc", text="2.網膜点 中心")
row = box.row()
row.prop(props, "cr_retina_major", text="半径")
row.prop(props, "cr_retina_minor", text="太さ")
box.prop(props, "cr_retina_color", text="色")
box = layout.box()
box.prop(props, "cr_gamma_loc", text="3.ガンマ基準点 中心")
row = box.row()
row.prop(props, "cr_gamma_major", text="半径")
row.prop(props, "cr_gamma_minor", text="太さ")
box.prop(props, "cr_gamma_color", text="色")
layout.separator()
layout.operator(CRYSTAL_OT_generate.bl_idname, icon='PLAY', text="トーラスを生成する")
layout.operator(CRYSTAL_OT_detach.bl_idname, icon='UNLINKED', text="切り離し (独立化して確定)")
class CRYSTAL_PT_info(bpy.types.Panel):
bl_label = "パラメータ情報"
bl_idname = f"{PREFIX_VAL}_PT_info"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_order = 1
def draw(self, context):
layout = self.layout
props = context.scene
box = layout.box()
box.label(text=f"水晶玉 : ({props.cr_crystal_loc[0]:.2f}, {props.cr_crystal_loc[1]:.2f}, {props.cr_crystal_loc[2]:.2f})")
box.label(text=f" └ 半径: {props.cr_crystal_major:.2f} / 太さ: {props.cr_crystal_minor:.2f}")
box = layout.box()
box.label(text=f"網膜点 : ({props.cr_retina_loc[0]:.2f}, {props.cr_retina_loc[1]:.2f}, {props.cr_retina_loc[2]:.2f})")
box.label(text=f" └ 半径: {props.cr_retina_major:.2f} / 太さ: {props.cr_retina_minor:.2f}")
box = layout.box()
box.label(text=f"ガンマ : ({props.cr_gamma_loc[0]:.2f}, {props.cr_gamma_loc[1]:.2f}, {props.cr_gamma_loc[2]:.2f})")
box.label(text=f" └ 半径: {props.cr_gamma_major:.2f} / 太さ: {props.cr_gamma_minor:.2f}")
layout.separator()
layout.operator(CRYSTAL_OT_copy_info.bl_idname, icon='COPYDOWN')
class CRYSTAL_PT_LinksPanel(bpy.types.Panel):
bl_label = "リンク"
bl_idname = f"{PREFIX_VAL}_PT_links_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_options = {'DEFAULT_CLOSED'}
bl_order = 100
def draw(self, context):
layout = self.layout
box = layout.box()
links = [
(LINK_NAME_1, LINK_URL_1, 'HELP'),
(LINK_NAME_2, LINK_URL_2, 'URL'),
(LINK_NAME_3, LINK_URL_3, 'URL'),
(LINK_NAME_4, LINK_URL_4, 'URL'),
]
for name, url, icon in links:
if name and url:
box.operator(CRYSTAL_OT_OpenUrl.bl_idname, text=name, icon=icon).url = url
class CRYSTAL_PT_RemovePanel(bpy.types.Panel):
bl_label = "アドオン削除"
bl_idname = f"{PREFIX_VAL}_PT_remove_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
bl_options = {'DEFAULT_CLOSED'}
bl_order = 101
def draw(self, context):
layout = self.layout
box = layout.box()
box.label(text="このアドオンを消去します", icon='ERROR')
box.operator(CRYSTAL_OT_RemoveAddon.bl_idname, icon='CANCEL')
# =========================================================
# 10. register()
# =========================================================
classes = (
CRYSTAL_OT_OpenUrl,
CRYSTAL_OT_RemoveAddon,
CRYSTAL_OT_generate,
CRYSTAL_OT_detach,
CRYSTAL_OT_copy_info,
CRYSTAL_PT_main,
CRYSTAL_PT_info,
CRYSTAL_PT_LinksPanel,
CRYSTAL_PT_RemovePanel,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
Scene = bpy.types.Scene
# 【制限】 min=0.0 を追加し、太さや半径がマイナスにならないよう設定
Scene.cr_crystal_loc = bpy.props.FloatVectorProperty(name="水晶玉 中心", default=CRYSTAL_LOC_DEFAULT, subtype='XYZ', update=cb_update_crystal)
Scene.cr_crystal_major = bpy.props.FloatProperty(name="水晶玉 半径", default=CRYSTAL_MAJOR_DEFAULT, min=0.0, update=cb_update_crystal)
Scene.cr_crystal_minor = bpy.props.FloatProperty(name="水晶玉 太さ", default=CRYSTAL_MINOR_DEFAULT, min=0.0, update=cb_update_crystal)
Scene.cr_crystal_color = bpy.props.FloatVectorProperty(name="水晶玉 色", default=COLOR_CRYSTAL_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_crystal)
Scene.cr_retina_loc = bpy.props.FloatVectorProperty(name="網膜点 中心", default=RETINA_LOC_DEFAULT, subtype='XYZ', update=cb_update_retina)
Scene.cr_retina_major = bpy.props.FloatProperty(name="網膜点 半径", default=RETINA_MAJOR_DEFAULT, min=0.0, update=cb_update_retina)
Scene.cr_retina_minor = bpy.props.FloatProperty(name="網膜点 太さ", default=RETINA_MINOR_DEFAULT, min=0.0, update=cb_update_retina)
Scene.cr_retina_color = bpy.props.FloatVectorProperty(name="網膜点 色", default=COLOR_RETINA_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_retina)
Scene.cr_gamma_loc = bpy.props.FloatVectorProperty(name="ガンマ基準 中心", default=GAMMA_LOC_DEFAULT, subtype='XYZ', update=cb_update_gamma)
Scene.cr_gamma_major = bpy.props.FloatProperty(name="ガンマ基準 半径", default=GAMMA_MAJOR_DEFAULT, min=0.0, update=cb_update_gamma)
Scene.cr_gamma_minor = bpy.props.FloatProperty(name="ガンマ基準 太さ", default=GAMMA_MINOR_DEFAULT, min=0.0, update=cb_update_gamma)
Scene.cr_gamma_color = bpy.props.FloatVectorProperty(name="ガンマ基準 色", default=COLOR_GAMMA_DEFAULT, subtype='COLOR', size=4, min=0, max=1, update=cb_update_gamma)
bpy.app.timers.register(setup_workspace, first_interval=0.1)
# =========================================================
# 11. unregister()
# =========================================================
def unregister():
Scene = bpy.types.Scene
props = [
"cr_crystal_loc", "cr_crystal_major", "cr_crystal_minor", "cr_crystal_color",
"cr_retina_loc", "cr_retina_major", "cr_retina_minor", "cr_retina_color",
"cr_gamma_loc", "cr_gamma_major", "cr_gamma_minor", "cr_gamma_color"
]
for p in props:
if hasattr(Scene, p):
delattr(Scene, p)
for cls in reversed(classes):
try:
bpy.utils.unregister_class(cls)
except RuntimeError:
pass
if __name__ == "__main__":
register()