blender 今だけメモ
import bpy
import bmesh
import webbrowser
import math
import random
import colorsys
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import StringProperty, FloatProperty, FloatVectorProperty, PointerProperty, BoolProperty, IntProperty
from datetime import datetime
from mathutils import Vector, Matrix
# =========================================================
# 【ユーザー設定】 カスタムタグ名とカテゴリー名、各初期値
# =========================================================
CUSTOM_TAG_NAME = "ProjSphere"
CUSTOM_CATEGORY_NAME = "[ 投影球体 ]"
# 1. 基本設定の初期値
DEFAULT_BASE_LOCATION = (3.0, 3.0, 3.0)
DEFAULT_BASE_ANGLE = -30.0
DEFAULT_RADIUS = 0.5
# 2. 球体の初期カラー (RGBA)
DEFAULT_COLOR_MAIN = (0.8, 0.8, 0.8, 1.0)
DEFAULT_COLOR_PROJ_X = (0.8, 0.2, 0.2, 1.0)
DEFAULT_COLOR_PROJ_Y = (0.2, 0.8, 0.2, 1.0)
DEFAULT_COLOR_PROJ_Z = (0.2, 0.2, 0.8, 1.0)
# 3. 三角形の初期値
DEFAULT_TRI_THICKNESS = 0.10
DEFAULT_TRI_COLOR_FRONT = (0.3, 0.1, 0.9, 1.0) # 表:青紫
DEFAULT_TRI_COLOR_BACK = (0.9, 0.1, 0.4, 1.0) # 裏:赤紫
# 4. 同心円トーラスの初期値
DEFAULT_TORUS_COUNT = 5
DEFAULT_TORUS_SPACING = 1.00
DEFAULT_TORUS_THICKNESS = 0.10
DEFAULT_TORUS_COLOR = (0.9, 0.9, 0.3, 1.0) # トーラス色:黄色
# 5. X軸・Y軸の初期値
DEFAULT_AXIS_LENGTH = 30.0 # ±30の範囲に伸びる
DEFAULT_AXIS_THICKNESS = 0.05
DEFAULT_COLOR_AXIS_X = (1.0, 0.25, 0.25, 1.0) # 赤
DEFAULT_COLOR_AXIS_Y = (0.25, 1.0, 0.25, 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_BASE = f"zPreview_{CUSTOM_TAG_NAME}_Mat"
bl_info = {
"name": f"zionad Addon [{CUSTOM_TAG_NAME} Generator]",
"author": "Your Name & AI Assistant",
"version": (11, 4),
"blender": (4, 0, 0),
"location": "View3D > Sidebar > [ 投影球体 ]",
"description": "投影球体、三角形、同心円トーラス、X/Y基準軸 ジェネレーター",
"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
_is_unloading = False
# =========================================================
# 表示同期用のヘルパー関数 (Blender 4/5 対策)
# =========================================================
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:
result = find_layer_coll(child, name)
if result:
return result
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 preview_is_broken():
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll is None:
return True
required_suffixes = ["Main", "ProjX", "ProjY", "ProjZ", "Triangle"]
for suffix in required_suffixes:
obj = bpy.data.objects.get(f"zPreview_{CUSTOM_TAG_NAME}_{suffix}")
if not obj or obj.name not in preview_coll.objects: return True
if obj.data is None or not hasattr(obj.data, "vertices") or len(obj.data.vertices) == 0: return True
try:
settings = bpy.context.scene.grok_sphere_settings
except AttributeError:
return False
if settings.show_torus and settings.torus_count > 0:
for i in range(1, settings.torus_count + 1):
obj = bpy.data.objects.get(f"zPreview_{CUSTOM_TAG_NAME}_Torus_{i}")
if not obj or obj.name not in preview_coll.objects: return True
if obj.data is None or not hasattr(obj.data, "vertices") or len(obj.data.vertices) == 0: return True
if settings.show_axis:
for suffix in ["AxisX", "AxisY"]:
obj = bpy.data.objects.get(f"zPreview_{CUSTOM_TAG_NAME}_{suffix}")
if not obj or obj.name not in preview_coll.objects: return True
if obj.data is None or not hasattr(obj.data, "vertices") or len(obj.data.vertices) == 0: return True
return False
def update_preview_visibility(self, context):
global _is_unloading
if _is_unloading: return
if self.show_preview:
if preview_is_broken():
build_preview(context)
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll:
visible = self.show_preview
# コレクションの表示状態更新
preview_coll.hide_viewport = not visible
preview_coll.hide_render = not visible
# LayerCollection側も同期
set_collection_visibility(context, PREVIEW_COLL_NAME, visible)
# ビューポートとUIの強制更新
context.view_layer.update()
if context.screen:
for area in context.screen.areas:
if area.type == 'VIEW_3D':
area.tag_redraw()
# =========================================================
# ライブアップデート&生成関数
# =========================================================
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 preview_watchdog():
global _is_unloading
if _is_unloading: return None
try:
settings = bpy.context.scene.grok_sphere_settings
except AttributeError:
return None
if settings.show_preview:
if preview_is_broken():
build_preview(bpy.context)
return 2.0
def get_or_create_material(suffix, color):
mat_name = f"{PREVIEW_MAT_NAME_BASE}_{suffix}"
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'
mat.shadow_method = 'HASHED'
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
mat.diffuse_color = color
return mat
def get_or_create_triangle_mix_material(color_front, color_back):
mat_name = f"{PREVIEW_MAT_NAME_BASE}_Triangle_Mix"
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'
mat.shadow_method = 'HASHED'
nodes = mat.node_tree.nodes
links = mat.node_tree.links
for node in nodes: nodes.remove(node)
node_output = nodes.new(type='ShaderNodeOutputMaterial')
node_mix = nodes.new(type='ShaderNodeMixShader')
node_front = nodes.new(type='ShaderNodeBsdfPrincipled')
node_back = nodes.new(type='ShaderNodeBsdfPrincipled')
node_geom = nodes.new(type='ShaderNodeNewGeometry')
node_front.inputs["Base Color"].default_value = color_front
if "Alpha" in node_front.inputs: node_front.inputs["Alpha"].default_value = color_front[3]
node_back.inputs["Base Color"].default_value = color_back
if "Alpha" in node_back.inputs: node_back.inputs["Alpha"].default_value = color_back[3]
links.new(node_geom.outputs["Backfacing"], node_mix.inputs[0])
links.new(node_front.outputs["BSDF"], node_mix.inputs[1])
links.new(node_back.outputs["BSDF"], node_mix.inputs[2])
links.new(node_mix.outputs["Shader"], node_output.inputs["Surface"])
mat.diffuse_color = color_front
return mat
def get_or_create_torus_material(i, settings):
mat_name = f"{PREVIEW_MAT_NAME_BASE}_Torus_{i}"
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'
mat.shadow_method = 'HASHED'
if settings.torus_random_color:
random.seed(i + 1234)
r, g, b = colorsys.hsv_to_rgb(random.random(), 0.8, 0.9)
color = (r, g, b, settings.torus_color[3])
else:
color = settings.torus_color
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
mat.diffuse_color = color
return mat
def build_preview(context):
global _is_updating, _is_unloading
if _is_unloading: return
if _is_updating: return
_is_updating = True
try:
settings = context.scene.grok_sphere_settings
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll and getattr(preview_coll, 'library', None):
cleanup_preview()
preview_coll = None
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 = False
preview_coll.hide_render = False
set_collection_visibility(context, PREVIEW_COLL_NAME, True)
# ---------------------------------------------------------
# 1. 座標の計算
# ---------------------------------------------------------
bx, by, bz = settings.base_location
a_rad = math.radians(settings.base_angle)
a120 = math.radians(120.0)
loc_X = Vector((bx * math.cos(a_rad), bx * math.sin(a_rad), 0.0))
loc_Y = Vector((by * math.cos(a_rad + a120), by * math.sin(a_rad + a120), 0.0))
loc_Z = Vector((bz * math.cos(a_rad + a120 * 2), bz * math.sin(a_rad + a120 * 2), 0.0))
# ---------------------------------------------------------
# 2. 球体の生成
# ---------------------------------------------------------
items = [
("Main", Vector((bx, by, bz)), settings.show_main, settings.color_main),
("ProjX", loc_X, settings.show_proj_x, settings.color_proj_x),
("ProjY", loc_Y, settings.show_proj_y, settings.color_proj_y),
("ProjZ", loc_Z, settings.show_proj_z, settings.color_proj_z)
]
for name_suffix, loc, is_show, color in items:
mat = get_or_create_material(name_suffix, color)
mesh = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_{name_suffix}")
bm_sphere = bmesh.new()
bmesh.ops.create_uvsphere(bm_sphere, u_segments=32, v_segments=16, radius=settings.radius)
bm_sphere.to_mesh(mesh)
bm_sphere.free()
mesh.update()
obj = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_{name_suffix}", mesh)
preview_coll.objects.link(obj)
obj.location = loc
obj.data.materials.append(mat)
obj.hide_set(not is_show)
obj.hide_viewport = not is_show
obj.hide_render = not is_show
for poly in mesh.polygons:
poly.use_smooth = True
if hasattr(mesh, "use_auto_smooth"):
try: mesh.use_auto_smooth = True
except: pass
# ---------------------------------------------------------
# 3. 三角形メッシュの生成
# ---------------------------------------------------------
mat_tri_mix = get_or_create_triangle_mix_material(settings.color_tri_front, settings.color_tri_back)
mat_tri_back = get_or_create_material("Triangle_Back", settings.color_tri_back)
mesh_tri = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_Triangle")
bm_tri = bmesh.new()
v1 = bm_tri.verts.new(loc_X)
v2 = bm_tri.verts.new(loc_Y)
v3 = bm_tri.verts.new(loc_Z)
bm_tri.faces.new((v1, v2, v3))
bm_tri.to_mesh(mesh_tri)
bm_tri.free()
mesh_tri.update()
obj_tri = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_Triangle", mesh_tri)
preview_coll.objects.link(obj_tri)
obj_tri.data.materials.append(mat_tri_mix)
obj_tri.data.materials.append(mat_tri_back)
obj_tri.hide_set(not settings.show_tri)
obj_tri.hide_viewport = not settings.show_tri
obj_tri.hide_render = not settings.show_tri
if settings.tri_thickness > 0:
mod = obj_tri.modifiers.new(name="Solidify", type='SOLIDIFY')
mod.thickness = settings.tri_thickness
mod.offset = 0.0
mod.material_offset = 1
mod.material_offset_rim = 1
# ---------------------------------------------------------
# 4. 同心円トーラスの生成
# ---------------------------------------------------------
if settings.show_torus and settings.torus_count > 0:
for i in range(1, settings.torus_count + 1):
mat_torus = get_or_create_torus_material(i, settings)
mesh_torus = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_Torus_{i}")
R = i * settings.torus_spacing
r = settings.torus_thickness
N = 48
n = 12
verts = []
faces = []
for idx_R in range(N):
u = idx_R * 2.0 * math.pi / N
cos_u, sin_u = math.cos(u), math.sin(u)
for idx_r in range(n):
v = idx_r * 2.0 * math.pi / n
cos_v, sin_v = math.cos(v), math.sin(v)
x = (R + r * cos_v) * cos_u
y = (R + r * cos_v) * sin_u
z = r * sin_v
verts.append((x, y, z))
for idx_R in range(N):
next_R = (idx_R + 1) % N
for idx_r in range(n):
next_r = (idx_r + 1) % n
v0 = idx_R * n + idx_r
v1 = idx_R * n + next_r
v2 = next_R * n + next_r
v3 = next_R * n + idx_r
faces.append((v0, v1, v2, v3))
mesh_torus.from_pydata(verts, [], faces)
for poly in mesh_torus.polygons:
poly.use_smooth = True
mesh_torus.update()
obj_torus = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_Torus_{i}", mesh_torus)
preview_coll.objects.link(obj_torus)
obj_torus.location = (0.0, 0.0, 0.0)
obj_torus.data.materials.append(mat_torus)
obj_torus.hide_set(not settings.show_torus)
obj_torus.hide_viewport = not settings.show_torus
obj_torus.hide_render = not settings.show_torus
if hasattr(mesh_torus, "use_auto_smooth"):
try: mesh_torus.use_auto_smooth = True
except: pass
# ---------------------------------------------------------
# 5. X軸・Y軸の生成 (Z=0)
# ---------------------------------------------------------
if settings.show_axis:
total_depth = settings.axis_length * 2 # ±の範囲にするため2倍
# --- X軸 ---
mat_axis_x = get_or_create_material("Axis_X", settings.color_axis_x)
mesh_axis_x = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_AxisX")
bm_x = bmesh.new()
bmesh.ops.create_cone(
bm_x, cap_ends=True, cap_tris=False, segments=16,
radius1=settings.axis_thickness, radius2=settings.axis_thickness, depth=total_depth
)
# Z方向からX方向へ寝かせる(Y軸周りに90度回転)
bmesh.ops.rotate(bm_x, verts=bm_x.verts, matrix=Matrix.Rotation(math.radians(90.0), 4, 'Y'))
bm_x.to_mesh(mesh_axis_x)
bm_x.free()
mesh_axis_x.update()
obj_x = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_AxisX", mesh_axis_x)
preview_coll.objects.link(obj_x)
obj_x.data.materials.append(mat_axis_x)
obj_x.hide_set(False)
obj_x.hide_viewport = False
obj_x.hide_render = False
for poly in mesh_axis_x.polygons: poly.use_smooth = True
# --- Y軸 ---
mat_axis_y = get_or_create_material("Axis_Y", settings.color_axis_y)
mesh_axis_y = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_AxisY")
bm_y = bmesh.new()
bmesh.ops.create_cone(
bm_y, cap_ends=True, cap_tris=False, segments=16,
radius1=settings.axis_thickness, radius2=settings.axis_thickness, depth=total_depth
)
# Z方向からY方向へ寝かせる(X軸周りに90度回転)
bmesh.ops.rotate(bm_y, verts=bm_y.verts, matrix=Matrix.Rotation(math.radians(90.0), 4, 'X'))
bm_y.to_mesh(mesh_axis_y)
bm_y.free()
mesh_axis_y.update()
obj_y = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_AxisY", mesh_axis_y)
preview_coll.objects.link(obj_y)
obj_y.data.materials.append(mat_axis_y)
obj_y.hide_set(False)
obj_y.hide_viewport = False
obj_y.hide_render = False
for poly in mesh_axis_y.polygons: poly.use_smooth = True
# --- 全体表示設定の最終反映 ---
if not settings.show_preview:
preview_coll.hide_viewport = True
preview_coll.hide_render = True
set_collection_visibility(context, PREVIEW_COLL_NAME, False)
# --- ViewLayerと画面の強制更新 ---
context.view_layer.update()
if context.screen:
for area in context.screen.areas:
if area.type == 'VIEW_3D':
area.tag_redraw()
finally:
_is_updating = False
# =========================================================
# プロパティとオペレータ
# =========================================================
class GROK_SphereSettings(PropertyGroup):
live_update: BoolProperty(name="Live Update", default=True)
show_preview: BoolProperty(name="Show Global Preview", default=False, update=update_preview_visibility)
base_location: FloatVectorProperty(name="Base Location", default=DEFAULT_BASE_LOCATION, update=on_prop_update)
base_angle: FloatProperty(name="Base Angle (X-Proj)", default=DEFAULT_BASE_ANGLE, update=on_prop_update)
radius: FloatProperty(name="Sphere Radius", default=DEFAULT_RADIUS, min=0.01, update=on_prop_update)
tri_thickness: FloatProperty(name="Tri Thickness", default=DEFAULT_TRI_THICKNESS, min=0.0, update=on_prop_update)
show_main: BoolProperty(name="Show Main", default=False, update=on_prop_update)
color_main: FloatVectorProperty(name="Main Color", subtype='COLOR', size=4, default=DEFAULT_COLOR_MAIN, min=0, max=1, update=on_prop_update)
show_proj_x: BoolProperty(name="Show Proj X", default=False, update=on_prop_update)
color_proj_x: FloatVectorProperty(name="Proj X Color", subtype='COLOR', size=4, default=DEFAULT_COLOR_PROJ_X, min=0, max=1, update=on_prop_update)
show_proj_y: BoolProperty(name="Show Proj Y", default=False, update=on_prop_update)
color_proj_y: FloatVectorProperty(name="Proj Y Color", subtype='COLOR', size=4, default=DEFAULT_COLOR_PROJ_Y, min=0, max=1, update=on_prop_update)
show_proj_z: BoolProperty(name="Show Proj Z", default=False, update=on_prop_update)
color_proj_z: FloatVectorProperty(name="Proj Z Color", subtype='COLOR', size=4, default=DEFAULT_COLOR_PROJ_Z, min=0, max=1, update=on_prop_update)
show_tri: BoolProperty(name="Show Triangle", default=False, update=on_prop_update)
color_tri_front: FloatVectorProperty(name="Tri Front Color", subtype='COLOR', size=4, default=DEFAULT_TRI_COLOR_FRONT, min=0, max=1, update=on_prop_update)
color_tri_back: FloatVectorProperty(name="Tri Back Color", subtype='COLOR', size=4, default=DEFAULT_TRI_COLOR_BACK, min=0, max=1, update=on_prop_update)
show_torus: BoolProperty(name="Show Torus", default=False, update=on_prop_update)
torus_count: IntProperty(name="Torus Count", default=DEFAULT_TORUS_COUNT, min=0, max=100, update=on_prop_update)
torus_spacing: FloatProperty(name="Torus Spacing", default=DEFAULT_TORUS_SPACING, min=0.01, update=on_prop_update)
torus_thickness: FloatProperty(name="Torus Thickness", default=DEFAULT_TORUS_THICKNESS, min=0.001, update=on_prop_update)
torus_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, default=DEFAULT_TORUS_COLOR, min=0, max=1, update=on_prop_update)
torus_random_color: BoolProperty(name="Random Color", default=False, update=on_prop_update)
# X軸・Y軸 用のプロパティ
show_axis: BoolProperty(name="Show Axes", default=False, update=on_prop_update)
axis_length: FloatProperty(name="Axis Length (±)", default=DEFAULT_AXIS_LENGTH, min=1.0, update=on_prop_update)
axis_thickness: FloatProperty(name="Axis Thickness", default=DEFAULT_AXIS_THICKNESS, min=0.001, update=on_prop_update)
color_axis_x: FloatVectorProperty(name="X Axis Color", subtype='COLOR', size=4, default=DEFAULT_COLOR_AXIS_X, min=0, max=1, update=on_prop_update)
color_axis_y: FloatVectorProperty(name="Y Axis Color", subtype='COLOR', size=4, default=DEFAULT_COLOR_AXIS_Y, min=0, max=1, update=on_prop_update)
class GROK_OT_ForceUpdate(Operator):
bl_idname = f"{get_prefix()}.force_update"
bl_label = "Force Update"
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_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
set_collection_visibility(context, PREVIEW_COLL_NAME, True)
run_id = datetime.now().strftime("%H%M%S")
preview_coll.name = f"{CUSTOM_TAG_NAME}_{run_id}"
for obj in preview_coll.objects:
clean_name = obj.name.replace(f"zPreview_{CUSTOM_TAG_NAME}_", "")
obj.name = f"{CUSTOM_TAG_NAME}_{clean_name}_{run_id}"
if obj.data: obj.data.name = f"Mesh_{clean_name}_{run_id}"
for mat_slot in obj.material_slots:
if mat_slot.material and PREVIEW_MAT_NAME_BASE in mat_slot.material.name:
new_mat_name = mat_slot.material.name.replace(PREVIEW_MAT_NAME_BASE, f"{CUSTOM_TAG_NAME}_Mat") + f"_{run_id}"
mat_slot.material.name = new_mat_name
self.report({'INFO'}, f"切り離し完了: {CUSTOM_TAG_NAME}_{run_id}")
context.view_layer.update()
if context.screen:
for area in context.screen.areas:
if area.type == 'VIEW_3D':
area.tag_redraw()
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} 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
row = layout.row()
row.prop(settings, "show_preview", text="Global Preview")
row.prop(settings, "live_update", text="Live Update")
box = layout.box()
box.label(text="Global Settings", icon='OBJECT_ORIGIN')
col = box.column(align=True)
col.prop(settings, "base_location")
col.prop(settings, "base_angle", text="Base Angle (X-Proj)")
col.prop(settings, "radius")
layout.separator()
box = layout.box()
box.label(text="Spheres (Visibility & Color)", icon='SPHERE')
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_main", text="本体 (Main)")
split.prop(settings, "color_main", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj_x", text="X成分投影 (-30°)")
split.prop(settings, "color_proj_x", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj_y", text="Y成分投影 (90°)")
split.prop(settings, "color_proj_y", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj_z", text="Z成分投影 (210°)")
split.prop(settings, "color_proj_z", text="")
layout.separator()
box = layout.box()
box.label(text="Triangle (Z+ Front / Z- Back)", icon='MESH_PLANE')
row = box.row()
row.prop(settings, "show_tri", text="三角形表示")
row.prop(settings, "tri_thickness", text="厚み")
row = box.row()
split = row.split(factor=0.5)
split.prop(settings, "color_tri_front", text="表(Front)")
split.prop(settings, "color_tri_back", text="裏(Back)")
layout.separator()
box = layout.box()
box.label(text="Concentric Torus (Z=0)", icon='MESH_TORUS')
row = box.row()
row.prop(settings, "show_torus", text="トーラス表示")
if settings.show_torus:
col = box.column(align=True)
col.prop(settings, "torus_count", text="個数 (0-100)")
col.prop(settings, "torus_spacing", text="間隔ステップ")
col.prop(settings, "torus_thickness", text="管の太さ")
row_c = col.row()
row_c.prop(settings, "torus_random_color", text="色をランダム化")
if not settings.torus_random_color:
row_c.prop(settings, "torus_color", text="")
layout.separator()
box = layout.box()
box.label(text="Reference Axes (Z=0)", icon='ORIENTATION_GLOBAL')
row = box.row()
row.prop(settings, "show_axis", text="X/Y軸を表示")
if settings.show_axis:
col = box.column(align=True)
col.prop(settings, "axis_length", text="長さ (±範囲)")
col.prop(settings, "axis_thickness", text="太さ")
row_c = col.row()
split = row_c.split(factor=0.5)
split.prop(settings, "color_axis_x", text="X軸色")
split.prop(settings, "color_axis_y", text="Y軸色")
layout.separator()
if not settings.live_update:
layout.operator(GROK_OT_ForceUpdate.bl_idname, icon='FILE_REFRESH')
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
bl_options = {'DEFAULT_CLOSED'}
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():
global _is_unloading
_is_unloading = False
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)
bpy.app.timers.register(preview_watchdog, first_interval=2.0)
def unregister():
global _is_unloading
_is_unloading = True
if bpy.app.timers.is_registered(preview_watchdog):
bpy.app.timers.unregister(preview_watchdog)
if bpy.app.timers.is_registered(deferred_build):
bpy.app.timers.unregister(deferred_build)
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
import random
import colorsys
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import StringProperty, FloatProperty, FloatVectorProperty, PointerProperty, BoolProperty, IntProperty
from datetime import datetime
from mathutils import Vector
# =========================================================
# 【ユーザー設定】 カスタムタグ名とカテゴリー名、各初期値
# =========================================================
CUSTOM_TAG_NAME = "ProjSphere"
CUSTOM_CATEGORY_NAME = "[ 投影球体 ]"
# 1. 基本設定の初期値
DEFAULT_BASE_LOCATION = (3.0, 3.0, 3.0)
DEFAULT_BASE_ANGLE = -30.0
DEFAULT_RADIUS = 0.5
# 2. 球体の初期カラー (RGBA)
DEFAULT_COLOR_MAIN = (0.8, 0.8, 0.8, 1.0)
DEFAULT_COLOR_PROJ_X = (0.8, 0.2, 0.2, 1.0)
DEFAULT_COLOR_PROJ_Y = (0.2, 0.8, 0.2, 1.0)
DEFAULT_COLOR_PROJ_Z = (0.2, 0.2, 0.8, 1.0)
# 3. 三角形の初期値
DEFAULT_TRI_THICKNESS = 0.10
DEFAULT_TRI_COLOR_FRONT = (0.3, 0.1, 0.9, 1.0) # 表:青紫
DEFAULT_TRI_COLOR_BACK = (0.9, 0.1, 0.4, 1.0) # 裏:赤紫
# 4. 同心円トーラスの初期値
DEFAULT_TORUS_COUNT = 5
DEFAULT_TORUS_SPACING = 1.00
DEFAULT_TORUS_THICKNESS = 0.10
DEFAULT_TORUS_COLOR = (0.9, 0.9, 0.3, 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_BASE = f"zPreview_{CUSTOM_TAG_NAME}_Mat"
bl_info = {
"name": f"zionad Addon [{CUSTOM_TAG_NAME} Generator]",
"author": "Your Name & AI Assistant",
"version": (11, 3),
"blender": (4, 0, 0),
"location": "View3D > Sidebar > [ 投影球体 ]",
"description": "投影球体、表裏色違い三角形、同心円トーラスジェネレーター(Blender 4.x/5.x 表示最適化版)",
"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
_is_unloading = False
# =========================================================
# 表示同期用のヘルパー関数 (Blender 4/5 対策)
# =========================================================
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:
result = find_layer_coll(child, name)
if result:
return result
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 preview_is_broken():
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll is None:
return True
required_suffixes = ["Main", "ProjX", "ProjY", "ProjZ", "Triangle"]
for suffix in required_suffixes:
obj = bpy.data.objects.get(f"zPreview_{CUSTOM_TAG_NAME}_{suffix}")
if not obj or obj.name not in preview_coll.objects: return True
if obj.data is None or not hasattr(obj.data, "vertices") or len(obj.data.vertices) == 0: return True
try:
settings = bpy.context.scene.grok_sphere_settings
except AttributeError:
return False
if settings.show_torus and settings.torus_count > 0:
for i in range(1, settings.torus_count + 1):
obj = bpy.data.objects.get(f"zPreview_{CUSTOM_TAG_NAME}_Torus_{i}")
if not obj or obj.name not in preview_coll.objects: return True
if obj.data is None or not hasattr(obj.data, "vertices") or len(obj.data.vertices) == 0: return True
return False
def update_preview_visibility(self, context):
global _is_unloading
if _is_unloading: return
if self.show_preview:
if preview_is_broken():
build_preview(context)
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll:
visible = self.show_preview
# コレクションの表示状態更新
preview_coll.hide_viewport = not visible
preview_coll.hide_render = not visible
# LayerCollection側も同期 (Blender 4/5 対策)
set_collection_visibility(context, PREVIEW_COLL_NAME, visible)
# ビューポートとUIの強制更新
context.view_layer.update()
if context.screen:
for area in context.screen.areas:
if area.type == 'VIEW_3D':
area.tag_redraw()
# =========================================================
# ライブアップデート&生成関数
# =========================================================
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 preview_watchdog():
global _is_unloading
if _is_unloading: return None
try:
settings = bpy.context.scene.grok_sphere_settings
except AttributeError:
return None
if settings.show_preview:
if preview_is_broken():
build_preview(bpy.context)
return 2.0
def get_or_create_material(suffix, color):
mat_name = f"{PREVIEW_MAT_NAME_BASE}_{suffix}"
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'
mat.shadow_method = 'HASHED'
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
mat.diffuse_color = color
return mat
def get_or_create_triangle_mix_material(color_front, color_back):
mat_name = f"{PREVIEW_MAT_NAME_BASE}_Triangle_Mix"
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'
mat.shadow_method = 'HASHED'
nodes = mat.node_tree.nodes
links = mat.node_tree.links
for node in nodes: nodes.remove(node)
node_output = nodes.new(type='ShaderNodeOutputMaterial')
node_mix = nodes.new(type='ShaderNodeMixShader')
node_front = nodes.new(type='ShaderNodeBsdfPrincipled')
node_back = nodes.new(type='ShaderNodeBsdfPrincipled')
node_geom = nodes.new(type='ShaderNodeNewGeometry')
node_front.inputs["Base Color"].default_value = color_front
if "Alpha" in node_front.inputs: node_front.inputs["Alpha"].default_value = color_front[3]
node_back.inputs["Base Color"].default_value = color_back
if "Alpha" in node_back.inputs: node_back.inputs["Alpha"].default_value = color_back[3]
links.new(node_geom.outputs["Backfacing"], node_mix.inputs[0])
links.new(node_front.outputs["BSDF"], node_mix.inputs[1])
links.new(node_back.outputs["BSDF"], node_mix.inputs[2])
links.new(node_mix.outputs["Shader"], node_output.inputs["Surface"])
mat.diffuse_color = color_front
return mat
def get_or_create_torus_material(i, settings):
mat_name = f"{PREVIEW_MAT_NAME_BASE}_Torus_{i}"
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'
mat.shadow_method = 'HASHED'
if settings.torus_random_color:
random.seed(i + 1234)
r, g, b = colorsys.hsv_to_rgb(random.random(), 0.8, 0.9)
color = (r, g, b, settings.torus_color[3])
else:
color = settings.torus_color
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
mat.diffuse_color = color
return mat
def build_preview(context):
global _is_updating, _is_unloading
if _is_unloading: return
if _is_updating: return
_is_updating = True
try:
settings = context.scene.grok_sphere_settings
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll and getattr(preview_coll, 'library', None):
cleanup_preview()
preview_coll = None
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 = False
preview_coll.hide_render = False
set_collection_visibility(context, PREVIEW_COLL_NAME, True)
# ---------------------------------------------------------
# 1. 座標の計算
# ---------------------------------------------------------
bx, by, bz = settings.base_location
a_rad = math.radians(settings.base_angle)
a120 = math.radians(120.0)
loc_X = Vector((bx * math.cos(a_rad), bx * math.sin(a_rad), 0.0))
loc_Y = Vector((by * math.cos(a_rad + a120), by * math.sin(a_rad + a120), 0.0))
loc_Z = Vector((bz * math.cos(a_rad + a120 * 2), bz * math.sin(a_rad + a120 * 2), 0.0))
# ---------------------------------------------------------
# 2. 球体の生成
# ---------------------------------------------------------
items = [
("Main", Vector((bx, by, bz)), settings.show_main, settings.color_main),
("ProjX", loc_X, settings.show_proj_x, settings.color_proj_x),
("ProjY", loc_Y, settings.show_proj_y, settings.color_proj_y),
("ProjZ", loc_Z, settings.show_proj_z, settings.color_proj_z)
]
for name_suffix, loc, is_show, color in items:
mat = get_or_create_material(name_suffix, color)
mesh = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_{name_suffix}")
bm_sphere = bmesh.new()
bmesh.ops.create_uvsphere(bm_sphere, u_segments=32, v_segments=16, radius=settings.radius)
bm_sphere.to_mesh(mesh)
bm_sphere.free()
mesh.update()
obj = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_{name_suffix}", mesh)
preview_coll.objects.link(obj)
obj.location = loc
obj.data.materials.append(mat)
# --- 個別オブジェクトの表示同期 ---
obj.hide_set(not is_show)
obj.hide_viewport = not is_show
obj.hide_render = not is_show
for poly in mesh.polygons:
poly.use_smooth = True
# (Blender 4.0以前互換用。4.1以降は影響なし)
if hasattr(mesh, "use_auto_smooth"):
try: mesh.use_auto_smooth = True
except: pass
# ---------------------------------------------------------
# 3. 三角形メッシュの生成
# ---------------------------------------------------------
mat_tri_mix = get_or_create_triangle_mix_material(settings.color_tri_front, settings.color_tri_back)
mat_tri_back = get_or_create_material("Triangle_Back", settings.color_tri_back)
mesh_tri = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_Triangle")
bm_tri = bmesh.new()
v1 = bm_tri.verts.new(loc_X)
v2 = bm_tri.verts.new(loc_Y)
v3 = bm_tri.verts.new(loc_Z)
bm_tri.faces.new((v1, v2, v3))
bm_tri.to_mesh(mesh_tri)
bm_tri.free()
mesh_tri.update()
obj_tri = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_Triangle", mesh_tri)
preview_coll.objects.link(obj_tri)
obj_tri.data.materials.append(mat_tri_mix)
obj_tri.data.materials.append(mat_tri_back)
obj_tri.hide_set(not settings.show_tri)
obj_tri.hide_viewport = not settings.show_tri
obj_tri.hide_render = not settings.show_tri
if settings.tri_thickness > 0:
mod = obj_tri.modifiers.new(name="Solidify", type='SOLIDIFY')
mod.thickness = settings.tri_thickness
mod.offset = 0.0
mod.material_offset = 1
mod.material_offset_rim = 1
# ---------------------------------------------------------
# 4. 同心円トーラスの生成
# ---------------------------------------------------------
if settings.show_torus and settings.torus_count > 0:
for i in range(1, settings.torus_count + 1):
mat_torus = get_or_create_torus_material(i, settings)
mesh_torus = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_Torus_{i}")
R = i * settings.torus_spacing
r = settings.torus_thickness
N = 48
n = 12
verts = []
faces = []
for idx_R in range(N):
u = idx_R * 2.0 * math.pi / N
cos_u, sin_u = math.cos(u), math.sin(u)
for idx_r in range(n):
v = idx_r * 2.0 * math.pi / n
cos_v, sin_v = math.cos(v), math.sin(v)
x = (R + r * cos_v) * cos_u
y = (R + r * cos_v) * sin_u
z = r * sin_v
verts.append((x, y, z))
for idx_R in range(N):
next_R = (idx_R + 1) % N
for idx_r in range(n):
next_r = (idx_r + 1) % n
v0 = idx_R * n + idx_r
v1 = idx_R * n + next_r
v2 = next_R * n + next_r
v3 = next_R * n + idx_r
faces.append((v0, v1, v2, v3))
mesh_torus.from_pydata(verts, [], faces)
for poly in mesh_torus.polygons:
poly.use_smooth = True
mesh_torus.update()
obj_torus = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_Torus_{i}", mesh_torus)
preview_coll.objects.link(obj_torus)
obj_torus.location = (0.0, 0.0, 0.0)
obj_torus.data.materials.append(mat_torus)
obj_torus.hide_set(not settings.show_torus)
obj_torus.hide_viewport = not settings.show_torus
obj_torus.hide_render = not settings.show_torus
if hasattr(mesh_torus, "use_auto_smooth"):
try: mesh_torus.use_auto_smooth = True
except: pass
# --- 全体表示設定の最終反映 ---
if not settings.show_preview:
preview_coll.hide_viewport = True
preview_coll.hide_render = True
set_collection_visibility(context, PREVIEW_COLL_NAME, False)
# --- ViewLayerと画面の強制更新 ---
context.view_layer.update()
if context.screen:
for area in context.screen.areas:
if area.type == 'VIEW_3D':
area.tag_redraw()
finally:
_is_updating = False
# =========================================================
# プロパティとオペレータ
# =========================================================
class GROK_SphereSettings(PropertyGroup):
live_update: BoolProperty(name="Live Update", default=True)
show_preview: BoolProperty(name="Show Global Preview", default=False, update=update_preview_visibility)
base_location: FloatVectorProperty(name="Base Location", default=DEFAULT_BASE_LOCATION, update=on_prop_update)
base_angle: FloatProperty(name="Base Angle (X-Proj)", default=DEFAULT_BASE_ANGLE, update=on_prop_update)
radius: FloatProperty(name="Sphere Radius", default=DEFAULT_RADIUS, min=0.01, update=on_prop_update)
tri_thickness: FloatProperty(name="Tri Thickness", default=DEFAULT_TRI_THICKNESS, min=0.0, update=on_prop_update)
show_main: BoolProperty(name="Show Main", default=False, update=on_prop_update)
color_main: FloatVectorProperty(name="Main Color", subtype='COLOR', size=4, default=DEFAULT_COLOR_MAIN, min=0, max=1, update=on_prop_update)
show_proj_x: BoolProperty(name="Show Proj X", default=False, update=on_prop_update)
color_proj_x: FloatVectorProperty(name="Proj X Color", subtype='COLOR', size=4, default=DEFAULT_COLOR_PROJ_X, min=0, max=1, update=on_prop_update)
show_proj_y: BoolProperty(name="Show Proj Y", default=False, update=on_prop_update)
color_proj_y: FloatVectorProperty(name="Proj Y Color", subtype='COLOR', size=4, default=DEFAULT_COLOR_PROJ_Y, min=0, max=1, update=on_prop_update)
show_proj_z: BoolProperty(name="Show Proj Z", default=False, update=on_prop_update)
color_proj_z: FloatVectorProperty(name="Proj Z Color", subtype='COLOR', size=4, default=DEFAULT_COLOR_PROJ_Z, min=0, max=1, update=on_prop_update)
show_tri: BoolProperty(name="Show Triangle", default=False, update=on_prop_update)
color_tri_front: FloatVectorProperty(name="Tri Front Color", subtype='COLOR', size=4, default=DEFAULT_TRI_COLOR_FRONT, min=0, max=1, update=on_prop_update)
color_tri_back: FloatVectorProperty(name="Tri Back Color", subtype='COLOR', size=4, default=DEFAULT_TRI_COLOR_BACK, min=0, max=1, update=on_prop_update)
show_torus: BoolProperty(name="Show Torus", default=False, update=on_prop_update)
torus_count: IntProperty(name="Torus Count", default=DEFAULT_TORUS_COUNT, min=0, max=100, update=on_prop_update)
torus_spacing: FloatProperty(name="Torus Spacing", default=DEFAULT_TORUS_SPACING, min=0.01, update=on_prop_update)
torus_thickness: FloatProperty(name="Torus Thickness", default=DEFAULT_TORUS_THICKNESS, min=0.001, update=on_prop_update)
torus_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, default=DEFAULT_TORUS_COLOR, min=0, max=1, update=on_prop_update)
torus_random_color: BoolProperty(name="Random Color", default=False, update=on_prop_update)
class GROK_OT_ForceUpdate(Operator):
bl_idname = f"{get_prefix()}.force_update"
bl_label = "Force Update"
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_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
set_collection_visibility(context, PREVIEW_COLL_NAME, True)
run_id = datetime.now().strftime("%H%M%S")
preview_coll.name = f"{CUSTOM_TAG_NAME}_{run_id}"
for obj in preview_coll.objects:
clean_name = obj.name.replace(f"zPreview_{CUSTOM_TAG_NAME}_", "")
obj.name = f"{CUSTOM_TAG_NAME}_{clean_name}_{run_id}"
if obj.data: obj.data.name = f"Mesh_{clean_name}_{run_id}"
for mat_slot in obj.material_slots:
if mat_slot.material and PREVIEW_MAT_NAME_BASE in mat_slot.material.name:
new_mat_name = mat_slot.material.name.replace(PREVIEW_MAT_NAME_BASE, f"{CUSTOM_TAG_NAME}_Mat") + f"_{run_id}"
mat_slot.material.name = new_mat_name
self.report({'INFO'}, f"切り離し完了: {CUSTOM_TAG_NAME}_{run_id}")
context.view_layer.update()
if context.screen:
for area in context.screen.areas:
if area.type == 'VIEW_3D':
area.tag_redraw()
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} 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
row = layout.row()
row.prop(settings, "show_preview", text="Global Preview")
row.prop(settings, "live_update", text="Live Update")
box = layout.box()
box.label(text="Global Settings", icon='OBJECT_ORIGIN')
col = box.column(align=True)
col.prop(settings, "base_location")
col.prop(settings, "base_angle", text="Base Angle (X-Proj)")
col.prop(settings, "radius")
layout.separator()
box = layout.box()
box.label(text="Spheres (Visibility & Color)", icon='SPHERE')
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_main", text="本体 (Main)")
split.prop(settings, "color_main", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj_x", text="X成分投影 (-30°)")
split.prop(settings, "color_proj_x", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj_y", text="Y成分投影 (90°)")
split.prop(settings, "color_proj_y", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj_z", text="Z成分投影 (210°)")
split.prop(settings, "color_proj_z", text="")
layout.separator()
box = layout.box()
box.label(text="Triangle (Z+ Front / Z- Back)", icon='MESH_PLANE')
row = box.row()
row.prop(settings, "show_tri", text="三角形表示")
row.prop(settings, "tri_thickness", text="厚み")
row = box.row()
split = row.split(factor=0.5)
split.prop(settings, "color_tri_front", text="表(Front)")
split.prop(settings, "color_tri_back", text="裏(Back)")
layout.separator()
box = layout.box()
box.label(text="Concentric Torus (Z=0)", icon='MESH_TORUS')
row = box.row()
row.prop(settings, "show_torus", text="トーラス表示")
if settings.show_torus:
col = box.column(align=True)
col.prop(settings, "torus_count", text="個数 (0-100)")
col.prop(settings, "torus_spacing", text="間隔ステップ")
col.prop(settings, "torus_thickness", text="管の太さ")
row_c = col.row()
row_c.prop(settings, "torus_random_color", text="色をランダム化")
if not settings.torus_random_color:
row_c.prop(settings, "torus_color", text="")
layout.separator()
if not settings.live_update:
layout.operator(GROK_OT_ForceUpdate.bl_idname, icon='FILE_REFRESH')
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
bl_options = {'DEFAULT_CLOSED'}
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():
global _is_unloading
_is_unloading = False
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)
bpy.app.timers.register(preview_watchdog, first_interval=2.0)
def unregister():
global _is_unloading
_is_unloading = True
if bpy.app.timers.is_registered(preview_watchdog):
bpy.app.timers.unregister(preview_watchdog)
if bpy.app.timers.is_registered(deferred_build):
bpy.app.timers.unregister(deferred_build)
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
import random
import colorsys
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import StringProperty, FloatProperty, FloatVectorProperty, PointerProperty, BoolProperty, IntProperty
from datetime import datetime
from mathutils import Vector
# =========================================================
# 【ユーザー設定】 カスタムタグ名とカテゴリー名、各初期値
# =========================================================
CUSTOM_TAG_NAME = "ProjSphere"
CUSTOM_CATEGORY_NAME = "[ 投影球体 ]"
# 1. 基本設定の初期値
DEFAULT_BASE_LOCATION = (3.0, 3.0, 3.0)
DEFAULT_BASE_ANGLE = -30.0
DEFAULT_RADIUS = 0.5
# 2. 球体の初期カラー (RGBA)
DEFAULT_COLOR_MAIN = (0.8, 0.8, 0.8, 1.0)
DEFAULT_COLOR_PROJ_X = (0.8, 0.2, 0.2, 1.0)
DEFAULT_COLOR_PROJ_Y = (0.2, 0.8, 0.2, 1.0)
DEFAULT_COLOR_PROJ_Z = (0.2, 0.2, 0.8, 1.0)
# 3. 三角形の初期値
DEFAULT_TRI_THICKNESS = 0.10
DEFAULT_TRI_COLOR_FRONT = (0.3, 0.1, 0.9, 1.0) # 表:青紫
DEFAULT_TRI_COLOR_BACK = (0.9, 0.1, 0.4, 1.0) # 裏:赤紫
# 4. 同心円トーラスの初期値
DEFAULT_TORUS_COUNT = 5
DEFAULT_TORUS_SPACING = 1.00
DEFAULT_TORUS_THICKNESS = 0.10
DEFAULT_TORUS_COLOR = (0.9, 0.9, 0.3, 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_BASE = f"zPreview_{CUSTOM_TAG_NAME}_Mat"
bl_info = {
"name": f"zionad Addon [{CUSTOM_TAG_NAME} Generator]",
"author": "Your Name & AI Assistant",
"version": (11, 2),
"blender": (4, 0, 0),
"location": "View3D > Sidebar > [ 投影球体 ]",
"description": "投影球体、表裏色違い三角形、同心円トーラスジェネレーター(初期値集約版)。",
"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
_is_unloading = False
# =========================================================
# プレビュー管理・クリーンアップ関数
# =========================================================
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 preview_is_broken():
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll is None:
return True
required_suffixes = ["Main", "ProjX", "ProjY", "ProjZ", "Triangle"]
for suffix in required_suffixes:
obj = bpy.data.objects.get(f"zPreview_{CUSTOM_TAG_NAME}_{suffix}")
if not obj or obj.name not in preview_coll.objects: return True
if obj.data is None or not hasattr(obj.data, "vertices") or len(obj.data.vertices) == 0: return True
try:
settings = bpy.context.scene.grok_sphere_settings
except AttributeError:
return False
if settings.show_torus and settings.torus_count > 0:
for i in range(1, settings.torus_count + 1):
obj = bpy.data.objects.get(f"zPreview_{CUSTOM_TAG_NAME}_Torus_{i}")
if not obj or obj.name not in preview_coll.objects: return True
if obj.data is None or not hasattr(obj.data, "vertices") or len(obj.data.vertices) == 0: return True
return False
def update_preview_visibility(self, context):
global _is_unloading
if _is_unloading: return
if self.show_preview:
if preview_is_broken():
build_preview(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
# =========================================================
# ライブアップデート&生成関数
# =========================================================
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 preview_watchdog():
global _is_unloading
if _is_unloading: return None
try:
settings = bpy.context.scene.grok_sphere_settings
except AttributeError:
return None
if settings.show_preview:
if preview_is_broken():
build_preview(bpy.context)
return 2.0
def get_or_create_material(suffix, color):
mat_name = f"{PREVIEW_MAT_NAME_BASE}_{suffix}"
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'
mat.shadow_method = 'HASHED'
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
mat.diffuse_color = color
return mat
def get_or_create_triangle_mix_material(color_front, color_back):
mat_name = f"{PREVIEW_MAT_NAME_BASE}_Triangle_Mix"
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'
mat.shadow_method = 'HASHED'
nodes = mat.node_tree.nodes
links = mat.node_tree.links
for node in nodes: nodes.remove(node)
node_output = nodes.new(type='ShaderNodeOutputMaterial')
node_mix = nodes.new(type='ShaderNodeMixShader')
node_front = nodes.new(type='ShaderNodeBsdfPrincipled')
node_back = nodes.new(type='ShaderNodeBsdfPrincipled')
node_geom = nodes.new(type='ShaderNodeNewGeometry')
node_front.inputs["Base Color"].default_value = color_front
if "Alpha" in node_front.inputs: node_front.inputs["Alpha"].default_value = color_front[3]
node_back.inputs["Base Color"].default_value = color_back
if "Alpha" in node_back.inputs: node_back.inputs["Alpha"].default_value = color_back[3]
links.new(node_geom.outputs["Backfacing"], node_mix.inputs[0])
links.new(node_front.outputs["BSDF"], node_mix.inputs[1])
links.new(node_back.outputs["BSDF"], node_mix.inputs[2])
links.new(node_mix.outputs["Shader"], node_output.inputs["Surface"])
mat.diffuse_color = color_front
return mat
def get_or_create_torus_material(i, settings):
mat_name = f"{PREVIEW_MAT_NAME_BASE}_Torus_{i}"
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'
mat.shadow_method = 'HASHED'
if settings.torus_random_color:
random.seed(i + 1234)
r, g, b = colorsys.hsv_to_rgb(random.random(), 0.8, 0.9)
color = (r, g, b, settings.torus_color[3])
else:
color = settings.torus_color
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
mat.diffuse_color = color
return mat
def build_preview(context):
global _is_updating, _is_unloading
if _is_unloading: return
if _is_updating: return
_is_updating = True
try:
settings = context.scene.grok_sphere_settings
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll and getattr(preview_coll, 'library', None):
cleanup_preview()
preview_coll = None
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
# ---------------------------------------------------------
# 1. 座標の計算
# ---------------------------------------------------------
bx, by, bz = settings.base_location
a_rad = math.radians(settings.base_angle)
a120 = math.radians(120.0)
loc_X = Vector((bx * math.cos(a_rad), bx * math.sin(a_rad), 0.0))
loc_Y = Vector((by * math.cos(a_rad + a120), by * math.sin(a_rad + a120), 0.0))
loc_Z = Vector((bz * math.cos(a_rad + a120 * 2), bz * math.sin(a_rad + a120 * 2), 0.0))
# ---------------------------------------------------------
# 2. 球体の生成
# ---------------------------------------------------------
items = [
("Main", Vector((bx, by, bz)), settings.show_main, settings.color_main),
("ProjX", loc_X, settings.show_proj_x, settings.color_proj_x),
("ProjY", loc_Y, settings.show_proj_y, settings.color_proj_y),
("ProjZ", loc_Z, settings.show_proj_z, settings.color_proj_z)
]
for name_suffix, loc, is_show, color in items:
mat = get_or_create_material(name_suffix, color)
mesh = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_{name_suffix}")
bm_sphere = bmesh.new()
bmesh.ops.create_uvsphere(bm_sphere, u_segments=32, v_segments=16, radius=settings.radius)
bm_sphere.to_mesh(mesh)
bm_sphere.free()
mesh.update()
obj = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_{name_suffix}", mesh)
preview_coll.objects.link(obj)
obj.location = loc
obj.data.materials.append(mat)
obj.hide_viewport = not is_show
obj.hide_render = not is_show
for poly in mesh.polygons:
poly.use_smooth = True
if hasattr(mesh, "use_auto_smooth"):
mesh.use_auto_smooth = True
# ---------------------------------------------------------
# 3. 三角形メッシュの生成
# ---------------------------------------------------------
mat_tri_mix = get_or_create_triangle_mix_material(settings.color_tri_front, settings.color_tri_back)
mat_tri_back = get_or_create_material("Triangle_Back", settings.color_tri_back)
mesh_tri = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_Triangle")
bm_tri = bmesh.new()
v1 = bm_tri.verts.new(loc_X)
v2 = bm_tri.verts.new(loc_Y)
v3 = bm_tri.verts.new(loc_Z)
bm_tri.faces.new((v1, v2, v3))
bm_tri.to_mesh(mesh_tri)
bm_tri.free()
mesh_tri.update()
obj_tri = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_Triangle", mesh_tri)
preview_coll.objects.link(obj_tri)
obj_tri.data.materials.append(mat_tri_mix)
obj_tri.data.materials.append(mat_tri_back)
obj_tri.hide_viewport = not settings.show_tri
obj_tri.hide_render = not settings.show_tri
if settings.tri_thickness > 0:
mod = obj_tri.modifiers.new(name="Solidify", type='SOLIDIFY')
mod.thickness = settings.tri_thickness
mod.offset = 0.0
mod.material_offset = 1
mod.material_offset_rim = 1
# ---------------------------------------------------------
# 4. 同心円トーラスの生成
# ---------------------------------------------------------
if settings.show_torus and settings.torus_count > 0:
for i in range(1, settings.torus_count + 1):
mat_torus = get_or_create_torus_material(i, settings)
mesh_torus = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_Torus_{i}")
R = i * settings.torus_spacing
r = settings.torus_thickness
N = 48
n = 12
verts = []
faces = []
for idx_R in range(N):
u = idx_R * 2.0 * math.pi / N
cos_u, sin_u = math.cos(u), math.sin(u)
for idx_r in range(n):
v = idx_r * 2.0 * math.pi / n
cos_v, sin_v = math.cos(v), math.sin(v)
x = (R + r * cos_v) * cos_u
y = (R + r * cos_v) * sin_u
z = r * sin_v
verts.append((x, y, z))
for idx_R in range(N):
next_R = (idx_R + 1) % N
for idx_r in range(n):
next_r = (idx_r + 1) % n
v0 = idx_R * n + idx_r
v1 = idx_R * n + next_r
v2 = next_R * n + next_r
v3 = next_R * n + idx_r
faces.append((v0, v1, v2, v3))
mesh_torus.from_pydata(verts, [], faces)
for poly in mesh_torus.polygons:
poly.use_smooth = True
mesh_torus.update()
obj_torus = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_Torus_{i}", mesh_torus)
preview_coll.objects.link(obj_torus)
obj_torus.location = (0.0, 0.0, 0.0)
obj_torus.data.materials.append(mat_torus)
obj_torus.hide_viewport = not settings.show_torus
obj_torus.hide_render = not settings.show_torus
if hasattr(mesh_torus, "use_auto_smooth"):
mesh_torus.use_auto_smooth = True
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 Global Preview", default=False, update=update_preview_visibility)
base_location: FloatVectorProperty(name="Base Location", default=DEFAULT_BASE_LOCATION, update=on_prop_update)
base_angle: FloatProperty(name="Base Angle (X-Proj)", default=DEFAULT_BASE_ANGLE, update=on_prop_update)
radius: FloatProperty(name="Sphere Radius", default=DEFAULT_RADIUS, min=0.01, update=on_prop_update)
tri_thickness: FloatProperty(name="Tri Thickness", default=DEFAULT_TRI_THICKNESS, min=0.0, update=on_prop_update)
show_main: BoolProperty(name="Show Main", default=False, update=on_prop_update)
color_main: FloatVectorProperty(name="Main Color", subtype='COLOR', size=4, default=DEFAULT_COLOR_MAIN, min=0, max=1, update=on_prop_update)
show_proj_x: BoolProperty(name="Show Proj X", default=False, update=on_prop_update)
color_proj_x: FloatVectorProperty(name="Proj X Color", subtype='COLOR', size=4, default=DEFAULT_COLOR_PROJ_X, min=0, max=1, update=on_prop_update)
show_proj_y: BoolProperty(name="Show Proj Y", default=False, update=on_prop_update)
color_proj_y: FloatVectorProperty(name="Proj Y Color", subtype='COLOR', size=4, default=DEFAULT_COLOR_PROJ_Y, min=0, max=1, update=on_prop_update)
show_proj_z: BoolProperty(name="Show Proj Z", default=False, update=on_prop_update)
color_proj_z: FloatVectorProperty(name="Proj Z Color", subtype='COLOR', size=4, default=DEFAULT_COLOR_PROJ_Z, min=0, max=1, update=on_prop_update)
show_tri: BoolProperty(name="Show Triangle", default=False, update=on_prop_update)
color_tri_front: FloatVectorProperty(name="Tri Front Color", subtype='COLOR', size=4, default=DEFAULT_TRI_COLOR_FRONT, min=0, max=1, update=on_prop_update)
color_tri_back: FloatVectorProperty(name="Tri Back Color", subtype='COLOR', size=4, default=DEFAULT_TRI_COLOR_BACK, min=0, max=1, update=on_prop_update)
show_torus: BoolProperty(name="Show Torus", default=False, update=on_prop_update)
torus_count: IntProperty(name="Torus Count", default=DEFAULT_TORUS_COUNT, min=0, max=100, update=on_prop_update)
torus_spacing: FloatProperty(name="Torus Spacing", default=DEFAULT_TORUS_SPACING, min=0.01, update=on_prop_update)
torus_thickness: FloatProperty(name="Torus Thickness", default=DEFAULT_TORUS_THICKNESS, min=0.001, update=on_prop_update)
torus_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, default=DEFAULT_TORUS_COLOR, min=0, max=1, update=on_prop_update)
torus_random_color: BoolProperty(name="Random Color", default=False, update=on_prop_update)
class GROK_OT_ForceUpdate(Operator):
bl_idname = f"{get_prefix()}.force_update"
bl_label = "Force Update"
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_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}_{run_id}"
for obj in preview_coll.objects:
clean_name = obj.name.replace(f"zPreview_{CUSTOM_TAG_NAME}_", "")
obj.name = f"{CUSTOM_TAG_NAME}_{clean_name}_{run_id}"
if obj.data: obj.data.name = f"Mesh_{clean_name}_{run_id}"
for mat_slot in obj.material_slots:
if mat_slot.material and PREVIEW_MAT_NAME_BASE in mat_slot.material.name:
new_mat_name = mat_slot.material.name.replace(PREVIEW_MAT_NAME_BASE, f"{CUSTOM_TAG_NAME}_Mat") + f"_{run_id}"
mat_slot.material.name = new_mat_name
self.report({'INFO'}, f"切り離し完了: {CUSTOM_TAG_NAME}_{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} 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
row = layout.row()
row.prop(settings, "show_preview", text="Global Preview")
row.prop(settings, "live_update", text="Live Update")
box = layout.box()
box.label(text="Global Settings", icon='OBJECT_ORIGIN')
col = box.column(align=True)
col.prop(settings, "base_location")
col.prop(settings, "base_angle", text="Base Angle (X-Proj)")
col.prop(settings, "radius")
layout.separator()
box = layout.box()
box.label(text="Spheres (Visibility & Color)", icon='SPHERE')
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_main", text="本体 (Main)")
split.prop(settings, "color_main", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj_x", text="X成分投影 (-30°)")
split.prop(settings, "color_proj_x", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj_y", text="Y成分投影 (90°)")
split.prop(settings, "color_proj_y", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj_z", text="Z成分投影 (210°)")
split.prop(settings, "color_proj_z", text="")
layout.separator()
box = layout.box()
box.label(text="Triangle (Z+ Front / Z- Back)", icon='MESH_PLANE')
row = box.row()
row.prop(settings, "show_tri", text="三角形表示")
row.prop(settings, "tri_thickness", text="厚み")
row = box.row()
split = row.split(factor=0.5)
split.prop(settings, "color_tri_front", text="表(Front)")
split.prop(settings, "color_tri_back", text="裏(Back)")
layout.separator()
box = layout.box()
box.label(text="Concentric Torus (Z=0)", icon='MESH_TORUS')
row = box.row()
row.prop(settings, "show_torus", text="トーラス表示")
if settings.show_torus:
col = box.column(align=True)
col.prop(settings, "torus_count", text="個数 (0-100)")
col.prop(settings, "torus_spacing", text="間隔ステップ")
col.prop(settings, "torus_thickness", text="管の太さ")
row_c = col.row()
row_c.prop(settings, "torus_random_color", text="色をランダム化")
if not settings.torus_random_color:
row_c.prop(settings, "torus_color", text="")
layout.separator()
if not settings.live_update:
layout.operator(GROK_OT_ForceUpdate.bl_idname, icon='FILE_REFRESH')
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
bl_options = {'DEFAULT_CLOSED'}
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():
global _is_unloading
_is_unloading = False
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)
bpy.app.timers.register(preview_watchdog, first_interval=2.0)
def unregister():
global _is_unloading
_is_unloading = True
if bpy.app.timers.is_registered(preview_watchdog):
bpy.app.timers.unregister(preview_watchdog)
if bpy.app.timers.is_registered(deferred_build):
bpy.app.timers.unregister(deferred_build)
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
import random
import colorsys
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import StringProperty, FloatProperty, FloatVectorProperty, PointerProperty, BoolProperty, IntProperty
from datetime import datetime
from mathutils import Vector
# =========================================================
# 【ユーザー設定】 カスタムタグ名とカテゴリー名
# =========================================================
CUSTOM_TAG_NAME = "ProjSphere"
CUSTOM_CATEGORY_NAME = "[ 投影球体 ]"
DEFAULT_BASE_LOCATION = (3.0, 3.0, 3.0)
DEFAULT_BASE_ANGLE = -30.0
DEFAULT_RADIUS = 0.5
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_BASE = f"zPreview_{CUSTOM_TAG_NAME}_Mat"
bl_info = {
"name": f"zionad Addon [{CUSTOM_TAG_NAME} Generator]",
"author": "Your Name & AI Assistant",
"version": (11, 1),
"blender": (4, 0, 0),
"location": "View3D > Sidebar > [ 投影球体 ]",
"description": "投影球体、表裏色違い三角形、同心円トーラスジェネレーター(完全修正版)。",
"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
_is_unloading = False
# =========================================================
# プレビュー管理・クリーンアップ関数
# =========================================================
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 preview_is_broken():
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll is None:
return True
required_suffixes = ["Main", "ProjX", "ProjY", "ProjZ", "Triangle"]
for suffix in required_suffixes:
obj = bpy.data.objects.get(f"zPreview_{CUSTOM_TAG_NAME}_{suffix}")
if not obj or obj.name not in preview_coll.objects: return True
if obj.data is None or not hasattr(obj.data, "vertices") or len(obj.data.vertices) == 0: return True
try:
settings = bpy.context.scene.grok_sphere_settings
except AttributeError:
return False
if settings.show_torus and settings.torus_count > 0:
for i in range(1, settings.torus_count + 1):
obj = bpy.data.objects.get(f"zPreview_{CUSTOM_TAG_NAME}_Torus_{i}")
if not obj or obj.name not in preview_coll.objects: return True
if obj.data is None or not hasattr(obj.data, "vertices") or len(obj.data.vertices) == 0: return True
return False
def update_preview_visibility(self, context):
global _is_unloading
if _is_unloading: return
if self.show_preview:
if preview_is_broken():
build_preview(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
# =========================================================
# ライブアップデート&生成関数
# =========================================================
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 preview_watchdog():
global _is_unloading
if _is_unloading: return None
try:
settings = bpy.context.scene.grok_sphere_settings
except AttributeError:
return None
if settings.show_preview:
if preview_is_broken():
build_preview(bpy.context)
return 2.0
def get_or_create_material(suffix, color):
mat_name = f"{PREVIEW_MAT_NAME_BASE}_{suffix}"
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'
mat.shadow_method = 'HASHED'
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
mat.diffuse_color = color
return mat
def get_or_create_triangle_mix_material(color_front, color_back):
mat_name = f"{PREVIEW_MAT_NAME_BASE}_Triangle_Mix"
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'
mat.shadow_method = 'HASHED'
nodes = mat.node_tree.nodes
links = mat.node_tree.links
for node in nodes: nodes.remove(node)
node_output = nodes.new(type='ShaderNodeOutputMaterial')
node_mix = nodes.new(type='ShaderNodeMixShader')
node_front = nodes.new(type='ShaderNodeBsdfPrincipled')
node_back = nodes.new(type='ShaderNodeBsdfPrincipled')
node_geom = nodes.new(type='ShaderNodeNewGeometry')
node_front.inputs["Base Color"].default_value = color_front
if "Alpha" in node_front.inputs: node_front.inputs["Alpha"].default_value = color_front[3]
node_back.inputs["Base Color"].default_value = color_back
if "Alpha" in node_back.inputs: node_back.inputs["Alpha"].default_value = color_back[3]
links.new(node_geom.outputs["Backfacing"], node_mix.inputs[0])
links.new(node_front.outputs["BSDF"], node_mix.inputs[1])
links.new(node_back.outputs["BSDF"], node_mix.inputs[2])
links.new(node_mix.outputs["Shader"], node_output.inputs["Surface"])
mat.diffuse_color = color_front
return mat
def get_or_create_torus_material(i, settings):
mat_name = f"{PREVIEW_MAT_NAME_BASE}_Torus_{i}"
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'
mat.shadow_method = 'HASHED'
if settings.torus_random_color:
random.seed(i + 1234)
r, g, b = colorsys.hsv_to_rgb(random.random(), 0.8, 0.9)
color = (r, g, b, settings.torus_color[3])
else:
color = settings.torus_color
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
mat.diffuse_color = color
return mat
def build_preview(context):
global _is_updating, _is_unloading
if _is_unloading: return
if _is_updating: return
_is_updating = True
try:
settings = context.scene.grok_sphere_settings
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll and getattr(preview_coll, 'library', None):
cleanup_preview()
preview_coll = None
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
# ---------------------------------------------------------
# 1. 座標の計算 (Z=0平面での 120度ごとの位相)
# ---------------------------------------------------------
bx, by, bz = settings.base_location
a_rad = math.radians(settings.base_angle)
a120 = math.radians(120.0)
loc_X = Vector((bx * math.cos(a_rad), bx * math.sin(a_rad), 0.0))
loc_Y = Vector((by * math.cos(a_rad + a120), by * math.sin(a_rad + a120), 0.0))
loc_Z = Vector((bz * math.cos(a_rad + a120 * 2), bz * math.sin(a_rad + a120 * 2), 0.0))
# ---------------------------------------------------------
# 2. 球体の生成
# ---------------------------------------------------------
items = [
("Main", Vector((bx, by, bz)), settings.show_main, settings.color_main),
("ProjX", loc_X, settings.show_proj_x, settings.color_proj_x),
("ProjY", loc_Y, settings.show_proj_y, settings.color_proj_y),
("ProjZ", loc_Z, settings.show_proj_z, settings.color_proj_z)
]
for name_suffix, loc, is_show, color in items:
mat = get_or_create_material(name_suffix, color)
mesh = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_{name_suffix}")
bm_sphere = bmesh.new()
bmesh.ops.create_uvsphere(bm_sphere, u_segments=32, v_segments=16, radius=settings.radius)
bm_sphere.to_mesh(mesh)
bm_sphere.free()
mesh.update()
obj = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_{name_suffix}", mesh)
preview_coll.objects.link(obj)
obj.location = loc
obj.data.materials.append(mat)
obj.hide_viewport = not is_show
obj.hide_render = not is_show
for poly in mesh.polygons:
poly.use_smooth = True
if hasattr(mesh, "use_auto_smooth"):
mesh.use_auto_smooth = True
# ---------------------------------------------------------
# 3. 三角形メッシュの生成 (表裏マテリアル対応)
# ---------------------------------------------------------
mat_tri_mix = get_or_create_triangle_mix_material(settings.color_tri_front, settings.color_tri_back)
mat_tri_back = get_or_create_material("Triangle_Back", settings.color_tri_back)
mesh_tri = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_Triangle")
bm_tri = bmesh.new()
v1 = bm_tri.verts.new(loc_X)
v2 = bm_tri.verts.new(loc_Y)
v3 = bm_tri.verts.new(loc_Z)
bm_tri.faces.new((v1, v2, v3))
bm_tri.to_mesh(mesh_tri)
bm_tri.free()
mesh_tri.update()
obj_tri = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_Triangle", mesh_tri)
preview_coll.objects.link(obj_tri)
obj_tri.data.materials.append(mat_tri_mix)
obj_tri.data.materials.append(mat_tri_back)
obj_tri.hide_viewport = not settings.show_tri
obj_tri.hide_render = not settings.show_tri
if settings.tri_thickness > 0:
mod = obj_tri.modifiers.new(name="Solidify", type='SOLIDIFY')
mod.thickness = settings.tri_thickness
mod.offset = 0.0
mod.material_offset = 1
mod.material_offset_rim = 1
# ---------------------------------------------------------
# 4. 同心円トーラスの生成 (数式による確実なメッシュ生成)
# ---------------------------------------------------------
if settings.show_torus and settings.torus_count > 0:
for i in range(1, settings.torus_count + 1):
mat_torus = get_or_create_torus_material(i, settings)
mesh_torus = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_Torus_{i}")
# APIエラーを避けるため、数学関数で頂点・面を計算する
R = i * settings.torus_spacing
r = settings.torus_thickness
N = 48 # 大円の分割数
n = 12 # 断面の分割数
verts = []
faces = []
for idx_R in range(N):
u = idx_R * 2.0 * math.pi / N
cos_u, sin_u = math.cos(u), math.sin(u)
for idx_r in range(n):
v = idx_r * 2.0 * math.pi / n
cos_v, sin_v = math.cos(v), math.sin(v)
x = (R + r * cos_v) * cos_u
y = (R + r * cos_v) * sin_u
z = r * sin_v
verts.append((x, y, z))
for idx_R in range(N):
next_R = (idx_R + 1) % N
for idx_r in range(n):
next_r = (idx_r + 1) % n
v0 = idx_R * n + idx_r
v1 = idx_R * n + next_r
v2 = next_R * n + next_r
v3 = next_R * n + idx_r
faces.append((v0, v1, v2, v3))
mesh_torus.from_pydata(verts, [], faces)
for poly in mesh_torus.polygons:
poly.use_smooth = True
mesh_torus.update()
obj_torus = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_Torus_{i}", mesh_torus)
preview_coll.objects.link(obj_torus)
obj_torus.location = (0.0, 0.0, 0.0)
obj_torus.data.materials.append(mat_torus)
obj_torus.hide_viewport = not settings.show_torus
obj_torus.hide_render = not settings.show_torus
if hasattr(mesh_torus, "use_auto_smooth"):
mesh_torus.use_auto_smooth = True
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 Global Preview", default=False, update=update_preview_visibility)
base_location: FloatVectorProperty(name="Base Location", default=DEFAULT_BASE_LOCATION, update=on_prop_update)
base_angle: FloatProperty(name="Base Angle (X-Proj)", default=DEFAULT_BASE_ANGLE, update=on_prop_update)
radius: FloatProperty(name="Sphere Radius", default=DEFAULT_RADIUS, min=0.01, update=on_prop_update)
tri_thickness: FloatProperty(name="Tri Thickness", default=0.1, min=0.0, update=on_prop_update)
show_main: BoolProperty(name="Show Main", default=False, update=on_prop_update)
color_main: FloatVectorProperty(name="Main Color", subtype='COLOR', size=4, default=(0.8, 0.8, 0.8, 1.0), min=0, max=1, update=on_prop_update)
show_proj_x: BoolProperty(name="Show Proj X", default=False, update=on_prop_update)
color_proj_x: FloatVectorProperty(name="Proj X Color", subtype='COLOR', size=4, default=(0.8, 0.2, 0.2, 1.0), min=0, max=1, update=on_prop_update)
show_proj_y: BoolProperty(name="Show Proj Y", default=False, update=on_prop_update)
color_proj_y: FloatVectorProperty(name="Proj Y Color", subtype='COLOR', size=4, default=(0.2, 0.8, 0.2, 1.0), min=0, max=1, update=on_prop_update)
show_proj_z: BoolProperty(name="Show Proj Z", default=False, update=on_prop_update)
color_proj_z: FloatVectorProperty(name="Proj Z Color", subtype='COLOR', size=4, default=(0.2, 0.2, 0.8, 1.0), min=0, max=1, update=on_prop_update)
show_tri: BoolProperty(name="Show Triangle", default=False, update=on_prop_update)
color_tri_front: FloatVectorProperty(name="Tri Front Color", subtype='COLOR', size=4, default=(0.3, 0.1, 0.9, 1.0), min=0, max=1, update=on_prop_update)
color_tri_back: FloatVectorProperty(name="Tri Back Color", subtype='COLOR', size=4, default=(0.9, 0.1, 0.4, 1.0), min=0, max=1, update=on_prop_update)
show_torus: BoolProperty(name="Show Torus", default=False, update=on_prop_update)
torus_count: IntProperty(name="Torus Count", default=5, min=0, max=100, update=on_prop_update)
torus_spacing: FloatProperty(name="Torus Spacing", default=1.0, min=0.01, update=on_prop_update)
torus_thickness: FloatProperty(name="Torus Thickness", default=0.03, min=0.001, update=on_prop_update)
torus_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, default=(0.5, 0.5, 0.5, 1.0), min=0, max=1, update=on_prop_update)
torus_random_color: BoolProperty(name="Random Color", default=False, update=on_prop_update)
class GROK_OT_ForceUpdate(Operator):
bl_idname = f"{get_prefix()}.force_update"
bl_label = "Force Update"
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_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}_{run_id}"
for obj in preview_coll.objects:
clean_name = obj.name.replace(f"zPreview_{CUSTOM_TAG_NAME}_", "")
obj.name = f"{CUSTOM_TAG_NAME}_{clean_name}_{run_id}"
if obj.data: obj.data.name = f"Mesh_{clean_name}_{run_id}"
for mat_slot in obj.material_slots:
if mat_slot.material and PREVIEW_MAT_NAME_BASE in mat_slot.material.name:
new_mat_name = mat_slot.material.name.replace(PREVIEW_MAT_NAME_BASE, f"{CUSTOM_TAG_NAME}_Mat") + f"_{run_id}"
mat_slot.material.name = new_mat_name
self.report({'INFO'}, f"切り離し完了: {CUSTOM_TAG_NAME}_{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} 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
row = layout.row()
row.prop(settings, "show_preview", text="Global Preview")
row.prop(settings, "live_update", text="Live Update")
box = layout.box()
box.label(text="Global Settings", icon='OBJECT_ORIGIN')
col = box.column(align=True)
col.prop(settings, "base_location")
col.prop(settings, "base_angle", text="Base Angle (X-Proj)")
col.prop(settings, "radius")
layout.separator()
box = layout.box()
box.label(text="Spheres (Visibility & Color)", icon='SPHERE')
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_main", text="本体 (Main)")
split.prop(settings, "color_main", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj_x", text="X成分投影 (-30°)")
split.prop(settings, "color_proj_x", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj_y", text="Y成分投影 (90°)")
split.prop(settings, "color_proj_y", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj_z", text="Z成分投影 (210°)")
split.prop(settings, "color_proj_z", text="")
layout.separator()
box = layout.box()
box.label(text="Triangle (Z+ Front / Z- Back)", icon='MESH_PLANE')
row = box.row()
row.prop(settings, "show_tri", text="三角形表示")
row.prop(settings, "tri_thickness", text="厚み")
row = box.row()
split = row.split(factor=0.5)
split.prop(settings, "color_tri_front", text="表(Front)")
split.prop(settings, "color_tri_back", text="裏(Back)")
layout.separator()
box = layout.box()
box.label(text="Concentric Torus (Z=0)", icon='MESH_TORUS')
row = box.row()
row.prop(settings, "show_torus", text="トーラス表示")
if settings.show_torus:
col = box.column(align=True)
col.prop(settings, "torus_count", text="個数 (0-100)")
col.prop(settings, "torus_spacing", text="間隔ステップ")
col.prop(settings, "torus_thickness", text="管の太さ")
row_c = col.row()
row_c.prop(settings, "torus_random_color", text="色をランダム化")
if not settings.torus_random_color:
row_c.prop(settings, "torus_color", text="")
layout.separator()
if not settings.live_update:
layout.operator(GROK_OT_ForceUpdate.bl_idname, icon='FILE_REFRESH')
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
bl_options = {'DEFAULT_CLOSED'}
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():
global _is_unloading
_is_unloading = False
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)
bpy.app.timers.register(preview_watchdog, first_interval=2.0)
def unregister():
global _is_unloading
_is_unloading = True
if bpy.app.timers.is_registered(preview_watchdog):
bpy.app.timers.unregister(preview_watchdog)
if bpy.app.timers.is_registered(deferred_build):
bpy.app.timers.unregister(deferred_build)
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, PointerProperty, BoolProperty
from datetime import datetime
from mathutils import Vector
# =========================================================
# 【ユーザー設定】 カスタムタグ名とカテゴリー名
# =========================================================
CUSTOM_TAG_NAME = "ProjSphere"
CUSTOM_CATEGORY_NAME = "[ 投影球体 ]"
DEFAULT_BASE_LOCATION = (3.0, 3.0, 3.0)
DEFAULT_BASE_ANGLE = -30.0
DEFAULT_RADIUS = 0.5
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_BASE = f"zPreview_{CUSTOM_TAG_NAME}_Mat"
bl_info = {
"name": f"zionad Addon [{CUSTOM_TAG_NAME} Generator]",
"author": "Your Name & AI Assistant",
"version": (10, 2),
"blender": (4, 0, 0),
"location": "View3D > Sidebar > [ 投影球体 ]",
"description": "XYZ成分のZ=0平面への投影と三角形を生成。初期非表示&描画バグ修正版。",
"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
_is_unloading = False
# =========================================================
# プレビュー管理・クリーンアップ関数
# =========================================================
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 suffix in ["Main", "ProjX", "ProjY", "ProjZ", "Triangle"]:
mat = bpy.data.materials.get(f"{PREVIEW_MAT_NAME_BASE}_{suffix}")
if mat: bpy.data.materials.remove(mat, do_unlink=True)
def preview_is_broken():
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll is None:
return True
required_suffixes = ["Main", "ProjX", "ProjY", "ProjZ", "Triangle"]
existing_count = 0
for obj in preview_coll.objects:
for suffix in required_suffixes:
if obj.name == f"zPreview_{CUSTOM_TAG_NAME}_{suffix}":
if obj.data is None or not hasattr(obj.data, "vertices") or len(obj.data.vertices) == 0:
return True
existing_count += 1
break
return existing_count != len(required_suffixes)
def update_preview_visibility(self, context):
global _is_unloading
if _is_unloading: return
if self.show_preview:
if preview_is_broken():
build_preview(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
# =========================================================
# ライブアップデート&生成関数
# =========================================================
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 preview_watchdog():
global _is_unloading
if _is_unloading:
return None
try:
settings = bpy.context.scene.grok_sphere_settings
except AttributeError:
return None
if settings.show_preview:
if preview_is_broken():
build_preview(bpy.context)
return 2.0
def get_or_create_material(suffix, color):
mat_name = f"{PREVIEW_MAT_NAME_BASE}_{suffix}"
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'
mat.shadow_method = 'HASHED'
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
mat.diffuse_color = color
return mat
def build_preview(context):
global _is_updating, _is_unloading
if _is_unloading: return
if _is_updating: return
_is_updating = True
try:
settings = context.scene.grok_sphere_settings
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll and getattr(preview_coll, 'library', None):
cleanup_preview()
preview_coll = None
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
# ---------------------------------------------------------
# 1. 座標の計算 (Z=0平面での 120度ごとの位相)
# ---------------------------------------------------------
bx, by, bz = settings.base_location
a_rad = math.radians(settings.base_angle)
a120 = math.radians(120.0)
loc_X = Vector((bx * math.cos(a_rad), bx * math.sin(a_rad), 0.0))
loc_Y = Vector((by * math.cos(a_rad + a120), by * math.sin(a_rad + a120), 0.0))
loc_Z = Vector((bz * math.cos(a_rad + a120 * 2), bz * math.sin(a_rad + a120 * 2), 0.0))
# ---------------------------------------------------------
# 2. 球体の生成
# ---------------------------------------------------------
items = [
("Main", Vector((bx, by, bz)), settings.show_main, settings.color_main),
("ProjX", loc_X, settings.show_proj_x, settings.color_proj_x),
("ProjY", loc_Y, settings.show_proj_y, settings.color_proj_y),
("ProjZ", loc_Z, settings.show_proj_z, settings.color_proj_z)
]
for name_suffix, loc, is_show, color in items:
mat = get_or_create_material(name_suffix, color)
mesh = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_{name_suffix}")
bm_sphere = bmesh.new()
bmesh.ops.create_uvsphere(bm_sphere, u_segments=32, v_segments=16, radius=settings.radius)
bm_sphere.to_mesh(mesh)
bm_sphere.free()
# ★修正: 生成後に明示的にメッシュをアップデート(これで表示バグが完全に直ります)
mesh.update()
obj = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_{name_suffix}", mesh)
preview_coll.objects.link(obj)
obj.location = loc
obj.data.materials.append(mat)
obj.hide_viewport = not is_show
obj.hide_render = not is_show
for poly in mesh.polygons:
poly.use_smooth = True
if hasattr(mesh, "use_auto_smooth"):
mesh.use_auto_smooth = True
# ---------------------------------------------------------
# 3. 三角形メッシュの生成
# ---------------------------------------------------------
mat_tri = get_or_create_material("Triangle", settings.color_tri)
mesh_tri = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_Triangle")
bm_tri = bmesh.new()
v1 = bm_tri.verts.new(loc_X)
v2 = bm_tri.verts.new(loc_Y)
v3 = bm_tri.verts.new(loc_Z)
bm_tri.faces.new((v1, v2, v3))
bm_tri.to_mesh(mesh_tri)
bm_tri.free()
# ★修正: 三角形も明示的にアップデート
mesh_tri.update()
obj_tri = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_Triangle", mesh_tri)
preview_coll.objects.link(obj_tri)
obj_tri.data.materials.append(mat_tri)
obj_tri.hide_viewport = not settings.show_tri
obj_tri.hide_render = not settings.show_tri
if settings.tri_thickness > 0:
mod = obj_tri.modifiers.new(name="Solidify", type='SOLIDIFY')
mod.thickness = settings.tri_thickness
mod.offset = 0.0
context.view_layer.update()
finally:
_is_updating = False
# =========================================================
# プロパティとオペレータ
# =========================================================
class GROK_SphereSettings(PropertyGroup):
live_update: BoolProperty(name="Live Update", default=True)
# ★修正: 起動時、Global Preview 自体をオフで開始する
show_preview: BoolProperty(name="Show Global Preview", default=False, update=update_preview_visibility)
base_location: FloatVectorProperty(name="Base Location", default=DEFAULT_BASE_LOCATION, update=on_prop_update)
base_angle: FloatProperty(name="Base Angle (X-Proj)", default=DEFAULT_BASE_ANGLE, update=on_prop_update)
radius: FloatProperty(name="Sphere Radius", default=DEFAULT_RADIUS, min=0.01, update=on_prop_update)
tri_thickness: FloatProperty(name="Tri Thickness", default=0.1, min=0.0, update=on_prop_update)
# ★修正: すべてのオブジェクトの初期状態を False (非表示) に設定
show_main: BoolProperty(name="Show Main", default=False, update=on_prop_update)
color_main: FloatVectorProperty(name="Main Color", subtype='COLOR', size=4, default=(0.8, 0.8, 0.8, 1.0), min=0, max=1, update=on_prop_update)
show_proj_x: BoolProperty(name="Show Proj X", default=False, update=on_prop_update)
color_proj_x: FloatVectorProperty(name="Proj X Color", subtype='COLOR', size=4, default=(0.8, 0.2, 0.2, 1.0), min=0, max=1, update=on_prop_update)
show_proj_y: BoolProperty(name="Show Proj Y", default=False, update=on_prop_update)
color_proj_y: FloatVectorProperty(name="Proj Y Color", subtype='COLOR', size=4, default=(0.2, 0.8, 0.2, 1.0), min=0, max=1, update=on_prop_update)
show_proj_z: BoolProperty(name="Show Proj Z", default=False, update=on_prop_update)
color_proj_z: FloatVectorProperty(name="Proj Z Color", subtype='COLOR', size=4, default=(0.2, 0.2, 0.8, 1.0), min=0, max=1, update=on_prop_update)
show_tri: BoolProperty(name="Show Triangle", default=False, update=on_prop_update)
color_tri: FloatVectorProperty(name="Tri Color", subtype='COLOR', size=4, default=(0.8, 0.8, 0.2, 0.5), min=0, max=1, update=on_prop_update)
class GROK_OT_ForceUpdate(Operator):
bl_idname = f"{get_prefix()}.force_update"
bl_label = "Force Update"
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_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}_{run_id}"
for suffix in ["Main", "ProjX", "ProjY", "ProjZ", "Triangle"]:
mat = bpy.data.materials.get(f"{PREVIEW_MAT_NAME_BASE}_{suffix}")
if mat: mat.name = f"{CUSTOM_TAG_NAME}_{suffix}_Mat_{run_id}"
for obj in preview_coll.objects:
clean_name = obj.name.replace(f"zPreview_{CUSTOM_TAG_NAME}_", "")
obj.name = f"{CUSTOM_TAG_NAME}_{clean_name}_{run_id}"
if obj.data: obj.data.name = f"Mesh_{clean_name}_{run_id}"
self.report({'INFO'}, f"切り離し完了: {CUSTOM_TAG_NAME}_{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} 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
row = layout.row()
row.prop(settings, "show_preview", text="Global Preview")
row.prop(settings, "live_update", text="Live Update")
box = layout.box()
box.label(text="Global Settings", icon='OBJECT_ORIGIN')
col = box.column(align=True)
col.prop(settings, "base_location")
col.prop(settings, "base_angle", text="Base Angle (X-Proj)")
col.prop(settings, "radius")
layout.separator()
box = layout.box()
box.label(text="Objects (Visibility & Color)", icon='COLOR')
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_main", text="本体 (Main)")
split.prop(settings, "color_main", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj_x", text="X成分投影 (-30°)")
split.prop(settings, "color_proj_x", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj_y", text="Y成分投影 (90°)")
split.prop(settings, "color_proj_y", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj_z", text="Z成分投影 (210°)")
split.prop(settings, "color_proj_z", text="")
layout.separator()
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_tri", text="三角形 (Triangle)")
split.prop(settings, "color_tri", text="")
row = box.row()
row.prop(settings, "tri_thickness", text="三角形の厚み")
layout.separator()
if not settings.live_update:
layout.operator(GROK_OT_ForceUpdate.bl_idname, icon='FILE_REFRESH')
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
bl_options = {'DEFAULT_CLOSED'}
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():
global _is_unloading
_is_unloading = False
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)
bpy.app.timers.register(preview_watchdog, first_interval=2.0)
def unregister():
global _is_unloading
_is_unloading = True
if bpy.app.timers.is_registered(preview_watchdog):
bpy.app.timers.unregister(preview_watchdog)
if bpy.app.timers.is_registered(deferred_build):
bpy.app.timers.unregister(deferred_build)
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, PointerProperty, BoolProperty
from datetime import datetime
from mathutils import Vector
# =========================================================
# 【ユーザー設定】 カスタムタグ名とカテゴリー名
# =========================================================
CUSTOM_TAG_NAME = "ProjSphere"
CUSTOM_CATEGORY_NAME = "[ 投影球体 ]"
DEFAULT_BASE_LOCATION = (3.0, 3.0, 3.0)
DEFAULT_BASE_ANGLE = -30.0
DEFAULT_RADIUS = 0.5
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_BASE = f"zPreview_{CUSTOM_TAG_NAME}_Mat"
bl_info = {
"name": f"zionad Addon [{CUSTOM_TAG_NAME} Generator]",
"author": "Your Name & AI Assistant",
"version": (10, 0),
"blender": (4, 0, 0),
"location": "View3D > Sidebar > [ 投影球体 ]",
"description": "XYZ成分のZ=0平面への投影(-30°, 90°, 210°)と、それらを結ぶ三角形を自動生成します。",
"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
_is_unloading = False
# =========================================================
# プレビュー管理・クリーンアップ関数
# =========================================================
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 suffix in ["Main", "ProjX", "ProjY", "ProjZ", "Triangle"]:
mat = bpy.data.materials.get(f"{PREVIEW_MAT_NAME_BASE}_{suffix}")
if mat: bpy.data.materials.remove(mat, do_unlink=True)
def preview_is_broken():
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll is None:
return True
required_suffixes = ["Main", "ProjX", "ProjY", "ProjZ", "Triangle"]
existing_count = 0
for obj in preview_coll.objects:
for suffix in required_suffixes:
if obj.name == f"zPreview_{CUSTOM_TAG_NAME}_{suffix}":
if obj.data is None:
return True
existing_count += 1
break
return existing_count != len(required_suffixes)
def update_preview_visibility(self, context):
global _is_unloading
if _is_unloading: return
if self.show_preview:
if preview_is_broken():
build_preview(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
# =========================================================
# ライブアップデート&生成関数
# =========================================================
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 preview_watchdog():
global _is_unloading
if _is_unloading:
return None
try:
settings = bpy.context.scene.grok_sphere_settings
except AttributeError:
return None
if settings.show_preview:
if preview_is_broken():
build_preview(bpy.context)
return 2.0
def get_or_create_material(suffix, color):
mat_name = f"{PREVIEW_MAT_NAME_BASE}_{suffix}"
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' # 半透明(Alpha Blend)に対応
mat.shadow_method = 'HASHED'
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
mat.diffuse_color = color
return mat
def build_preview(context):
global _is_updating, _is_unloading
if _is_unloading: return
if _is_updating: return
_is_updating = True
try:
settings = context.scene.grok_sphere_settings
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll and getattr(preview_coll, 'library', None):
cleanup_preview()
preview_coll = None
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
# ---------------------------------------------------------
# 1. 座標の計算 (Z=0平面での 120度ごとの位相)
# ---------------------------------------------------------
bx, by, bz = settings.base_location
a_rad = math.radians(settings.base_angle) # デフォルト -30度
a120 = math.radians(120.0)
loc_X = Vector((bx * math.cos(a_rad), bx * math.sin(a_rad), 0.0))
loc_Y = Vector((by * math.cos(a_rad + a120), by * math.sin(a_rad + a120), 0.0))
loc_Z = Vector((bz * math.cos(a_rad + a120 * 2), bz * math.sin(a_rad + a120 * 2), 0.0))
# ---------------------------------------------------------
# 2. 球体の生成
# ---------------------------------------------------------
items = [
("Main", Vector((bx, by, bz)), settings.show_main, settings.color_main),
("ProjX", loc_X, settings.show_proj_x, settings.color_proj_x),
("ProjY", loc_Y, settings.show_proj_y, settings.color_proj_y),
("ProjZ", loc_Z, settings.show_proj_z, settings.color_proj_z)
]
bm_sphere = bmesh.new()
bmesh.ops.create_uvsphere(bm_sphere, u_segments=32, v_segments=16, radius=settings.radius)
for name_suffix, loc, is_show, color in items:
mat = get_or_create_material(name_suffix, color)
mesh = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_{name_suffix}")
bm_sphere.to_mesh(mesh)
obj = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_{name_suffix}", mesh)
preview_coll.objects.link(obj)
obj.location = loc
obj.data.materials.append(mat)
obj.hide_viewport = not is_show
obj.hide_render = not is_show
for poly in mesh.polygons:
poly.use_smooth = True
if hasattr(mesh, "use_auto_smooth"):
mesh.use_auto_smooth = True
bm_sphere.free()
# ---------------------------------------------------------
# 3. 三角形メッシュの生成
# ---------------------------------------------------------
mat_tri = get_or_create_material("Triangle", settings.color_tri)
mesh_tri = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_Triangle")
bm_tri = bmesh.new()
v1 = bm_tri.verts.new(loc_X)
v2 = bm_tri.verts.new(loc_Y)
v3 = bm_tri.verts.new(loc_Z)
bm_tri.faces.new((v1, v2, v3))
bm_tri.to_mesh(mesh_tri)
bm_tri.free()
obj_tri = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_Triangle", mesh_tri)
preview_coll.objects.link(obj_tri)
obj_tri.data.materials.append(mat_tri)
obj_tri.hide_viewport = not settings.show_tri
obj_tri.hide_render = not settings.show_tri
# 三角形の厚み付け (Solidifyモディファイアを使用)
if settings.tri_thickness > 0:
mod = obj_tri.modifiers.new(name="Solidify", type='SOLIDIFY')
mod.thickness = settings.tri_thickness
mod.offset = 0.0 # 両面に均等に厚みをつける
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 Global Preview", default=True, update=update_preview_visibility)
base_location: FloatVectorProperty(name="Base Location", default=DEFAULT_BASE_LOCATION, update=on_prop_update)
base_angle: FloatProperty(name="Base Angle (X-Proj)", default=DEFAULT_BASE_ANGLE, update=on_prop_update)
radius: FloatProperty(name="Sphere Radius", default=DEFAULT_RADIUS, min=0.01, update=on_prop_update)
tri_thickness: FloatProperty(name="Tri Thickness", default=0.1, min=0.0, update=on_prop_update)
# 各球体と三角形の表示設定 (RGBA対応)
show_main: BoolProperty(name="Show Main", default=True, update=on_prop_update)
color_main: FloatVectorProperty(name="Main Color", subtype='COLOR', size=4, default=(0.8, 0.8, 0.8, 1.0), min=0, max=1, update=on_prop_update)
show_proj_x: BoolProperty(name="Show Proj X", default=True, update=on_prop_update)
color_proj_x: FloatVectorProperty(name="Proj X Color", subtype='COLOR', size=4, default=(0.8, 0.2, 0.2, 1.0), min=0, max=1, update=on_prop_update)
show_proj_y: BoolProperty(name="Show Proj Y", default=True, update=on_prop_update)
color_proj_y: FloatVectorProperty(name="Proj Y Color", subtype='COLOR', size=4, default=(0.2, 0.8, 0.2, 1.0), min=0, max=1, update=on_prop_update)
show_proj_z: BoolProperty(name="Show Proj Z", default=True, update=on_prop_update)
color_proj_z: FloatVectorProperty(name="Proj Z Color", subtype='COLOR', size=4, default=(0.2, 0.2, 0.8, 1.0), min=0, max=1, update=on_prop_update)
show_tri: BoolProperty(name="Show Triangle", default=True, update=on_prop_update)
color_tri: FloatVectorProperty(name="Tri Color", subtype='COLOR', size=4, default=(0.8, 0.8, 0.2, 0.5), min=0, max=1, update=on_prop_update)
class GROK_OT_ForceUpdate(Operator):
bl_idname = f"{get_prefix()}.force_update"
bl_label = "Force Update"
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_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}_{run_id}"
for suffix in ["Main", "ProjX", "ProjY", "ProjZ", "Triangle"]:
mat = bpy.data.materials.get(f"{PREVIEW_MAT_NAME_BASE}_{suffix}")
if mat: mat.name = f"{CUSTOM_TAG_NAME}_{suffix}_Mat_{run_id}"
for obj in preview_coll.objects:
clean_name = obj.name.replace(f"zPreview_{CUSTOM_TAG_NAME}_", "")
obj.name = f"{CUSTOM_TAG_NAME}_{clean_name}_{run_id}"
if obj.data: obj.data.name = f"Mesh_{clean_name}_{run_id}"
self.report({'INFO'}, f"切り離し完了: {CUSTOM_TAG_NAME}_{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} 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
row = layout.row()
row.prop(settings, "show_preview", text="Global Preview")
row.prop(settings, "live_update", text="Live Update")
box = layout.box()
box.label(text="Global Settings", icon='OBJECT_ORIGIN')
col = box.column(align=True)
col.prop(settings, "base_location")
col.prop(settings, "base_angle", text="Base Angle (X-Proj)")
col.prop(settings, "radius")
layout.separator()
box = layout.box()
box.label(text="Objects (Visibility & Color)", icon='COLOR')
# 確実なUI崩れ防止レイアウト
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_main", text="本体 (Main)")
split.prop(settings, "color_main", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj_x", text="X成分投影 (-30°)")
split.prop(settings, "color_proj_x", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj_y", text="Y成分投影 (90°)")
split.prop(settings, "color_proj_y", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj_z", text="Z成分投影 (210°)")
split.prop(settings, "color_proj_z", text="")
layout.separator()
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_tri", text="三角形 (Triangle)")
split.prop(settings, "color_tri", text="")
row = box.row()
row.prop(settings, "tri_thickness", text="三角形の厚み")
layout.separator()
if not settings.live_update:
layout.operator(GROK_OT_ForceUpdate.bl_idname, icon='FILE_REFRESH')
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():
global _is_unloading
_is_unloading = False
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)
bpy.app.timers.register(preview_watchdog, first_interval=2.0)
def unregister():
global _is_unloading
_is_unloading = True
if bpy.app.timers.is_registered(preview_watchdog):
bpy.app.timers.unregister(preview_watchdog)
if bpy.app.timers.is_registered(deferred_build):
bpy.app.timers.unregister(deferred_build)
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, PointerProperty, BoolProperty
from datetime import datetime
from mathutils import Vector
# =========================================================
# 【ユーザー設定】 カスタムタグ名とカテゴリー名
# =========================================================
CUSTOM_TAG_NAME = "ProjSphere"
CUSTOM_CATEGORY_NAME = "[ 投影球体 ]"
DEFAULT_BASE_LOCATION = (3.0, 3.0, 3.0)
DEFAULT_ROTATION_ANGLE = -30.0
DEFAULT_RADIUS = 0.5
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_BASE = f"zPreview_{CUSTOM_TAG_NAME}_Mat"
bl_info = {
"name": f"zionad Addon [{CUSTOM_TAG_NAME} Generator]",
"author": "Your Name & AI Assistant",
"version": (9, 0),
"blender": (4, 0, 0),
"location": "View3D > Sidebar > [ 投影球体 ]",
"description": "本体、x軸投影、Z軸回転の3球体を生成。自己修復機能付き。",
"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)
for suffix in ["Main", "Proj", "Rot"]:
mat = bpy.data.materials.get(f"{PREVIEW_MAT_NAME_BASE}_{suffix}")
if mat: bpy.data.materials.remove(mat, do_unlink=True)
# ---------------------------------------------------------
# 【提案1&4】 プレビュー破損チェック(オブジェクト数と中身の検証)
# ---------------------------------------------------------
def preview_is_broken():
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll is None:
return True
required_suffixes = ["Main", "Proj", "Rot"]
existing_count = 0
for obj in preview_coll.objects:
for suffix in required_suffixes:
if obj.name == f"zPreview_{CUSTOM_TAG_NAME}_{suffix}":
# メッシュ自体が消去されていたら破損とみなす
if obj.data is None:
return True
existing_count += 1
break
# 3つ完全に揃っていない場合は破損判定
return existing_count != 3
# ---------------------------------------------------------
# 【提案1】 show_preview ONにした瞬間の自動復旧
# ---------------------------------------------------------
def update_preview_visibility(self, context):
if self.show_preview:
if preview_is_broken():
build_preview(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)
# ---------------------------------------------------------
# 【提案3】 2秒間隔のWatchdog(常に完全性を監視)
# ---------------------------------------------------------
def preview_watchdog():
try:
settings = bpy.context.scene.grok_sphere_settings
except AttributeError:
return None # アドオン削除後などに走ったら停止する
if settings.show_preview:
if preview_is_broken():
build_preview(bpy.context)
return 2.0 # 2秒後に再度実行
def get_or_create_material(suffix, color):
mat_name = f"{PREVIEW_MAT_NAME_BASE}_{suffix}"
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
mat.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (*color, 1.0)
mat.diffuse_color = (*color, 1.0)
return mat
def build_preview(context):
global _is_updating
if _is_updating: return
_is_updating = True
try:
settings = context.scene.grok_sphere_settings
# ---------------------------------------------------------
# 【提案2】 異常状態(ライブラリリンク等)の自己修復
# ---------------------------------------------------------
preview_coll = bpy.data.collections.get(PREVIEW_COLL_NAME)
if preview_coll and getattr(preview_coll, 'library', None):
cleanup_preview()
preview_coll = None
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
bx, by, bz = settings.base_location
angle_rad = math.radians(settings.rotation_angle)
items = [
("Main", Vector((bx, by, bz)), settings.show_main, settings.color_main),
("Proj", Vector((bx, 0.0, 0.0)), settings.show_proj, settings.color_proj),
("Rot", Vector((bx * math.cos(angle_rad), bx * math.sin(angle_rad), 0.0)), settings.show_rot, settings.color_rot)
]
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=settings.radius)
for name_suffix, loc, is_show, color in items:
mat = get_or_create_material(name_suffix, color)
mesh = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_{name_suffix}")
bm.to_mesh(mesh)
obj = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_{name_suffix}", mesh)
preview_coll.objects.link(obj)
obj.location = loc
obj.data.materials.append(mat)
obj.hide_viewport = not is_show
obj.hide_render = not is_show
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 Global Preview", default=True, update=update_preview_visibility)
base_location: FloatVectorProperty(name="Base Location", default=DEFAULT_BASE_LOCATION, update=on_prop_update)
rotation_angle: FloatProperty(name="Rotation Angle", default=DEFAULT_ROTATION_ANGLE, update=on_prop_update)
radius: FloatProperty(name="Radius", default=DEFAULT_RADIUS, min=0.01, update=on_prop_update)
show_main: BoolProperty(name="Show Main", default=True, update=on_prop_update)
color_main: FloatVectorProperty(name="Main Color", subtype='COLOR', default=(0.8, 0.2, 0.2), min=0, max=1, update=on_prop_update)
show_proj: BoolProperty(name="Show Proj", default=True, update=on_prop_update)
color_proj: FloatVectorProperty(name="Proj Color", subtype='COLOR', default=(0.2, 0.8, 0.2), min=0, max=1, update=on_prop_update)
show_rot: BoolProperty(name="Show Rot", default=True, update=on_prop_update)
color_rot: FloatVectorProperty(name="Rot Color", subtype='COLOR', default=(0.2, 0.2, 0.8), min=0, max=1, update=on_prop_update)
class GROK_OT_ForceUpdate(Operator):
bl_idname = f"{get_prefix()}.force_update"
bl_label = "Force Update"
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_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}_{run_id}"
for suffix in ["Main", "Proj", "Rot"]:
mat = bpy.data.materials.get(f"{PREVIEW_MAT_NAME_BASE}_{suffix}")
if mat: mat.name = f"{CUSTOM_TAG_NAME}_{suffix}_Mat_{run_id}"
for obj in preview_coll.objects:
clean_name = obj.name.replace(f"zPreview_{CUSTOM_TAG_NAME}_", "")
obj.name = f"{CUSTOM_TAG_NAME}_{clean_name}_{run_id}"
if obj.data: obj.data.name = f"Mesh_{clean_name}_{run_id}"
self.report({'INFO'}, f"切り離し完了: {CUSTOM_TAG_NAME}_{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} 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
row = layout.row()
row.prop(settings, "show_preview", text="Global Preview")
row.prop(settings, "live_update", text="Live Update")
box = layout.box()
box.label(text="Global Settings", icon='OBJECT_ORIGIN')
col = box.column(align=True)
col.prop(settings, "base_location")
col.prop(settings, "rotation_angle")
col.prop(settings, "radius")
layout.separator()
box = layout.box()
box.label(text="Individual Spheres", icon='COLOR')
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_main", text="本体 (Main)")
split.prop(settings, "color_main", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj", text="X投影 (Projected)")
split.prop(settings, "color_proj", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_rot", text="回転 (Rotated)")
split.prop(settings, "color_rot", text="")
layout.separator()
if not settings.live_update:
layout.operator(GROK_OT_ForceUpdate.bl_idname, icon='FILE_REFRESH')
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)
bpy.app.timers.register(preview_watchdog, first_interval=2.0) # Watchdog登録
def unregister():
cleanup_preview()
if hasattr(bpy.types.Scene, 'grok_sphere_settings'): del bpy.types.Scene.grok_sphere_settings
# タイマーはNoneを返すと自動解除されるが、明示的に解除できれば安全
if bpy.app.timers.is_registered(preview_watchdog):
bpy.app.timers.unregister(preview_watchdog)
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, PointerProperty, BoolProperty
from datetime import datetime
from mathutils import Vector
# =========================================================
# 【ユーザー設定】 カスタムタグ名とカテゴリー名
# =========================================================
CUSTOM_TAG_NAME = "ProjSphere"
CUSTOM_CATEGORY_NAME = "[ 投影球体 ]"
DEFAULT_BASE_LOCATION = (3.0, 3.0, 3.0)
DEFAULT_ROTATION_ANGLE = -30.0
DEFAULT_RADIUS = 0.5
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_BASE = f"zPreview_{CUSTOM_TAG_NAME}_Mat"
bl_info = {
"name": f"zionad Addon [{CUSTOM_TAG_NAME} Generator]",
"author": "Your Name & AI Assistant",
"version": (8, 2),
"blender": (4, 0, 0),
"location": "View3D > Sidebar > [ 投影球体 ]",
"description": "本体、x軸投影、Z軸回転の3つの球体を個別色・個別表示で生成します。",
"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)
for suffix in ["Main", "Proj", "Rot"]:
mat = bpy.data.materials.get(f"{PREVIEW_MAT_NAME_BASE}_{suffix}")
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 get_or_create_material(suffix, color):
mat_name = f"{PREVIEW_MAT_NAME_BASE}_{suffix}"
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
mat.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (*color, 1.0)
mat.diffuse_color = (*color, 1.0)
return mat
def build_preview(context):
global _is_updating
if _is_updating: return
_is_updating = True
try:
settings = context.scene.grok_sphere_settings
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
bx, by, bz = settings.base_location
angle_rad = math.radians(settings.rotation_angle)
items = [
("Main", Vector((bx, by, bz)), settings.show_main, settings.color_main),
("Proj", Vector((bx, 0.0, 0.0)), settings.show_proj, settings.color_proj),
("Rot", Vector((bx * math.cos(angle_rad), bx * math.sin(angle_rad), 0.0)), settings.show_rot, settings.color_rot)
]
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=settings.radius)
for name_suffix, loc, is_show, color in items:
mat = get_or_create_material(name_suffix, color)
mesh = bpy.data.meshes.new(f"zPreview_{CUSTOM_TAG_NAME}_{name_suffix}")
bm.to_mesh(mesh)
obj = bpy.data.objects.new(f"zPreview_{CUSTOM_TAG_NAME}_{name_suffix}", mesh)
preview_coll.objects.link(obj)
obj.location = loc
obj.data.materials.append(mat)
# 生成は常に行い、可視性のみコントロール
obj.hide_viewport = not is_show
obj.hide_render = not is_show
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 Global Preview", default=True, update=update_preview_visibility)
base_location: FloatVectorProperty(name="Base Location", default=DEFAULT_BASE_LOCATION, update=on_prop_update)
rotation_angle: FloatProperty(name="Rotation Angle", default=DEFAULT_ROTATION_ANGLE, update=on_prop_update)
radius: FloatProperty(name="Radius", default=DEFAULT_RADIUS, min=0.01, update=on_prop_update)
show_main: BoolProperty(name="Show Main", default=True, update=on_prop_update)
color_main: FloatVectorProperty(name="Main Color", subtype='COLOR', default=(0.8, 0.2, 0.2), min=0, max=1, update=on_prop_update)
show_proj: BoolProperty(name="Show Proj", default=True, update=on_prop_update)
color_proj: FloatVectorProperty(name="Proj Color", subtype='COLOR', default=(0.2, 0.8, 0.2), min=0, max=1, update=on_prop_update)
show_rot: BoolProperty(name="Show Rot", default=True, update=on_prop_update)
color_rot: FloatVectorProperty(name="Rot Color", subtype='COLOR', default=(0.2, 0.2, 0.8), min=0, max=1, update=on_prop_update)
class GROK_OT_ForceUpdate(Operator):
bl_idname = f"{get_prefix()}.force_update"
bl_label = "Force Update"
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_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}_{run_id}"
for suffix in ["Main", "Proj", "Rot"]:
mat = bpy.data.materials.get(f"{PREVIEW_MAT_NAME_BASE}_{suffix}")
if mat: mat.name = f"{CUSTOM_TAG_NAME}_{suffix}_Mat_{run_id}"
for obj in preview_coll.objects:
clean_name = obj.name.replace(f"zPreview_{CUSTOM_TAG_NAME}_", "")
obj.name = f"{CUSTOM_TAG_NAME}_{clean_name}_{run_id}"
if obj.data: obj.data.name = f"Mesh_{clean_name}_{run_id}"
self.report({'INFO'}, f"切り離し完了: {CUSTOM_TAG_NAME}_{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} 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
# アイコンボタン化を廃止し、明確なチェックボックスとして配置
row = layout.row()
row.prop(settings, "show_preview", text="Global Preview")
row.prop(settings, "live_update", text="Live Update")
box = layout.box()
box.label(text="Global Settings", icon='OBJECT_ORIGIN')
col = box.column(align=True)
col.prop(settings, "base_location")
col.prop(settings, "rotation_angle")
col.prop(settings, "radius")
layout.separator()
box = layout.box()
box.label(text="Individual Spheres", icon='COLOR')
# ★修正: チェックを外してもUIが消滅しない安全なレイアウト(左にチェック、右に色)
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_main", text="本体 (Main)")
split.prop(settings, "color_main", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_proj", text="X投影 (Projected)")
split.prop(settings, "color_proj", text="")
row = box.row()
split = row.split(factor=0.6)
split.prop(settings, "show_rot", text="回転 (Rotated)")
split.prop(settings, "color_rot", text="")
layout.separator()
if not settings.live_update:
layout.operator(GROK_OT_ForceUpdate.bl_idname, icon='FILE_REFRESH')
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()