import bpy
import bmesh
import math
import random
import colorsys
from datetime import datetime
from mathutils import Vector, Euler
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import IntProperty, FloatProperty, FloatVectorProperty, PointerProperty, BoolProperty, EnumProperty
CUSTOM_CATEGORY_NAME = "[ トーラス球体 ]"
PREVIEW_COLL_SPHERES = "zPreview_TS_Spheres"
PREVIEW_COLL_FRAME = "zPreview_TS_Frame"
_is_updating = False
_is_unloading = False
# =========================================================
# ヘルパー: コレクションの表示同期 / クリーンアップ
# =========================================================
def set_collection_visibility(context, coll_name, visible):
if not hasattr(context, "view_layer") or not context.view_layer:
return
layer_coll = context.view_layer.layer_collection
def find_layer_coll(layer, name):
if layer.collection.name == name: return layer
for child in layer.children:
res = find_layer_coll(child, name)
if res: return res
return None
lc = find_layer_coll(layer_coll, coll_name)
if lc:
lc.exclude = False
lc.hide_viewport = not visible
def cleanup_specific_preview(coll_name):
preview_coll = bpy.data.collections.get(coll_name)
if preview_coll:
for obj in list(preview_coll.objects):
mesh = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if mesh:
try: bpy.data.meshes.remove(mesh, do_unlink=True)
except: pass
bpy.data.collections.remove(preview_coll)
def cleanup_all_previews():
cleanup_specific_preview(PREVIEW_COLL_SPHERES)
cleanup_specific_preview(PREVIEW_COLL_FRAME)
for mat in list(bpy.data.materials):
if mat.name.startswith("zPreview_TS_"):
bpy.data.materials.remove(mat, do_unlink=True)
def get_or_create_material(mat_name, color):
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(name=mat_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
mat.diffuse_color = color
return mat
# =========================================================
# 矢印生成ヘルパー
# =========================================================
def create_arrow_mesh(name, origin, target, thickness, head_size):
mesh = bpy.data.meshes.new(name)
bm = bmesh.new()
vec = target - origin
dist = vec.length
if dist < 0.0001:
bm.to_mesh(mesh)
bm.free()
return mesh
dir_vec = vec.normalized()
rot_quat = dir_vec.to_track_quat('Z', 'Y')
shaft_len = max(0.0, dist - head_size)
if shaft_len > 0:
geom_shaft = bmesh.ops.create_cone(
bm, cap_ends=True, cap_tris=False, segments=16,
radius1=thickness, radius2=thickness, depth=shaft_len
)
bmesh.ops.translate(bm, verts=geom_shaft['verts'], vec=(0, 0, shaft_len / 2.0))
if head_size > 0:
geom_head = bmesh.ops.create_cone(
bm, cap_ends=True, cap_tris=False, segments=16,
radius1=thickness * 2.5, radius2=0.0, depth=head_size
)
bmesh.ops.translate(bm, verts=geom_head['verts'], vec=(0, 0, shaft_len + head_size / 2.0))
bmesh.ops.rotate(bm, verts=bm.verts, cent=(0,0,0), matrix=rot_quat.to_matrix())
bmesh.ops.translate(bm, verts=bm.verts, vec=origin)
bm.to_mesh(mesh)
bm.free()
for poly in mesh.polygons: poly.use_smooth = True
return mesh
# =========================================================
# ライブアップデート
# =========================================================
def deferred_build():
global _is_unloading
if _is_unloading: return None
build_previews(bpy.context)
return None
def on_prop_update(self, context):
global _is_unloading
if _is_unloading: return
if self.live_update and not bpy.app.timers.is_registered(deferred_build):
bpy.app.timers.register(deferred_build, first_interval=0.05)
def update_spheres_preview(self, context):
global _is_unloading
if _is_unloading: return
if not self.show_spheres_preview:
cleanup_specific_preview(PREVIEW_COLL_SPHERES)
build_previews(context)
def update_frame_preview(self, context):
global _is_unloading
if _is_unloading: return
if not self.show_frame_preview:
cleanup_specific_preview(PREVIEW_COLL_FRAME)
build_previews(context)
# =========================================================
# プレビュー生成コア処理 (球体と額縁を完全に分離)
# =========================================================
def build_previews(context):
global _is_updating, _is_unloading
if _is_unloading or _is_updating: return
_is_updating = True
try:
settings = context.scene.ts_settings
# -----------------------------------------------------
# 1. トーラス球体の生成
# -----------------------------------------------------
if settings.show_spheres_preview:
cleanup_specific_preview(PREVIEW_COLL_SPHERES)
coll_spheres = bpy.data.collections.new(PREVIEW_COLL_SPHERES)
context.scene.collection.children.link(coll_spheres)
set_collection_visibility(context, PREVIEW_COLL_SPHERES, True)
rot_euler = Euler(settings.spheres_rotation, 'XYZ')
arrow_origin_vec = Vector(settings.arrow_origin)
if settings.show_arrows:
mat_arrow = get_or_create_material("zPreview_TS_Mat_Arrow", settings.arrow_color)
for i in range(settings.count):
angle = (2.0 * math.pi / settings.count) * i
pos = Vector((
settings.torus_radius * math.cos(angle),
settings.torus_radius * math.sin(angle),
0.0
))
pos.rotate(rot_euler)
pos += Vector(settings.spheres_loc)
# 球体の生成
mesh_sphere = bpy.data.meshes.new(f"zPreview_TS_Sphere_{i}")
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=settings.sphere_radius)
bm.to_mesh(mesh_sphere)
bm.free()
obj_sphere = bpy.data.objects.new(f"zPreview_TS_Sphere_{i}", mesh_sphere)
coll_spheres.objects.link(obj_sphere)
obj_sphere.location = pos
for poly in mesh_sphere.polygons: poly.use_smooth = True
if settings.color_mode == 'SINGLE':
mat = get_or_create_material("zPreview_TS_Mat_Sphere_Single", settings.single_color)
else:
random.seed(settings.random_seed + i)
r, g, b = colorsys.hsv_to_rgb(random.random(), 0.8, 0.9)
mat = get_or_create_material(f"zPreview_TS_Mat_Sphere_Rand_{i}", (r, g, b, 1.0))
obj_sphere.data.materials.append(mat)
# 矢印の生成
if settings.show_arrows:
mesh_arrow = create_arrow_mesh(
f"zPreview_TS_Arrow_{i}",
arrow_origin_vec, pos,
settings.arrow_thickness, settings.arrow_head_size
)
obj_arrow = bpy.data.objects.new(f"zPreview_TS_Arrow_{i}", mesh_arrow)
coll_spheres.objects.link(obj_arrow)
obj_arrow.data.materials.append(mat_arrow)
# -----------------------------------------------------
# 2. 独立した額縁の生成
# -----------------------------------------------------
if settings.show_frame_preview:
cleanup_specific_preview(PREVIEW_COLL_FRAME)
coll_frame = bpy.data.collections.new(PREVIEW_COLL_FRAME)
context.scene.collection.children.link(coll_frame)
set_collection_visibility(context, PREVIEW_COLL_FRAME, True)
mesh_frame = bpy.data.meshes.new("zPreview_TS_Frame")
bm_sq = bmesh.new()
S = settings.frame_size / 2.0
w = settings.frame_width
s = max(0.001, S - w)
z = -settings.frame_thickness / 2.0
v0 = bm_sq.verts.new((-S, -S, z))
v1 = bm_sq.verts.new(( S, -S, z))
v2 = bm_sq.verts.new(( S, S, z))
v3 = bm_sq.verts.new((-S, S, z))
v4 = bm_sq.verts.new((-s, -s, z))
v5 = bm_sq.verts.new(( s, -s, z))
v6 = bm_sq.verts.new(( s, s, z))
v7 = bm_sq.verts.new((-s, s, z))
f1 = bm_sq.faces.new((v0, v1, v5, v4))
f2 = bm_sq.faces.new((v1, v2, v6, v5))
f3 = bm_sq.faces.new((v2, v3, v7, v6))
f4 = bm_sq.faces.new((v3, v0, v4, v7))
extruded = bmesh.ops.extrude_face_region(bm_sq, geom=[f1, f2, f3, f4])
extrude_verts = [elem for elem in extruded['geom'] if isinstance(elem, bmesh.types.BMVert)]
bmesh.ops.translate(bm_sq, verts=extrude_verts, vec=(0, 0, settings.frame_thickness))
# 独立した回転と位置の適用
rot_mat = Euler(settings.frame_rotation, 'XYZ').to_matrix()
bmesh.ops.rotate(bm_sq, verts=bm_sq.verts, cent=(0,0,0), matrix=rot_mat)
bmesh.ops.translate(bm_sq, verts=bm_sq.verts, vec=settings.frame_loc)
bmesh.ops.recalc_face_normals(bm_sq, faces=bm_sq.faces)
bm_sq.to_mesh(mesh_frame)
bm_sq.free()
for poly in mesh_frame.polygons: poly.use_smooth = True
obj_frame = bpy.data.objects.new("zPreview_TS_Frame_Obj", mesh_frame)
coll_frame.objects.link(obj_frame)
mat_sq = get_or_create_material("zPreview_TS_Mat_Frame", settings.frame_color)
obj_frame.data.materials.append(mat_sq)
context.view_layer.update()
finally:
_is_updating = False
# =========================================================
# プロパティ定義
# =========================================================
class TS_Settings(PropertyGroup):
live_update: BoolProperty(name="Live Update", default=True)
# --- 球体ジェネレーター設定 ---
show_spheres_preview: BoolProperty(name="球体プレビュー表示", default=True, update=update_spheres_preview)
count: IntProperty(name="球の数", default=12, min=3, update=on_prop_update)
torus_radius: FloatProperty(name="配置半径", default=5.0, min=0.1, update=on_prop_update)
sphere_radius: FloatProperty(name="球の半径", default=1.0, min=0.01, update=on_prop_update)
spheres_loc: FloatVectorProperty(name="配置中心位置", default=(0.0, 0.0, 0.0), subtype='TRANSLATION', update=on_prop_update)
spheres_rotation: FloatVectorProperty(name="回転軸 (XYZ)", default=(0.0, 0.0, 0.0), subtype='EULER', update=on_prop_update)
color_mode: EnumProperty(
name="色設定", items=[('SINGLE', "1色 (全部同じ)", ""), ('RANDOM', "ランダム", "")],
default='SINGLE', update=on_prop_update
)
single_color: FloatVectorProperty(name="カラー", subtype='COLOR', size=4, default=(0.2, 0.6, 1.0, 1.0), min=0, max=1, update=on_prop_update)
random_seed: IntProperty(name="ランダムシード", default=123, update=on_prop_update)
show_arrows: BoolProperty(name="矢印を表示", default=False, update=on_prop_update)
arrow_origin: FloatVectorProperty(name="矢印の起点", default=(0.0, 0.0, 0.0), subtype='TRANSLATION', update=on_prop_update)
arrow_thickness: FloatProperty(name="太さ", default=0.05, min=0.001, update=on_prop_update)
arrow_head_size: FloatProperty(name="ヘッド長さ", default=0.5, min=0.01, update=on_prop_update)
arrow_color: FloatVectorProperty(name="矢印の色", subtype='COLOR', size=4, default=(0.8, 0.2, 0.2, 1.0), min=0, max=1, update=on_prop_update)
# --- 独立額縁ジェネレーター設定 ---
show_frame_preview: BoolProperty(name="額縁プレビュー表示", default=False, update=update_frame_preview)
frame_size: FloatProperty(name="サイズ (外寸)", default=4.0, min=0.5, update=on_prop_update)
frame_width: FloatProperty(name="枠の太さ", default=0.5, min=0.01, update=on_prop_update)
frame_thickness: FloatProperty(name="厚み", default=0.2, min=0.001, update=on_prop_update)
frame_color: FloatVectorProperty(name="額縁の色", subtype='COLOR', size=4, default=(0.1, 0.8, 0.3, 1.0), min=0, max=1, update=on_prop_update)
frame_loc: FloatVectorProperty(name="中心位置", default=(0.0, -10.0, 0.0), subtype='TRANSLATION', update=on_prop_update)
frame_rotation: FloatVectorProperty(name="回転 (XYZ)", default=(0.0, 0.0, 0.0), subtype='EULER', update=on_prop_update)
# =========================================================
# オペレーター (それぞれの確定)
# =========================================================
class TS_OT_DetachSpheres(Operator):
bl_idname = "ts.detach_spheres"
bl_label = "球体を確定 & 切り離し"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
preview_coll = bpy.data.collections.get(PREVIEW_COLL_SPHERES)
if not preview_coll or not preview_coll.objects:
self.report({'WARNING'}, "球体のプレビューがありません。")
return {'CANCELLED'}
run_id = datetime.now().strftime("%H%M%S")
preview_coll.name = f"Spheres_{run_id}"
for obj in preview_coll.objects:
obj.name = obj.name.replace("zPreview_TS_", "TS_") + f"_{run_id}"
if obj.data: obj.data.name = obj.data.name.replace("zPreview_TS_", "Mesh_") + f"_{run_id}"
for mat_slot in obj.material_slots:
if mat_slot.material and "zPreview_TS_" in mat_slot.material.name:
mat_slot.material.name = mat_slot.material.name.replace("zPreview_TS_", "Mat_TS_") + f"_{run_id}"
self.report({'INFO'}, f"球体を確定しました: Spheres_{run_id}")
build_previews(context)
return {'FINISHED'}
class TS_OT_DetachFrame(Operator):
bl_idname = "ts.detach_frame"
bl_label = "額縁を確定 & 切り離し"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
preview_coll = bpy.data.collections.get(PREVIEW_COLL_FRAME)
if not preview_coll or not preview_coll.objects:
self.report({'WARNING'}, "額縁のプレビューがありません。")
return {'CANCELLED'}
run_id = datetime.now().strftime("%H%M%S")
preview_coll.name = f"Frame_{run_id}"
for obj in preview_coll.objects:
obj.name = obj.name.replace("zPreview_TS_", "TS_") + f"_{run_id}"
if obj.data: obj.data.name = obj.data.name.replace("zPreview_TS_", "Mesh_") + f"_{run_id}"
for mat_slot in obj.material_slots:
if mat_slot.material and "zPreview_TS_" in mat_slot.material.name:
mat_slot.material.name = mat_slot.material.name.replace("zPreview_TS_", "Mat_TS_") + f"_{run_id}"
self.report({'INFO'}, f"額縁を確定しました: Frame_{run_id}")
build_previews(context)
return {'FINISHED'}
# =========================================================
# パネル UI 1: トーラス球体ジェネレーター
# =========================================================
class TS_PT_SpheresPanel(Panel):
bl_label = "トーラス球体 ジェネレーター"
bl_idname = "TS_PT_spheres"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CUSTOM_CATEGORY_NAME
def draw(self, context):
layout = self.layout
settings = context.scene.ts_settings
row = layout.row()
row.prop(settings, "show_spheres_preview", text="プレビュー表示")
row.prop(settings, "live_update")
if not settings.show_spheres_preview: return
layout.separator()
box = layout.box()
col = box.column(align=True)
col.prop(settings, "count")
col.prop(settings, "torus_radius")
col.prop(settings, "sphere_radius")
box.separator()
col = box.column(align=True)
col.prop(settings, "spheres_loc")
col.prop(settings, "spheres_rotation")
box = layout.box()
box.label(text="カラー設定", icon='COLOR')
box.prop(settings, "color_mode", expand=True)
if settings.color_mode == 'SINGLE':
box.prop(settings, "single_color", text="")
else:
box.prop(settings, "random_seed")
box = layout.box()
box.label(text="矢印設定", icon='EMPTY_SINGLE_ARROW')
box.prop(settings, "show_arrows")
if settings.show_arrows:
col = box.column(align=True)
col.prop(settings, "arrow_origin", text="起点")
col.prop(settings, "arrow_thickness")
col.prop(settings, "arrow_head_size")
col.prop(settings, "arrow_color")
layout.separator()
row = layout.row()
row.scale_y = 1.5
row.operator(TS_OT_DetachSpheres.bl_idname, icon='DUPLICATE')
# =========================================================
# パネル UI 2: 独立額縁ジェネレーター
# =========================================================
class TS_PT_FramePanel(Panel):
bl_label = "独立額縁 ジェネレーター"
bl_idname = "TS_PT_frame"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CUSTOM_CATEGORY_NAME
def draw(self, context):
layout = self.layout
settings = context.scene.ts_settings
layout.prop(settings, "show_frame_preview", text="プレビュー表示")
if not settings.show_frame_preview: return
layout.separator()
box = layout.box()
col = box.column(align=True)
col.prop(settings, "frame_size")
col.prop(settings, "frame_width")
col.prop(settings, "frame_thickness")
box.separator()
col = box.column(align=True)
col.prop(settings, "frame_loc")
col.prop(settings, "frame_rotation")
box.separator()
box.prop(settings, "frame_color")
layout.separator()
row = layout.row()
row.scale_y = 1.5
row.operator(TS_OT_DetachFrame.bl_idname, icon='DUPLICATE')
# =========================================================
# パネル UI 3: アドオン削除機能
# =========================================================
def delayed_unregister():
try: unregister()
except Exception: pass
return None
class TS_OT_RemoveAllPanels(Operator):
bl_idname = "ts.remove_all_panels"
bl_label = "Unregister Addon"
def execute(self, context):
bpy.app.timers.register(delayed_unregister, first_interval=0.01)
return {'FINISHED'}
class TS_PT_RemovePanel(Panel):
bl_label = "Remove Addon"
bl_idname = "TS_PT_remove"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CUSTOM_CATEGORY_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
self.layout.operator(TS_OT_RemoveAllPanels.bl_idname, icon='CANCEL')
# =========================================================
# 登録処理
# =========================================================
classes = (
TS_Settings,
TS_OT_DetachSpheres,
TS_OT_DetachFrame,
TS_OT_RemoveAllPanels,
TS_PT_SpheresPanel,
TS_PT_FramePanel,
TS_PT_RemovePanel,
)
def register():
global _is_unloading
_is_unloading = False
if hasattr(bpy.types.Scene, 'ts_settings'):
del bpy.types.Scene.ts_settings
for cls in classes:
try: bpy.utils.register_class(cls)
except ValueError:
bpy.utils.unregister_class(cls)
bpy.utils.register_class(cls)
bpy.types.Scene.ts_settings = PointerProperty(type=TS_Settings)
def unregister():
global _is_unloading
_is_unloading = True
if bpy.app.timers.is_registered(deferred_build):
bpy.app.timers.unregister(deferred_build)
cleanup_all_previews()
if hasattr(bpy.types.Scene, 'ts_settings'):
del bpy.types.Scene.ts_settings
for cls in reversed(classes):
try: bpy.utils.unregister_class(cls)
except Exception: pass
if __name__ == "__main__":
register()
import bpy
import bmesh
import math
import random
import colorsys
from datetime import datetime
from mathutils import Vector, Euler
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import IntProperty, FloatProperty, FloatVectorProperty, PointerProperty, BoolProperty, EnumProperty
CUSTOM_TAG_NAME = "TorusSpheres"
CUSTOM_CATEGORY_NAME = "[ トーラス球体 ]"
PREVIEW_COLL_NAME = f"zPreview_{CUSTOM_TAG_NAME}"
PREVIEW_MAT_NAME_BASE = f"zPreview_{CUSTOM_TAG_NAME}_Mat"
_is_updating = False
_is_unloading = False
# =========================================================
# ヘルパー: コレクションの表示同期 / クリーンアップ
# =========================================================
def set_collection_visibility(context, coll_name, visible):
if not hasattr(context, "view_layer") or not context.view_layer:
return
layer_coll = context.view_layer.layer_collection
def find_layer_coll(layer, name):
if layer.collection.name == name: return layer
for child in layer.children:
res = find_layer_coll(child, name)
if res: return res
return None
lc = find_layer_coll(layer_coll, coll_name)
if lc:
lc.exclude = False
lc.hide_viewport = not visible
def cleanup_preview():
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll:
for obj in list(preview_coll.objects):
mesh = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if mesh:
try: bpy.data.meshes.remove(mesh, do_unlink=True)
except: pass
bpy.data.collections.remove(preview_coll)
for mat in list(bpy.data.materials):
if mat.name.startswith(PREVIEW_MAT_NAME_BASE):
bpy.data.materials.remove(mat, do_unlink=True)
# =========================================================
# ライブアップデートとマテリアル生成
# =========================================================
def deferred_build():
global _is_unloading
if _is_unloading: return None
build_preview(bpy.context)
return None
def on_prop_update(self, context):
global _is_unloading
if _is_unloading: return
if self.live_update and not bpy.app.timers.is_registered(deferred_build):
bpy.app.timers.register(deferred_build, first_interval=0.05)
def update_preview_visibility(self, context):
global _is_unloading
if _is_unloading: return
if self.show_preview:
build_preview(context)
else:
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll:
preview_coll.hide_viewport = True
preview_coll.hide_render = True
set_collection_visibility(context, PREVIEW_COLL_NAME, False)
context.view_layer.update()
def get_or_create_material(mat_name, color):
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(name=mat_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
mat.diffuse_color = color
return mat
# =========================================================
# プレビュー生成コア処理
# =========================================================
def build_preview(context):
global _is_updating, _is_unloading
if _is_unloading or _is_updating: return
_is_updating = True
try:
settings = context.scene.ts_settings
cleanup_preview()
if not settings.show_preview:
return
preview_coll = bpy.data.collections.new(PREVIEW_COLL_NAME)
context.scene.collection.children.link(preview_coll)
preview_coll.hide_viewport = False
preview_coll.hide_render = False
set_collection_visibility(context, PREVIEW_COLL_NAME, True)
rot_euler = Euler(settings.rotation, 'XYZ')
for i in range(settings.count):
angle = (2.0 * math.pi / settings.count) * i
# Z=0 (XY平面) 計算
pos = Vector((
settings.torus_radius * math.cos(angle),
settings.torus_radius * math.sin(angle),
0.0
))
# 回転と中心オフセットの適用
pos.rotate(rot_euler)
pos += Vector(settings.center_loc)
# プレビュー用メッシュの生成
mesh = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_Sphere_{i}")
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=settings.sphere_radius)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_Sphere_{i}", mesh)
preview_coll.objects.link(obj)
obj.location = pos
for poly in mesh.polygons:
poly.use_smooth = True
# マテリアル設定 (1色 or ランダム)
if settings.color_mode == 'SINGLE':
color = settings.single_color
mat_name = f"{PREVIEW_MAT_NAME_BASE}_Single"
else:
random.seed(settings.random_seed + i)
r, g, b = colorsys.hsv_to_rgb(random.random(), 0.8, 0.9)
color = (r, g, b, 1.0)
mat_name = f"{PREVIEW_MAT_NAME_BASE}_Rand_{i}"
mat = get_or_create_material(mat_name, color)
obj.data.materials.append(mat)
context.view_layer.update()
finally:
_is_updating = False
# =========================================================
# プロパティ定義
# =========================================================
class TS_Settings(PropertyGroup):
show_preview: BoolProperty(name="プレビュー表示", default=True, update=update_preview_visibility)
live_update: BoolProperty(name="Live Update", default=True)
count: IntProperty(name="球の数", default=12, min=3, update=on_prop_update)
torus_radius: FloatProperty(name="配置半径", default=5.0, min=0.1, update=on_prop_update)
sphere_radius: FloatProperty(name="球の半径", default=1.0, min=0.01, update=on_prop_update)
center_loc: FloatVectorProperty(name="中心位置", default=(0.0, 0.0, 0.0), subtype='TRANSLATION', update=on_prop_update)
rotation: FloatVectorProperty(name="回転軸 (XYZ)", default=(0.0, 0.0, 0.0), subtype='EULER', update=on_prop_update)
color_mode: EnumProperty(
name="色設定",
items=[('SINGLE', "1色 (全部同じ)", ""), ('RANDOM', "ランダム", "")],
default='SINGLE', update=on_prop_update
)
single_color: FloatVectorProperty(name="カラー", subtype='COLOR', size=4, default=(0.2, 0.6, 1.0, 1.0), min=0, max=1, update=on_prop_update)
random_seed: IntProperty(name="ランダムシード", default=123, update=on_prop_update)
# =========================================================
# オペレーター (確定・削除)
# =========================================================
class TS_OT_Detach(Operator):
bl_idname = "ts.detach"
bl_label = "確定 & 切り離し"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if not preview_coll or not preview_coll.objects:
self.report({'WARNING'}, "プレビューが表示されていません。")
return {'CANCELLED'}
run_id = datetime.now().strftime("%H%M%S")
preview_coll.name = f"{CUSTOM_TAG_NAME}_{run_id}"
# オブジェクトとマテリアルを独立名に変更(重複増殖を防ぐ)
for obj in preview_coll.objects:
obj.name = obj.name.replace(f"zPreview_{CUSTOM_TAG_NAME}_", f"{CUSTOM_TAG_NAME}_") + f"_{run_id}"
if obj.data:
obj.data.name = obj.data.name.replace(f"zPreview_{CUSTOM_TAG_NAME}_", "Mesh_") + f"_{run_id}"
for mat_slot in obj.material_slots:
if mat_slot.material and PREVIEW_MAT_NAME_BASE in mat_slot.material.name:
new_name = mat_slot.material.name.replace(PREVIEW_MAT_NAME_BASE, f"{CUSTOM_TAG_NAME}_Mat") + f"_{run_id}"
mat_slot.material.name = new_name
self.report({'INFO'}, f"切り離し完了: {CUSTOM_TAG_NAME}_{run_id}")
# 次の配置のために新しいプレビューを作り直す
build_preview(context)
return {'FINISHED'}
def delayed_unregister():
try:
unregister()
except Exception as e:
print("Unregister error:", e)
return None
class TS_OT_RemovePanels(Operator):
bl_idname = "ts.remove_panels"
bl_label = "Unregister Addon"
def execute(self, context):
# エラー回避: UIの描画サイクルが終わった直後(0.01秒後)に安全に削除を実行する
bpy.app.timers.register(delayed_unregister, first_interval=0.01)
self.report({'INFO'}, "パネルとプレビューを削除しました")
return {'FINISHED'}
# =========================================================
# パネル
# =========================================================
class TS_PT_MainPanel(Panel):
bl_label = "ドキュメント / 設定"
bl_idname = "TS_PT_main"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CUSTOM_CATEGORY_NAME
def draw(self, context):
layout = self.layout
settings = context.scene.ts_settings
box = layout.box()
box.label(text="【使用方法】", icon='INFO')
box.label(text="数値を変更するとリアルタイムで更新されます。")
box.label(text="確定ボタンを押すと別コレクションに分離されます。")
layout.separator()
row = layout.row()
row.prop(settings, "show_preview")
row.prop(settings, "live_update")
layout.separator()
col = layout.column(align=True)
col.prop(settings, "count")
col.prop(settings, "torus_radius")
col.prop(settings, "sphere_radius")
layout.separator()
col = layout.column(align=True)
col.prop(settings, "center_loc")
col.prop(settings, "rotation")
layout.separator()
box = layout.box()
box.label(text="カラー設定", icon='COLOR')
box.prop(settings, "color_mode", expand=True)
if settings.color_mode == 'SINGLE':
box.prop(settings, "single_color")
else:
box.prop(settings, "random_seed")
layout.separator()
row = layout.row()
row.scale_y = 1.5
row.operator(TS_OT_Detach.bl_idname, icon='DUPLICATE')
class TS_PT_RemovePanel(Panel):
bl_label = "アドオン削除"
bl_idname = "TS_PT_remove"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CUSTOM_CATEGORY_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
self.layout.operator(TS_OT_RemovePanels.bl_idname, icon='CANCEL', text="パネルとプレビューを消去")
# =========================================================
# 登録処理
# =========================================================
classes = (
TS_Settings,
TS_OT_Detach,
TS_OT_RemovePanels,
TS_PT_MainPanel,
TS_PT_RemovePanel,
)
def register():
global _is_unloading
_is_unloading = False
for cls in classes:
# 重複登録エラーを防ぐため安全に登録
if not hasattr(bpy.types, cls.__name__):
bpy.utils.register_class(cls)
bpy.types.Scene.ts_settings = PointerProperty(type=TS_Settings)
def unregister():
global _is_unloading
_is_unloading = True
if bpy.app.timers.is_registered(deferred_build):
bpy.app.timers.unregister(deferred_build)
cleanup_preview()
if hasattr(bpy.types.Scene, 'ts_settings'):
del bpy.types.Scene.ts_settings
for cls in reversed(classes):
if hasattr(bpy.types, cls.__name__):
try:
bpy.utils.unregister_class(cls)
except RuntimeError:
pass
if __name__ == "__main__":
register()