blender 今だけメモ
import bpy
import bmesh
import webbrowser
import math
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import StringProperty, FloatProperty, FloatVectorProperty, EnumProperty, PointerProperty, IntProperty, IntVectorProperty, BoolProperty
from datetime import datetime
from mathutils import Vector, Euler
# =========================================================
# 【ユーザー設定】 カスタムタグ名とカテゴリー名
# =========================================================
CUSTOM_TAG_NAME = "MySphere"
CUSTOM_CATEGORY_NAME = "[ 配列 ]"
# =========================================================
# パラメータ設定エリア (初期値と制限値)
# =========================================================
DEFAULT_SPACE_SIZE = (20.0, 20.0, 20.0)
DEFAULT_GRID_COUNT = (3, 3, 3)
DEFAULT_TOTAL_COUNT = 15
DEFAULT_RADIUS = 1.0
START_TIMESTAMP = datetime.now().strftime("%Y%m%d%H%M%S")
PREFIX = f"aiond_{START_TIMESTAMP}"
PREVIEW_COLL_NAME = f"zPreview_{CUSTOM_TAG_NAME}"
PREVIEW_MAT_NAME = f"zPreview_{CUSTOM_TAG_NAME}_Mat"
bl_info = {
"name": f"zionad Addon [{CUSTOM_TAG_NAME} Generator]",
"author": "Your Name & AI Assistant",
"version": (7, 0),
"blender": (4, 0, 0),
"location": "View3D > Sidebar > Grok Addon",
"description": "Live-update 3D arrays. Adjust parameters then Detach. Auto-clean on exit.",
"category": CUSTOM_CATEGORY_NAME,
}
ADDON_LINKS = ({"label": "アドオン削除パネル 20250530", "url": "<https://memo2017.hatenablog.com/entry/2025/05/30/202341>"},)
def get_prefix(): return PREFIX
# =========================================================
# プレビュー管理・クリーンアップ関数
# =========================================================
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)
mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
if mat:
bpy.data.materials.remove(mat, do_unlink=True)
def update_preview_visibility(self, context):
"""表示・非表示の切り替え"""
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll:
preview_coll.hide_viewport = not self.show_preview
preview_coll.hide_render = not self.show_preview
# =========================================================
# ライブアップデート用関数
# =========================================================
_is_updating = False
def deferred_build():
build_preview(bpy.context)
return None
def on_prop_update(self, context):
if self.live_update and not bpy.app.timers.is_registered(deferred_build):
bpy.app.timers.register(deferred_build, first_interval=0.05)
def build_preview(context):
global _is_updating
if _is_updating: return
_is_updating = True
try:
settings = context.scene.grok_sphere_settings
# --- 1. プレビュー用コレクションの準備 ---
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
else:
preview_coll = bpy.data.collections.new(PREVIEW_COLL_NAME)
context.scene.collection.children.link(preview_coll)
# 現在の表示状態を適用
preview_coll.hide_viewport = not settings.show_preview
preview_coll.hide_render = not settings.show_preview
# --- 2. プレビュー用マテリアルの準備 ---
mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
if not mat:
mat = bpy.data.materials.new(name=PREVIEW_MAT_NAME)
mat.use_nodes = True
mat.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (*settings.color, 1.0)
mat.diffuse_color = (*settings.color, 1.0)
# --- 3. 座標リストの計算 ---
size = Vector(settings.space_size)
half_size = size / 2.0
locations = []
if settings.layout_mode == 'CUBE':
nx, ny, nz = settings.grid_count
step_x = size.x / max(1, nx - 1) if nx > 1 else 0
step_y = size.y / max(1, ny - 1) if ny > 1 else 0
step_z = size.z / max(1, nz - 1) if nz > 1 else 0
for z_idx in range(nz):
for y_idx in range(ny):
for x_idx in range(nx):
x = -half_size.x + x_idx * step_x
y = -half_size.y + y_idx * step_y
z = -half_size.z + z_idx * step_z
locations.append(Vector((x, y, z)))
else:
count = settings.count
for i in range(count):
if settings.layout_mode == 'DIAGONAL':
factor = i / (count - 1) if count > 1 else 0
start_p = -half_size
end_p = half_size
locations.append(start_p.lerp(end_p, factor))
elif settings.layout_mode == 'CIRCLE':
angle = 2 * math.pi * (i / count)
r = min(half_size.x, half_size.y)
locations.append(Vector((r * math.cos(angle), r * math.sin(angle), 0)))
elif settings.layout_mode == 'GRID':
cols = math.ceil(math.sqrt(count))
rows = math.ceil(count / cols)
c = i % cols
r_idx = i // cols
step_x = size.x / max(1, (cols - 1)) if cols > 1 else 0
step_y = size.y / max(1, (rows - 1)) if rows > 1 else 0
x = -half_size.x + c * step_x
y = -half_size.y + r_idx * step_y
locations.append(Vector((x, y, 0)))
# --- 4. 回転とオフセットの計算 ---
rot_euler = Euler(settings.rotation, 'XYZ')
rot_quat = rot_euler.to_quaternion()
global_offset = Vector(settings.offset)
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=settings.radius)
# --- 5. 球体の生成 ---
for i, loc in enumerate(locations):
final_loc = (rot_quat @ loc) + global_offset
mesh = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_{i:03d}")
bm.to_mesh(mesh)
obj = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_{i+1:03d}", mesh)
preview_coll.objects.link(obj)
obj.location = final_loc
obj.rotation_euler = rot_euler
obj.data.materials.append(mat)
for poly in mesh.polygons:
poly.use_smooth = True
if hasattr(mesh, "use_auto_smooth"):
mesh.use_auto_smooth = True
bm.free()
context.view_layer.update()
finally:
_is_updating = False
# =========================================================
# プロパティとオペレータ
# =========================================================
class GROK_SphereSettings(PropertyGroup):
live_update: BoolProperty(name="Live Update", default=True)
show_preview: BoolProperty(name="Show Preview", default=True, description="プレビューの表示/非表示", update=update_preview_visibility)
space_size: FloatVectorProperty(name="Space Size", default=DEFAULT_SPACE_SIZE, update=on_prop_update)
grid_count: IntVectorProperty(name="XYZ Array", default=DEFAULT_GRID_COUNT, min=1, update=on_prop_update)
count: IntProperty(name="Total Count", default=DEFAULT_TOTAL_COUNT, min=2, update=on_prop_update)
radius: FloatProperty(name="Radius", default=DEFAULT_RADIUS, min=0.01, update=on_prop_update)
color: FloatVectorProperty(name="Color", subtype='COLOR', default=(0.35, 0.75, 0.85), min=0, max=1, update=on_prop_update)
layout_mode: EnumProperty(
name="Layout Mode",
items=[
('CUBE', "Cube (立体配列)", "3D Grid array"),
('DIAGONAL', "Diagonal (対角線)", "Line from corner to corner"),
('CIRCLE', "Circle (円環)", "Evenly spaced on a circle"),
('GRID', "Grid (平面グリッド)", "Evenly spaced flat grid")
],
default='CUBE', update=on_prop_update
)
offset: FloatVectorProperty(name="Offset (XYZ)", subtype='TRANSLATION', default=(0.0, 0.0, 0.0), update=on_prop_update)
rotation: FloatVectorProperty(name="Rotation (XYZ)", subtype='EULER', unit='ROTATION', default=(0.0, 0.0, 0.0), update=on_prop_update)
class GROK_OT_ForceUpdate(Operator):
bl_idname = f"{get_prefix()}.force_update"
bl_label = "Force Update Preview"
def execute(self, context):
build_preview(context)
return {'FINISHED'}
class GROK_OT_DetachSpheres(Operator):
bl_idname = f"{get_prefix()}.detach_spheres"
bl_label = f"Finalize & Detach {CUSTOM_TAG_NAME}"
bl_description = "現在のプレビューを独立したオブジェクトとして確定させます"
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'}
# 切り離し時に非表示になっていたら表示に戻す
preview_coll.hide_viewport = False
preview_coll.hide_render = False
run_id = datetime.now().strftime("%H%M%S")
preview_coll.name = f"{CUSTOM_TAG_NAME}_Array_{run_id}"
mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
if mat:
mat.name = f"{CUSTOM_TAG_NAME}_Mat_{run_id}"
for i, obj in enumerate(preview_coll.objects):
obj.name = f"{CUSTOM_TAG_NAME}_{run_id}_{i+1:03d}"
if obj.data:
obj.data.name = f"{CUSTOM_TAG_NAME}_Mesh_{run_id}_{i+1:03d}"
self.report({'INFO'}, f"切り離し完了: {CUSTOM_TAG_NAME}_Array_{run_id}")
# 新しいプレビューを再生成
build_preview(context)
return {'FINISHED'}
class GROK_OT_OpenURL(Operator):
bl_idname = f"{get_prefix()}.open_url"; bl_label = "Open URL"; url: StringProperty(default="")
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class GROK_OT_RemoveAllPanels(Operator):
bl_idname = f"{get_prefix()}.remove_all_panels"; bl_label = "Unregister Addon"
def execute(self, context):
# アドオン削除時にもクリーンアップを実行
cleanup_preview()
unregister()
return {'FINISHED'}
# =========================================================
# UI パネル
# =========================================================
class GROK_PT_SphereCreatorPanel(Panel):
bl_label = f"Live {CUSTOM_TAG_NAME} Array"
bl_idname = f"{PREFIX}_PT_sphere_creator"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CUSTOM_CATEGORY_NAME
bl_order = 0
def draw(self, context):
layout = self.layout
settings = context.scene.grok_sphere_settings
# 上部に表示/非表示とライブアップデートのトグル
row = layout.row()
icon_eye = 'HIDE_OFF' if settings.show_preview else 'HIDE_ON'
row.prop(settings, "show_preview", toggle=True, icon=icon_eye, text="Preview")
row.prop(settings, "live_update", toggle=True, icon='FILE_REFRESH', text="Live")
box = layout.box()
box.label(text="Space & Layout", icon='MESH_CUBE')
col = box.column(align=True)
col.prop(settings, "space_size")
col.prop(settings, "layout_mode")
if settings.layout_mode == 'CUBE':
col.prop(settings, "grid_count")
else:
col.prop(settings, "count")
layout.separator()
box = layout.box()
box.label(text="Transform (移動・回転)", icon='OBJECT_ORIGIN')
col = box.column(align=True)
col.prop(settings, "offset")
col.prop(settings, "rotation")
layout.separator()
box = layout.box()
box.label(text=f"Shape Settings", icon='MESH_UVSPHERE')
col = box.column(align=True)
col.prop(settings, "radius")
col.prop(settings, "color")
layout.separator()
if not settings.live_update:
layout.operator(GROK_OT_ForceUpdate.bl_idname, icon='FILE_REFRESH', text="Update Preview")
row = layout.row()
row.scale_y = 1.5
row.operator(GROK_OT_DetachSpheres.bl_idname, icon='DUPLICATE', text="Finalize & Detach (切り離し)")
class GROK_PT_LinksPanel(Panel):
bl_label = "Documentation Links"
bl_idname = f"{PREFIX}_PT_links"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = CUSTOM_CATEGORY_NAME; bl_order = 1
def draw(self, context):
for link in ADDON_LINKS:
op = self.layout.operator(GROK_OT_OpenURL.bl_idname, text=link["label"], icon='URL')
op.url = link["url"]
class GROK_PT_RemovePanel(Panel):
bl_label = "Remove Addon"
bl_idname = f"{PREFIX}_PT_remove"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = CUSTOM_CATEGORY_NAME; bl_order = 2; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): self.layout.operator(GROK_OT_RemoveAllPanels.bl_idname, icon='CANCEL')
classes = (
GROK_SphereSettings, GROK_OT_ForceUpdate, GROK_OT_DetachSpheres,
GROK_OT_OpenURL, GROK_OT_RemoveAllPanels, GROK_PT_SphereCreatorPanel, GROK_PT_LinksPanel, GROK_PT_RemovePanel
)
def register():
for cls in classes: bpy.utils.register_class(cls)
bpy.types.Scene.grok_sphere_settings = PointerProperty(type=GROK_SphereSettings)
# アドオン有効化時に初期プレビューを作成
bpy.app.timers.register(deferred_build, first_interval=0.5)
def unregister():
# ★ アンインストール/無効化時にプレビューのごみを完全に消す
cleanup_preview()
if hasattr(bpy.types.Scene, 'grok_sphere_settings'): del bpy.types.Scene.grok_sphere_settings
for cls in reversed(classes):
try: bpy.utils.unregister_class(cls)
except: pass
if __name__ == "__main__": register()
import bpy
import bmesh
import webbrowser
import math
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import StringProperty, FloatProperty, FloatVectorProperty, EnumProperty, PointerProperty, IntProperty, IntVectorProperty, BoolProperty
from datetime import datetime
from mathutils import Vector, Euler
# =========================================================
# 【ユーザー設定】 カスタムタグ名とカテゴリー名
# =========================================================
CUSTOM_TAG_NAME = "MySphere"
CUSTOM_CATEGORY_NAME = "[ 配列 ]"
# =========================================================
# パラメータ設定エリア (初期値と制限値)
# =========================================================
DEFAULT_SPACE_SIZE = (20.0, 20.0, 20.0)
DEFAULT_GRID_COUNT = (3, 3, 3)
DEFAULT_TOTAL_COUNT = 15
DEFAULT_RADIUS = 1.0
START_TIMESTAMP = datetime.now().strftime("%Y%m%d%H%M%S")
PREFIX = f"aiond_{START_TIMESTAMP}"
PREVIEW_COLL_NAME = f"zPreview_{CUSTOM_TAG_NAME}"
PREVIEW_MAT_NAME = f"zPreview_{CUSTOM_TAG_NAME}_Mat"
bl_info = {
"name": f"zionad Addon [{CUSTOM_TAG_NAME} Generator]",
"author": "Your Name & AI Assistant",
"version": (6, 0),
"blender": (4, 0, 0),
"location": "View3D > Sidebar > Grok Addon",
"description": "Live-update 3D arrays. Adjust parameters then Detach.",
"category": CUSTOM_CATEGORY_NAME,
}
ADDON_LINKS = ({"label": "アドオン削除パネル 20250530", "url": "<https://memo2017.hatenablog.com/entry/2025/05/30/202341>"},)
def get_prefix(): return PREFIX
# =========================================================
# ライブアップデート用関数
# =========================================================
_is_updating = False
def deferred_build():
"""Blenderのクラッシュを防ぐための遅延実行"""
build_preview(bpy.context)
return None
def on_prop_update(self, context):
"""パラメータが変更されたらタイマーで再生成を予約"""
if self.live_update and not bpy.app.timers.is_registered(deferred_build):
bpy.app.timers.register(deferred_build, first_interval=0.05)
def build_preview(context):
global _is_updating
if _is_updating: return
_is_updating = True
try:
settings = context.scene.grok_sphere_settings
# --- 1. プレビュー用コレクションの準備 ---
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
else:
preview_coll = bpy.data.collections.new(PREVIEW_COLL_NAME)
context.scene.collection.children.link(preview_coll)
# --- 2. プレビュー用マテリアルの準備 ---
mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
if not mat:
mat = bpy.data.materials.new(name=PREVIEW_MAT_NAME)
mat.use_nodes = True
mat.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (*settings.color, 1.0)
mat.diffuse_color = (*settings.color, 1.0)
# --- 3. 座標リストの計算 ---
size = Vector(settings.space_size)
half_size = size / 2.0
locations = []
if settings.layout_mode == 'CUBE':
nx, ny, nz = settings.grid_count
step_x = size.x / max(1, nx - 1) if nx > 1 else 0
step_y = size.y / max(1, ny - 1) if ny > 1 else 0
step_z = size.z / max(1, nz - 1) if nz > 1 else 0
for z_idx in range(nz):
for y_idx in range(ny):
for x_idx in range(nx):
x = -half_size.x + x_idx * step_x
y = -half_size.y + y_idx * step_y
z = -half_size.z + z_idx * step_z
locations.append(Vector((x, y, z)))
else:
count = settings.count
for i in range(count):
if settings.layout_mode == 'DIAGONAL':
factor = i / (count - 1) if count > 1 else 0
start_p = -half_size
end_p = half_size
locations.append(start_p.lerp(end_p, factor))
elif settings.layout_mode == 'CIRCLE':
angle = 2 * math.pi * (i / count)
r = min(half_size.x, half_size.y)
locations.append(Vector((r * math.cos(angle), r * math.sin(angle), 0)))
elif settings.layout_mode == 'GRID':
cols = math.ceil(math.sqrt(count))
rows = math.ceil(count / cols)
c = i % cols
r_idx = i // cols
step_x = size.x / max(1, (cols - 1)) if cols > 1 else 0
step_y = size.y / max(1, (rows - 1)) if rows > 1 else 0
x = -half_size.x + c * step_x
y = -half_size.y + r_idx * step_y
locations.append(Vector((x, y, 0)))
# --- 4. 回転とオフセット(位置ずらし)の計算 ---
rot_euler = Euler(settings.rotation, 'XYZ')
rot_quat = rot_euler.to_quaternion()
global_offset = Vector(settings.offset)
# 雛形となる1つのBMeshを作成(軽量化のため使い回す)
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=settings.radius)
# --- 5. 球体の生成 ---
for i, loc in enumerate(locations):
# 個別の座標に回転をかけ、オフセットを足す
final_loc = (rot_quat @ loc) + global_offset
mesh = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_{i:03d}")
bm.to_mesh(mesh)
obj = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_{i+1:03d}", mesh)
preview_coll.objects.link(obj)
obj.location = final_loc
obj.rotation_euler = rot_euler # オブジェクト自体の向きも一応揃える
obj.data.materials.append(mat)
for poly in mesh.polygons:
poly.use_smooth = True
if hasattr(mesh, "use_auto_smooth"):
mesh.use_auto_smooth = True
bm.free()
context.view_layer.update()
finally:
_is_updating = False
# =========================================================
# プロパティとオペレータ
# =========================================================
class GROK_SphereSettings(PropertyGroup):
live_update: BoolProperty(name="Live Update", default=True, description="自動でプレビューを更新します")
space_size: FloatVectorProperty(name="Space Size", default=DEFAULT_SPACE_SIZE, update=on_prop_update)
grid_count: IntVectorProperty(name="XYZ Array", default=DEFAULT_GRID_COUNT, min=1, update=on_prop_update)
count: IntProperty(name="Total Count", default=DEFAULT_TOTAL_COUNT, min=2, update=on_prop_update)
radius: FloatProperty(name="Radius", default=DEFAULT_RADIUS, min=0.01, update=on_prop_update)
color: FloatVectorProperty(name="Color", subtype='COLOR', default=(0.35, 0.75, 0.85), min=0, max=1, update=on_prop_update)
layout_mode: EnumProperty(
name="Layout Mode",
items=[
('CUBE', "Cube (立体配列)", "3D Grid array"),
('DIAGONAL', "Diagonal (対角線)", "Line from corner to corner"),
('CIRCLE', "Circle (円環)", "Evenly spaced on a circle"),
('GRID', "Grid (平面グリッド)", "Evenly spaced flat grid")
],
default='CUBE', update=on_prop_update
)
# 追加:中心位置ずらしと回転
offset: FloatVectorProperty(name="Offset (XYZ)", subtype='TRANSLATION', default=(0.0, 0.0, 0.0), update=on_prop_update)
rotation: FloatVectorProperty(name="Rotation (XYZ)", subtype='EULER', unit='ROTATION', default=(0.0, 0.0, 0.0), update=on_prop_update)
class GROK_OT_ForceUpdate(Operator):
bl_idname = f"{get_prefix()}.force_update"
bl_label = "Force Update Preview"
def execute(self, context):
build_preview(context)
return {'FINISHED'}
class GROK_OT_DetachSpheres(Operator):
bl_idname = f"{get_prefix()}.detach_spheres"
bl_label = f"Finalize & Detach {CUSTOM_TAG_NAME}"
bl_description = "現在のプレビューを独立したオブジェクトとして切り離し(確定させ)ます"
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'}
# 確定用の固有ID(時間)
run_id = datetime.now().strftime("%H%M%S")
# 1. コレクション名を確定済みにリネームして独立
preview_coll.name = f"{CUSTOM_TAG_NAME}_Array_{run_id}"
# 2. マテリアル名を確定済みにリネーム
mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
if mat:
mat.name = f"{CUSTOM_TAG_NAME}_Mat_{run_id}"
# 3. オブジェクトとメッシュをリネーム
for i, obj in enumerate(preview_coll.objects):
obj.name = f"{CUSTOM_TAG_NAME}_{run_id}_{i+1:03d}"
if obj.data:
obj.data.name = f"{CUSTOM_TAG_NAME}_Mesh_{run_id}_{i+1:03d}"
self.report({'INFO'}, f"切り離し完了: {CUSTOM_TAG_NAME}_Array_{run_id}")
# 切り離し終わったので、次に備えて新しいプレビューを作成
build_preview(context)
return {'FINISHED'}
class GROK_OT_OpenURL(Operator):
bl_idname = f"{get_prefix()}.open_url"; bl_label = "Open URL"; url: StringProperty(default="")
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class GROK_OT_RemoveAllPanels(Operator):
bl_idname = f"{get_prefix()}.remove_all_panels"; bl_label = "Unregister Addon"
def execute(self, context): unregister(); return {'FINISHED'}
# =========================================================
# UI パネル
# =========================================================
class GROK_PT_SphereCreatorPanel(Panel):
bl_label = f"Live {CUSTOM_TAG_NAME} Array"
bl_idname = f"{PREFIX}_PT_sphere_creator"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CUSTOM_CATEGORY_NAME
bl_order = 0
def draw(self, context):
layout = self.layout
settings = context.scene.grok_sphere_settings
layout.prop(settings, "live_update", toggle=True, icon='FILE_REFRESH')
box = layout.box()
box.label(text="Space & Layout", icon='MESH_CUBE')
col = box.column(align=True)
col.prop(settings, "space_size")
col.prop(settings, "layout_mode")
if settings.layout_mode == 'CUBE':
col.prop(settings, "grid_count")
else:
col.prop(settings, "count")
layout.separator()
box = layout.box()
box.label(text="Transform (移動・回転)", icon='OBJECT_ORIGIN')
col = box.column(align=True)
col.prop(settings, "offset")
col.prop(settings, "rotation")
layout.separator()
box = layout.box()
box.label(text=f"Shape Settings", icon='MESH_UVSPHERE')
col = box.column(align=True)
col.prop(settings, "radius")
col.prop(settings, "color")
layout.separator()
if not settings.live_update:
layout.operator(GROK_OT_ForceUpdate.bl_idname, icon='FILE_REFRESH', text="Update Preview")
# 切り離しボタン(目立つように)
row = layout.row()
row.scale_y = 1.5
row.operator(GROK_OT_DetachSpheres.bl_idname, icon='DUPLICATE', text="Finalize & Detach (切り離し)")
class GROK_PT_LinksPanel(Panel):
bl_label = "Documentation Links"
bl_idname = f"{PREFIX}_PT_links"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = CUSTOM_CATEGORY_NAME; bl_order = 1
def draw(self, context):
for link in ADDON_LINKS:
op = self.layout.operator(GROK_OT_OpenURL.bl_idname, text=link["label"], icon='URL')
op.url = link["url"]
class GROK_PT_RemovePanel(Panel):
bl_label = "Remove Addon"
bl_idname = f"{PREFIX}_PT_remove"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = CUSTOM_CATEGORY_NAME; bl_order = 2; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): self.layout.operator(GROK_OT_RemoveAllPanels.bl_idname, icon='CANCEL')
classes = (
GROK_SphereSettings, GROK_OT_ForceUpdate, GROK_OT_DetachSpheres,
GROK_OT_OpenURL, GROK_OT_RemoveAllPanels, GROK_PT_SphereCreatorPanel, GROK_PT_LinksPanel, GROK_PT_RemovePanel
)
def register():
for cls in classes: bpy.utils.register_class(cls)
bpy.types.Scene.grok_sphere_settings = PointerProperty(type=GROK_SphereSettings)
# アドオン有効化時に初期プレビューを出すためのおまじない
bpy.app.timers.register(deferred_build, first_interval=0.5)
def unregister():
if hasattr(bpy.types.Scene, 'grok_sphere_settings'): del bpy.types.Scene.grok_sphere_settings
for cls in reversed(classes):
try: bpy.utils.unregister_class(cls)
except: pass
if __name__ == "__main__": register()
import bpy
import bmesh
import webbrowser
import math
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import StringProperty, FloatProperty, FloatVectorProperty, EnumProperty, PointerProperty, IntProperty, IntVectorProperty
from datetime import datetime
from mathutils import Vector
# =========================================================
# 【ユーザー設定】 カスタムタグ名とカテゴリー名
# =========================================================
CUSTOM_TAG_NAME = "MySphere"
CUSTOM_CATEGORY_NAME = "[ 配列 ]"
# =========================================================
# パラメータ設定エリア (初期値と制限値)
# =========================================================
DEFAULT_SPACE_SIZE = (20.0, 20.0, 20.0)
DEFAULT_GRID_COUNT = (3, 3, 3) # Cube配列用のX, Y, Zの初期個数
DEFAULT_TOTAL_COUNT = 15 # Cube以外のモードでの初期総数
DEFAULT_RADIUS = 1.0
START_TIMESTAMP = datetime.now().strftime("%Y%m%d%H%M%S")
PREFIX = f"aiond_{START_TIMESTAMP}"
bl_info = {
"name": f"zionad Addon [{CUSTOM_TAG_NAME} Generator]",
"author": "Your Name & AI Assistant",
"version": (5, 0),
"blender": (4, 0, 0),
"location": "View3D > Sidebar > Grok Addon",
"description": "Generates independent 3D arrays of spheres.",
"category": CUSTOM_CATEGORY_NAME,
}
ADDON_LINKS = ({"label": "アドオン削除パネル 20250530", "url": "<https://memo2017.hatenablog.com/entry/2025/05/30/202341>"},)
def get_prefix(): return PREFIX
class GROK_SphereSettings(PropertyGroup):
space_size: FloatVectorProperty(name="Space Size", default=DEFAULT_SPACE_SIZE)
grid_count: IntVectorProperty(
name="XYZ Array",
default=DEFAULT_GRID_COUNT,
min=1,
description="Number of spheres in X, Y, and Z axes"
)
count: IntProperty(name="Total Count", default=DEFAULT_TOTAL_COUNT, min=2)
radius: FloatProperty(name="Radius", default=DEFAULT_RADIUS, min=0.01)
color: FloatVectorProperty(name="Color", subtype='COLOR', default=(0.35, 0.75, 0.85), min=0, max=1)
layout_mode: EnumProperty(
name="Layout Mode",
items=[
('CUBE', "Cube (立体配列)", "3D Grid array based on XYZ inputs (e.g., 3x3x3)"),
('DIAGONAL', "Diagonal (対角線)", "Line from corner to corner of the space"),
('CIRCLE', "Circle (円環)", "Evenly spaced on a circle in XY plane"),
('GRID', "Grid (平面グリッド)", "Evenly spaced flat grid on XY plane")
],
default='CUBE'
)
class GROK_OT_GenerateSpheres(Operator):
bl_idname = f"{get_prefix()}.generate_spheres"
bl_label = f"Generate {CUSTOM_TAG_NAME}s"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
settings = context.scene.grok_sphere_settings
# --- 1. 実行ごとにユニークなIDを作成(上書きせず独立させるため) ---
run_id = datetime.now().strftime("%H%M%S")
# 毎回新しい独立したコレクションを作成
coll_name = f"{CUSTOM_TAG_NAME}_Collection_{run_id}"
target_coll = bpy.data.collections.new(coll_name)
context.scene.collection.children.link(target_coll)
# --- 2. 独立したマテリアルを作成 ---
mat_name = f"{CUSTOM_TAG_NAME}_Mat_{run_id}"
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
mat.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (*settings.color, 1.0)
size = Vector(settings.space_size)
half_size = size / 2.0
# --- 3. 配置座標の計算リストを作成 ---
locations = []
if settings.layout_mode == 'CUBE':
nx, ny, nz = settings.grid_count
step_x = size.x / max(1, nx - 1) if nx > 1 else 0
step_y = size.y / max(1, ny - 1) if ny > 1 else 0
step_z = size.z / max(1, nz - 1) if nz > 1 else 0
for z_idx in range(nz):
for y_idx in range(ny):
for x_idx in range(nx):
x = -half_size.x + x_idx * step_x
y = -half_size.y + y_idx * step_y
z = -half_size.z + z_idx * step_z
locations.append(Vector((x, y, z)))
else:
count = settings.count
for i in range(count):
if settings.layout_mode == 'DIAGONAL':
factor = i / (count - 1) if count > 1 else 0
start_p = -half_size
end_p = half_size
locations.append(start_p.lerp(end_p, factor))
elif settings.layout_mode == 'CIRCLE':
angle = 2 * math.pi * (i / count)
r = min(half_size.x, half_size.y)
locations.append(Vector((r * math.cos(angle), r * math.sin(angle), 0)))
elif settings.layout_mode == 'GRID':
cols = math.ceil(math.sqrt(count))
rows = math.ceil(count / cols)
c = i % cols
r_idx = i // cols
step_x = size.x / max(1, (cols - 1)) if cols > 1 else 0
step_y = size.y / max(1, (rows - 1)) if rows > 1 else 0
x = -half_size.x + c * step_x
y = -half_size.y + r_idx * step_y
locations.append(Vector((x, y, 0)))
# --- 4. 球体の生成と適用 ---
for i, loc in enumerate(locations):
# メッシュやオブジェクト名にも run_id を付与して完全に独立させる
mesh = bpy.data.meshes.new(f"{CUSTOM_TAG_NAME}_Mesh_{run_id}_{i:03d}")
obj = bpy.data.objects.new(f"{CUSTOM_TAG_NAME}_{run_id}_{i+1:03d}", mesh)
target_coll.objects.link(obj)
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=settings.radius)
bm.to_mesh(mesh)
bm.free()
obj.location = loc
obj.data.materials.append(mat)
for poly in mesh.polygons:
poly.use_smooth = True
if hasattr(mesh, "use_auto_smooth"):
mesh.use_auto_smooth = True
context.view_layer.update()
return {'FINISHED'}
class GROK_OT_OpenURL(Operator):
bl_idname = f"{get_prefix()}.open_url"; bl_label = "Open URL"; url: StringProperty(default="")
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class GROK_OT_RemoveAllPanels(Operator):
bl_idname = f"{get_prefix()}.remove_all_panels"; bl_label = "Unregister Addon"
def execute(self, context): unregister(); return {'FINISHED'}
class GROK_PT_SphereCreatorPanel(Panel):
bl_label = f"3D {CUSTOM_TAG_NAME} Generator"
bl_idname = f"{PREFIX}_PT_sphere_creator"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CUSTOM_CATEGORY_NAME
bl_order = 0
def draw(self, context):
layout = self.layout
settings = context.scene.grok_sphere_settings
box = layout.box()
box.label(text="Space & Layout", icon='MESH_CUBE')
col = box.column(align=True)
col.prop(settings, "space_size")
col.prop(settings, "layout_mode")
layout.separator()
box = layout.box()
box.label(text=f"{CUSTOM_TAG_NAME} Settings", icon='MESH_UVSPHERE')
col = box.column(align=True)
# Cubeモードとそれ以外で入力フォームを出し分ける
if settings.layout_mode == 'CUBE':
row = col.row(align=True)
row.prop(settings, "grid_count")
else:
col.prop(settings, "count")
col.prop(settings, "radius")
col.prop(settings, "color")
layout.separator()
# ボタンを押すたびに新規追加作成される
layout.operator(GROK_OT_GenerateSpheres.bl_idname, icon='PLAY', text=f"Create New {CUSTOM_TAG_NAME} Array")
class GROK_PT_LinksPanel(Panel):
bl_label = "Documentation Links"
bl_idname = f"{PREFIX}_PT_links"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = CUSTOM_CATEGORY_NAME; bl_order = 1
def draw(self, context):
for link in ADDON_LINKS:
op = self.layout.operator(GROK_OT_OpenURL.bl_idname, text=link["label"], icon='URL')
op.url = link["url"]
class GROK_PT_RemovePanel(Panel):
bl_label = "Remove Addon"
bl_idname = f"{PREFIX}_PT_remove"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = CUSTOM_CATEGORY_NAME; bl_order = 2; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): self.layout.operator(GROK_OT_RemoveAllPanels.bl_idname, icon='CANCEL')
classes = (
GROK_SphereSettings, GROK_OT_GenerateSpheres,
GROK_OT_OpenURL, GROK_OT_RemoveAllPanels, GROK_PT_SphereCreatorPanel, GROK_PT_LinksPanel, GROK_PT_RemovePanel
)
def register():
for cls in classes: bpy.utils.register_class(cls)
bpy.types.Scene.grok_sphere_settings = PointerProperty(type=GROK_SphereSettings)
def unregister():
if hasattr(bpy.types.Scene, 'grok_sphere_settings'): del bpy.types.Scene.grok_sphere_settings
for cls in reversed(classes):
try: bpy.utils.unregister_class(cls)
except: pass
if __name__ == "__main__": register()