進化版 画面中央 透視投影視座位置 情報コピー付き20260319cc
# Copied: 15:00:01
import bpy
import bmesh
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime
# ==============================================================================
# 設定エリア & ID管理
# ==============================================================================
# ★ PREFIXに大文字が含まれていても、内部で自動的に小文字に変換して処理します
PREFIX = "Sphere20260227"
TAB_NAME = " [ Sphere copy ] "
# ★ このスクリプト自身のID (コピー機能で使用)
# ### ZIONAD_SOURCE_ID: SPHERE_2026_02_27_FIXED ###
bl_info = {
"name": f"zionad 520 [ Sphere Gen ] {PREFIX}",
"author": "zionadchat",
"version": (3, 1, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": "Unique Material Sphere Generator",
"category": "3D View",
}
# 内部変数
# オペレーターID用には小文字化したPrefixを使用 (エラー回避のため)
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SPHERE_2026_02_27_FIXED ###"
ADDON_LINKS = (
{"label": "Code Copy Template", "url": "<https://www.notion.so/Code-copy-20260221>"},
{"label": "Theory Background", "url": "<https://www.notion.so/Einstein-from-20260119>"},
)
# ==============================================================================
# デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"show_preview": True,
"sphere_color": (0.0391, 0.8000, 0.1647, 0.8000),
"sphere_loc": (0.0000, 0.0000, 0.0000),
"sphere_radius": 5.0000,
}
# <END_DICT>
# ==============================================================================
# マテリアル作成ロジック (常に新規作成)
# ==============================================================================
def create_unique_material(color, name_prefix="Mat"):
timestamp = datetime.now().strftime('%M%S%f')[:5]
mat_name = f"{name_prefix}_{timestamp}"
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
if mat.use_nodes:
tree = mat.node_tree
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (0, 0)
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (300, 0)
tree.links.new(bsdf.outputs[0], out.inputs[0])
if "Base Color" in bsdf.inputs:
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
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
# プレビュー用の定数を関数内で定義せずグローバルで管理
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_TAG = f"{PREFIX}_preview_tag"
def update_preview_geometry(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_NAME)
context.scene.collection.children.link(col)
# 既存削除
for o in [o for o in col.objects if o.get(PREVIEW_TAG)]:
bpy.data.objects.remove(o, do_unlink=True)
if not props.show_preview: return
bm = bmesh.new()
try:
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=props.sphere_radius)
bmesh.ops.translate(bm, vec=Vector(props.sphere_loc), verts=bm.verts)
mesh = bpy.data.meshes.new(f"PreviewMesh_{PREFIX}")
bm.to_mesh(mesh)
obj = bpy.data.objects.new(f"[Preview] Sphere", mesh)
obj[PREVIEW_TAG] = True
col.objects.link(obj)
mat = create_unique_material(props.sphere_color, "Mat_Preview")
obj.data.materials.append(mat)
obj.select_set(False)
finally:
bm.free()
_timer = None
def delayed_update():
global _timer
_timer = None
if bpy.context and bpy.context.scene:
update_preview_geometry(bpy.context)
return None
def on_update(self, context):
global _timer
if _timer:
try: bpy.app.timers.unregister(_timer)
except: pass
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_SphereProps(PropertyGroup):
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
sphere_color: FloatVectorProperty(name="Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['sphere_color'], update=on_update)
sphere_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['sphere_loc'], update=on_update)
sphere_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['sphere_radius'], min=0.01, update=on_update)
# ==============================================================================
# OPERATORS (修正点: bl_idname には OP_PREFIX = PREFIX.lower() を使用)
# ==============================================================================
class OT_CreateSphere(Operator):
# ★ ここで小文字化されたIDを使用
bl_idname = f"{OP_PREFIX}.create_sphere"
bl_label = "Create Sphere"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=props.sphere_radius)
bmesh.ops.translate(bm, vec=Vector(props.sphere_loc), verts=bm.verts)
mesh = bpy.data.meshes.new(f"Sphere_Mesh")
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(f"Sphere_{datetime.now().strftime('%H%M%S')}", mesh)
if context.collection:
context.collection.objects.link(obj)
else:
context.scene.collection.objects.link(obj)
unique_mat = create_unique_material(props.sphere_color, "Mat_Unique")
obj.data.materials.append(unique_mat)
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
context.view_layer.objects.active = obj
self.report({'INFO'}, "Created Sphere with Unique Material")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string():
target_text = t; break
if not target_text:
self.report({'ERROR'}, "Script source not found.")
return {'CANCELLED'}
code = target_text.as_string()
c, l = props.sphere_color, props.sphere_loc
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "sphere_color": ({c[0]:.4f}, {c[1]:.4f}, {c[2]:.4f}, {c[3]:.4f}),\n'
new_dict += f' "sphere_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
new_dict += f' "sphere_radius": {props.sphere_radius:.4f},\n'
new_dict += "}\n"
try:
start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
pre, post = code.split(start)[0], code.split(end)[1]
final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
context.window_manager.clipboard = final
self.report({'INFO'}, "Code copied!")
except: return {'CANCELLED'}
return {'FINISHED'}
class OT_Reset(Operator):
bl_idname = f"{OP_PREFIX}.reset"
bl_label = "Reset"
def execute(self, context):
p = getattr(context.scene, PROPS_NAME)
p.sphere_loc = (0,0,0); p.sphere_radius = 5.0
return {'FINISHED'}
class OT_OpenUrl(Operator):
bl_idname = f"{OP_PREFIX}.open_url"; bl_label = "Open URL"; url: StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class OT_RemoveAddon(Operator):
bl_idname = f"{OP_PREFIX}.remove_addon"; bl_label = "Remove Addon"
def execute(self, context):
bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_MainPanel(Panel):
bl_label = "Sphere Generator"
bl_idname = f"{PREFIX}_PT_main"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: layout.label(text="Reload Script"); return
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
box = layout.box()
if not props.show_preview:
box.label(text="Preview is Hidden", icon='INFO')
box.prop(props, "sphere_color")
box.prop(props, "sphere_loc")
box.prop(props, "sphere_radius")
box.operator(OT_Reset.bl_idname, icon='LOOP_BACK', text="Reset Position")
layout.separator()
col = layout.column()
col.scale_y = 1.5
col.operator(OT_CreateSphere.bl_idname, icon='MESH_UVSPHERE', text="Create Sphere (Unique)")
class PT_LinksPanel(Panel):
bl_label = "Links"; bl_idname = f"{PREFIX}_PT_links"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
for l in ADDON_LINKS: self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"]).url = l["url"]
class PT_RemovePanel(Panel):
bl_label = "System"; bl_idname = f"{PREFIX}_PT_remove"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): self.layout.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Remove Addon")
# ==============================================================================
# REGISTER
# ==============================================================================
classes = (PG_SphereProps, OT_CreateSphere, OT_CopyFullScript, OT_Reset, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, PT_LinksPanel, PT_RemovePanel)
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_SphereProps))
def unregister():
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes): bpy.utils.unregister_class(c)
if __name__ == "__main__": register()