基本オブジェクト操作

Prefix トーラス正方形 20260324

進化版 画面中央 20260319aa

進化版 画面中央 透視投影視座位置 20260319bb

進化版 画面中央 透視投影視座位置 情報コピー付き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()