blender 今だけメモ
















import bpy
import bmesh
import webbrowser
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import StringProperty, FloatProperty, FloatVectorProperty, EnumProperty, PointerProperty
from datetime import datetime
from mathutils import Vector, Euler
from math import radians

# =========================================================
#  パラメータ設定エリア (初期値と制限値)
# =========================================================
DEFAULT_DICE_SIZE = 2.0
MIN_DICE_SIZE = 0.1

DEFAULT_ROUNDNESS = 0.1
MIN_ROUNDNESS = 0.01
MAX_ROUNDNESS = 0.5

# マイナス=くぼみ(Hole), プラス=出っ張り(Bump)
DEFAULT_PIP_DEPTH = -0.20
MIN_PIP_DEPTH = -1.0
MAX_PIP_DEPTH = 1.0

MIN_PIP_RADIUS = 0.01
MIN_PIP_SPREAD = 0.1
MAX_PIP_SPREAD = 1.5

# 各面の回転初期値
ROT_FACE_BOTTOM = (radians(180), 0, 0)
ROT_FACE_BACK   = (radians(90), 0, 0)
ROT_FACE_LEFT   = (0, radians(90), 0)
ROT_FACE_RIGHT  = (0, radians(-90), 0)
ROT_FACE_FRONT  = (radians(-90), 0, 0)
ROT_FACE_TOP    = (0, 0, 0)
# =========================================================

START_TIMESTAMP = datetime.now().strftime("%Y%m%d%H%M%S")
PREFIX = f"aiond_{START_TIMESTAMP}_aiond"

bl_info = {
    "name": "zionad Addon [Manual Dice & Links]",
    "author": "Your Name & AI Assistant",
    "version": (4, 8), 
    "blender": (4, 0, 0),
    "location": "View3D > Sidebar > Grok Addon",
    "description": "Dice Creator. Fixed Finalize bug and Render visibility.",
    "category": f"   zionlink_{START_TIMESTAMP}_aiond[Addons]   ",
}

ADDON_CATEGORY_NAME = bl_info["category"]
ADDON_LINKS = ({"label": "アドオン削除パネル 20250530", "url": "<https://memo2017.hatenablog.com/entry/2025/05/30/202341>"},)

def get_prefix(): return PREFIX

def update_pip_depth(self, context):
    """存在する全てのピップカッターのZスケールとモディファイアをリアルタイムで更新"""
    cutter_coll = bpy.data.collections.get(self.cutter_collection_name)
    target_obj = bpy.data.objects.get(self.base_dice_name)

    if target_obj and cutter_coll:
        mod = target_obj.modifiers.get("Bool_All_Cutters")
        if not mod:
            mod = target_obj.modifiers.new(name="Bool_All_Cutters", type='BOOLEAN')
            mod.operand_type = 'COLLECTION'
            mod.collection = cutter_coll
            mod.solver = 'EXACT'
        mod.operation = 'DIFFERENCE' if self.pip_depth < 0 else 'UNION'

    if not cutter_coll: return

    safe_depth = abs(self.pip_depth) if abs(self.pip_depth) > 0.001 else 0.001
    for obj in cutter_coll.objects:
        obj.scale.z = safe_depth
    
    if context.area:
        context.area.tag_redraw()

class GROK_DiceSettings(PropertyGroup):
    base_size: FloatProperty(name="Dice Size", default=DEFAULT_DICE_SIZE, min=MIN_DICE_SIZE)
    roundness: FloatProperty(name="Roundness", default=DEFAULT_ROUNDNESS, min=MIN_ROUNDNESS, max=MAX_ROUNDNESS)
    base_color: FloatVectorProperty(name="Base Color", subtype='COLOR', default=(0.9, 0.9, 0.9), min=0, max=1)
    
    dice_system: EnumProperty(
        name="System",
        items=[('RIGHT_HAND', "Right-Hand", ""), ('LEFT_HAND', "Left-Hand", "")],
        default='RIGHT_HAND'
    )
    
    pip_rot: FloatVectorProperty(name="Pip Rotation", subtype='EULER', unit='ROTATION')
    
    pip_depth: FloatProperty(
        name="Depth (-) / Height (+)",
        default=DEFAULT_PIP_DEPTH,
        min=MIN_PIP_DEPTH,
        max=MAX_PIP_DEPTH,
        description="Negative values create holes, positive values create bumps",
        update=update_pip_depth
    )
    
    pip_radius_1: FloatProperty(name="Radius 1", default=0.88, min=MIN_PIP_RADIUS)
    pip_spread_1: FloatProperty(name="Spread 1", default=1.50, min=MIN_PIP_SPREAD, max=MAX_PIP_SPREAD)
    pip_color_1: FloatVectorProperty(name="Color 1", subtype='COLOR', default=(0.85, 0.25, 0.25), min=0, max=1)

    pip_radius_2: FloatProperty(name="Radius 2", default=0.91, min=MIN_PIP_RADIUS)
    pip_spread_2: FloatProperty(name="Spread 2", default=0.55, min=MIN_PIP_SPREAD, max=MAX_PIP_SPREAD)
    pip_color_2: FloatVectorProperty(name="Color 2", subtype='COLOR', default=(0.35, 0.75, 0.40), min=0, max=1)

    pip_radius_3: FloatProperty(name="Radius 3", default=0.52, min=MIN_PIP_RADIUS)
    pip_spread_3: FloatProperty(name="Spread 3", default=0.94, min=MIN_PIP_SPREAD, max=MAX_PIP_SPREAD)
    pip_color_3: FloatVectorProperty(name="Color 3", subtype='COLOR', default=(0.45, 0.55, 0.90), min=0, max=1)

    pip_radius_4: FloatProperty(name="Radius 4", default=0.67, min=MIN_PIP_RADIUS)
    pip_spread_4: FloatProperty(name="Spread 4", default=0.61, min=MIN_PIP_SPREAD, max=MAX_PIP_SPREAD)
    pip_color_4: FloatVectorProperty(name="Color 4", subtype='COLOR', default=(0.90, 0.85, 0.35), min=0, max=1)

    pip_radius_5: FloatProperty(name="Radius 5", default=0.73, min=MIN_PIP_RADIUS)
    pip_spread_5: FloatProperty(name="Spread 5", default=0.76, min=MIN_PIP_SPREAD, max=MAX_PIP_SPREAD)
    pip_color_5: FloatVectorProperty(name="Color 5", subtype='COLOR', default=(0.35, 0.80, 0.85), min=0, max=1)

    pip_radius_6: FloatProperty(name="Radius 6", default=0.70, min=MIN_PIP_RADIUS)
    pip_spread_6: FloatProperty(name="Spread 6", default=0.73, min=MIN_PIP_SPREAD, max=MAX_PIP_SPREAD)
    pip_color_6: FloatVectorProperty(name="Color 6", subtype='COLOR', default=(0.75, 0.45, 0.75), min=0, max=1)
    
    base_dice_name: StringProperty()
    cutter_collection_name: StringProperty()

class GROK_OT_CreateBaseDice(Operator):
    bl_idname = f"{get_prefix()}.create_base_dice"
    bl_label = "1. Create Base Dice"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        settings = context.scene.grok_dice_settings
        collection_name = f"{PREFIX}_Cutters"
        settings.cutter_collection_name = collection_name
        
        if collection_name in bpy.data.collections:
            coll = bpy.data.collections[collection_name]
            for obj in list(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:
            coll = bpy.data.collections.new(collection_name)
            context.scene.collection.children.link(coll)
        
        die_name = "Dice_Base"
        die_obj = bpy.data.objects.get(die_name)
        if die_obj:
            mesh = die_obj.data
            bpy.data.objects.remove(die_obj, do_unlink=True)
            if mesh:
                try: bpy.data.meshes.remove(mesh, do_unlink=True)
                except: pass
                
        mesh = bpy.data.meshes.new(f"{die_name}_Mesh")
        die_obj = bpy.data.objects.new(die_name, mesh)
        context.scene.collection.objects.link(die_obj)
        settings.base_dice_name = die_name
        
        bm = bmesh.new()
        bmesh.ops.create_cube(bm, size=settings.base_size)
        bm.to_mesh(mesh)
        bm.free()
        
        base_mat_name = f"{PREFIX}_Base_Material"
        mat = bpy.data.materials.get(base_mat_name)
        if not mat:
            mat = bpy.data.materials.new(name=base_mat_name)
            mat.use_nodes = True
        mat.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (*settings.base_color, 1.0)
        mat.diffuse_color = (*settings.base_color, 1.0)
        die_obj.data.materials.append(mat)
        
        bevel_mod = die_obj.modifiers.new(name="RoundEdges", type='BEVEL')
        bevel_mod.width = settings.roundness * settings.base_size
        bevel_mod.segments = 5
        subdiv_mod = die_obj.modifiers.new(name="Smooth", type='SUBSURF')
        subdiv_mod.levels = 2
        
        for p in mesh.polygons: p.use_smooth = True
        if hasattr(mesh, "use_auto_smooth"):
            mesh.use_auto_smooth = True
            
        return {'FINISHED'}

class GROK_OT_CreatePipCutter(Operator):
    bl_idname = f"{get_prefix()}.create_pip_cutter"
    bl_label = "2. Create All Pips & Combine"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        settings = context.scene.grok_dice_settings
        cutter_collection = bpy.data.collections.get(settings.cutter_collection_name)
        target_obj = bpy.data.objects.get(settings.base_dice_name)

        if not target_obj: return {'CANCELLED'}

        if cutter_collection:
            objects_to_remove = [obj for obj in cutter_collection.objects if obj.name.startswith("Pip_Cutters_")]
            for obj in objects_to_remove:
                mesh = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if mesh:
                    try: bpy.data.meshes.remove(mesh, do_unlink=True)
                    except: pass

        s = settings.base_size / 2
        physical_faces = {
            'BOTTOM': {'center': Vector((0, 0, -s)), 'rotation': ROT_FACE_BOTTOM}, 'BACK':   {'center': Vector((0, s, 0)),  'rotation': ROT_FACE_BACK},
            'LEFT':   {'center': Vector((-s, 0, 0)), 'rotation': ROT_FACE_LEFT},   'RIGHT':  {'center': Vector((s, 0, 0)),  'rotation': ROT_FACE_RIGHT},
            'FRONT':  {'center': Vector((0, -s, 0)),  'rotation': ROT_FACE_FRONT},  'TOP':    {'center': Vector((0, 0, s)),  'rotation': ROT_FACE_TOP},
        }
        pip_to_physical_map = {
            'RIGHT_HAND': {1: 'BOTTOM', 2: 'BACK', 3: 'LEFT', 4: 'RIGHT', 5: 'FRONT', 6: 'TOP'},
            'LEFT_HAND':  {1: 'BOTTOM', 2: 'BACK', 3: 'RIGHT', 4: 'LEFT', 5: 'FRONT', 6: 'TOP'},
        }
        
        for pip_count in range(1, 7):
            physical_face_key = pip_to_physical_map[settings.dice_system][pip_count]
            physical_data = physical_faces[physical_face_key]
            
            face_center = physical_data['center']
            base_rot_euler = Euler(physical_data['rotation'], 'XYZ')
            base_rot_quat = base_rot_euler.to_quaternion()
            user_rot_quat = Euler(settings.pip_rot, 'XYZ').to_quaternion()
            
            pip_radius = getattr(settings, f'pip_radius_{pip_count}')
            pip_spread = getattr(settings, f'pip_spread_{pip_count}')
            p = settings.base_size * 0.3 * pip_spread
            
            pip_patterns = {
                1: [(0,0,0)], 2: [(-p,-p,0),(p,p,0)], 3: [(-p,-p,0),(0,0,0),(p,p,0)], 4: [(-p,-p,0),(-p,p,0),(p,-p,0),(p,p,0)],
                5: [(-p,-p,0),(-p,p,0),(0,0,0),(p,-p,0),(p,p,0)], 6: [(-p,-p,0),(-p,0,0),(-p,p,0),(p,-p,0),(p,0,0),(p,p,0)],
            }
            
            mat_name = f"{PREFIX}_Pip_Material_{pip_count}"
            mat = bpy.data.materials.get(mat_name)
            if not mat:
                mat = bpy.data.materials.new(name=mat_name)
                mat.use_nodes = True
            color_val = getattr(settings, f"pip_color_{pip_count}")
            mat.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (*color_val, 1.0)
            mat.diffuse_color = (*color_val, 1.0)

            pip_positions = pip_patterns.get(pip_count, [])
            for i, pos in enumerate(pip_positions):
                location = face_center + (base_rot_quat @ user_rot_quat @ Vector(pos))
                
                mesh_name = f"Pip_Cutters_Face_{pip_count}_{i}"
                mesh = bpy.data.meshes.new(mesh_name + "_Mesh")
                cutter_obj = bpy.data.objects.new(mesh_name, mesh)
                
                if cutter_collection:
                    cutter_collection.objects.link(cutter_obj)
                
                bm = bmesh.new()
                bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
                bm.to_mesh(mesh)
                bm.free()
                
                cutter_obj.data.materials.append(mat)
                cutter_obj.location = location
                cutter_obj.rotation_euler = base_rot_euler
                
                safe_depth = abs(settings.pip_depth) if abs(settings.pip_depth) > 0.001 else 0.001
                cutter_obj.scale = (pip_radius, pip_radius, safe_depth)
                
                # ★修正ポイント:計算には含めるが、表示は見えなくする確実な設定
                cutter_obj.display_type = 'WIRE' # SolidやRenderedプレビューで透明になる
                cutter_obj.hide_render = True    # 最終レンダリング画像(F12)で写らなくなる
                # (前回ここにあった hide_set や hide_viewport はバグの元なので削除しました)

                for poly in mesh.polygons: poly.use_smooth = True
                if hasattr(mesh, "use_auto_smooth"):
                    mesh.use_auto_smooth = True

        if cutter_collection:
            mod = target_obj.modifiers.get("Bool_All_Cutters")
            if not mod:
                mod = target_obj.modifiers.new(name="Bool_All_Cutters", type='BOOLEAN')
                mod.operand_type = 'COLLECTION'
                mod.collection = cutter_collection
                mod.solver = 'EXACT'
            mod.operation = 'DIFFERENCE' if settings.pip_depth < 0 else 'UNION'

            wn_mod = target_obj.modifiers.get("Fix_Shading")
            if not wn_mod:
                wn_mod = target_obj.modifiers.new(name="Fix_Shading", type='WEIGHTED_NORMAL')
                wn_mod.keep_sharp = True
            
            context.view_layer.objects.active = target_obj
            try:
                bpy.ops.object.modifier_move_to_bottom(modifier="Fix_Shading")
            except:
                pass

            target_mat_names = {m.name for m in target_obj.data.materials if m}
            for cutter_obj in cutter_collection.objects:
                for mat in cutter_obj.data.materials:
                    if mat and mat.name not in target_mat_names:
                        target_obj.data.materials.append(mat)
                        target_mat_names.add(mat.name)

        return {'FINISHED'}

class GROK_OT_ApplyAllModifiers(Operator):
    bl_idname = f"{get_prefix()}.apply_all_modifiers"
    bl_label = "3. Finalize Shape"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        obj = bpy.data.objects.get(context.scene.grok_dice_settings.base_dice_name)
        return obj and len(obj.modifiers) > 0

    def execute(self, context):
        settings = context.scene.grok_dice_settings
        target_obj = bpy.data.objects.get(settings.base_dice_name)
        if not target_obj: return {'CANCELLED'}
        
        # ★修正ポイント:画面で見えている状態を確実に適用(Apply)する方式に変更
        context.view_layer.objects.active = target_obj
        target_obj.select_set(True)
        
        # モディファイアを上から順番に確定させる
        for mod in target_obj.modifiers:
            try:
                bpy.ops.object.modifier_apply(modifier=mod.name)
            except Exception as e:
                print(f"Failed to apply {mod.name}: {e}")
        
        # 用済みのカッターコレクションを削除して綺麗にする
        collection_name = settings.cutter_collection_name
        if collection_name in bpy.data.collections:
            coll = bpy.data.collections[collection_name]
            for obj in list(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(coll)
        settings.cutter_collection_name = ""
        
        return {'FINISHED'}

class GROK_OT_OpenURL(Operator):
    bl_idname = f"{get_prefix()}.open_url"; bl_label = "Open URL"; url: StringProperty(default="")
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

class GROK_OT_RemoveAllPanels(Operator):
    bl_idname = f"{get_prefix()}.remove_all_panels"; bl_label = "Unregister Addon"
    def execute(self, context): unregister(); return {'FINISHED'}

class GROK_PT_DiceCreatorPanel(Panel):
    bl_label = "Manual Dice Creator"
    bl_idname = f"{PREFIX}_PT_dice_creator"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY_NAME
    bl_order = 0

    def draw(self, context):
        layout = self.layout
        settings = context.scene.grok_dice_settings
        
        box = layout.box()
        box.label(text="Step 1: Create Base", icon='MESH_CUBE')
        col = box.column(align=True)
        col.prop(settings, "base_size")
        col.prop(settings, "roundness")
        col.prop(settings, "base_color")
        col.prop(settings, "dice_system")
        box.operator(GROK_OT_CreateBaseDice.bl_idname)
        layout.separator()

        box = layout.box()
        box.label(text="Step 2: Setup Pips", icon='MESH_CYLINDER')
        
        col = box.column(align=True)
        col.prop(settings, "pip_rot")
        col.prop(settings, "pip_depth")
        
        row = box.row()
        row.label(text="← Negative(Hole)   Positive(Bump) →", icon='INFO')
        
        settings_box = box.box()
        header = settings_box.row(align=True)
        header.label(text="Face"); header.label(text="Radius"); header.label(text="Spread"); header.label(text="Color")
        for i in range(1, 7):
            r = settings_box.row(align=True)
            r.label(text=f"{i}")
            r.prop(settings, f"pip_radius_{i}", text="")
            r.prop(settings, f"pip_spread_{i}", text="")
            r.prop(settings, f"pip_color_{i}", text="")
            
        box.operator(GROK_OT_CreatePipCutter.bl_idname)
        box.label(text="Shading is automatically fixed.", icon='INFO')
        layout.separator()

        box = layout.box()
        box.label(text="Step 3: Finalize", icon='CHECKMARK')
        box.operator(GROK_OT_ApplyAllModifiers.bl_idname)
        box.label(text="Applies modifiers safely.", icon='INFO')

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 = ADDON_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 = ADDON_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_DiceSettings, GROK_OT_CreateBaseDice, GROK_OT_CreatePipCutter, GROK_OT_ApplyAllModifiers,
    GROK_OT_OpenURL, GROK_OT_RemoveAllPanels, GROK_PT_DiceCreatorPanel, GROK_PT_LinksPanel, GROK_PT_RemovePanel
)

def register():
    for cls in classes: bpy.utils.register_class(cls)
    bpy.types.Scene.grok_dice_settings = PointerProperty(type=GROK_DiceSettings)

def unregister():
    if hasattr(bpy.types.Scene, 'grok_dice_settings'): del bpy.types.Scene.grok_dice_settings
    for cls in reversed(classes):
        try: bpy.utils.unregister_class(cls)
        except: pass

if __name__ == "__main__": register()

import bpy
import bmesh
import webbrowser
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import StringProperty, FloatProperty, FloatVectorProperty, EnumProperty, PointerProperty
from datetime import datetime
from mathutils import Vector, Euler
from math import radians

# =========================================================
#  パラメータ設定エリア (初期値と制限値)
# =========================================================
DEFAULT_DICE_SIZE = 2.0
MIN_DICE_SIZE = 0.1

DEFAULT_ROUNDNESS = 0.1
MIN_ROUNDNESS = 0.01
MAX_ROUNDNESS = 0.5

# マイナス=くぼみ(Hole), プラス=出っ張り(Bump)
DEFAULT_PIP_DEPTH = -0.20
MIN_PIP_DEPTH = -1.0
MAX_PIP_DEPTH = 1.0

MIN_PIP_RADIUS = 0.01
MIN_PIP_SPREAD = 0.1
MAX_PIP_SPREAD = 1.5

# 各面の回転初期値
ROT_FACE_BOTTOM = (radians(180), 0, 0)
ROT_FACE_BACK   = (radians(90), 0, 0)
ROT_FACE_LEFT   = (0, radians(90), 0)
ROT_FACE_RIGHT  = (0, radians(-90), 0)
ROT_FACE_FRONT  = (radians(-90), 0, 0)
ROT_FACE_TOP    = (0, 0, 0)
# =========================================================

START_TIMESTAMP = datetime.now().strftime("%Y%m%d%H%M%S")
PREFIX = f"aiond_{START_TIMESTAMP}_aiond"

bl_info = {
    "name": "zionad Addon [Manual Dice & Links]",
    "author": "Your Name & AI Assistant",
    "version": (4, 7), 
    "blender": (4, 0, 0),
    "location": "View3D > Sidebar > Grok Addon",
    "description": "Dice Creator. Fixed Rendered view visibility issue for perfect holes.",
    "category": f"   zionlink_{START_TIMESTAMP}_aiond[Addons]   ",
}

ADDON_CATEGORY_NAME = bl_info["category"]
ADDON_LINKS = ({"label": "アドオン削除パネル 20250530", "url": "<https://memo2017.hatenablog.com/entry/2025/05/30/202341>"},)

def get_prefix(): return PREFIX

def update_pip_depth(self, context):
    """存在する全てのピップカッターのZスケールとモディファイアをリアルタイムで更新"""
    cutter_coll = bpy.data.collections.get(self.cutter_collection_name)
    target_obj = bpy.data.objects.get(self.base_dice_name)

    if target_obj and cutter_coll:
        mod = target_obj.modifiers.get("Bool_All_Cutters")
        if not mod:
            mod = target_obj.modifiers.new(name="Bool_All_Cutters", type='BOOLEAN')
            mod.operand_type = 'COLLECTION'
            mod.collection = cutter_coll
            mod.solver = 'EXACT'
        mod.operation = 'DIFFERENCE' if self.pip_depth < 0 else 'UNION'

    if not cutter_coll: return

    safe_depth = abs(self.pip_depth) if abs(self.pip_depth) > 0.001 else 0.001
    for obj in cutter_coll.objects:
        obj.scale.z = safe_depth
    
    if context.area:
        context.area.tag_redraw()

class GROK_DiceSettings(PropertyGroup):
    base_size: FloatProperty(name="Dice Size", default=DEFAULT_DICE_SIZE, min=MIN_DICE_SIZE)
    roundness: FloatProperty(name="Roundness", default=DEFAULT_ROUNDNESS, min=MIN_ROUNDNESS, max=MAX_ROUNDNESS)
    base_color: FloatVectorProperty(name="Base Color", subtype='COLOR', default=(0.9, 0.9, 0.9), min=0, max=1)
    
    dice_system: EnumProperty(
        name="System",
        items=[('RIGHT_HAND', "Right-Hand", ""), ('LEFT_HAND', "Left-Hand", "")],
        default='RIGHT_HAND'
    )
    
    pip_rot: FloatVectorProperty(name="Pip Rotation", subtype='EULER', unit='ROTATION')
    
    pip_depth: FloatProperty(
        name="Depth (-) / Height (+)",
        default=DEFAULT_PIP_DEPTH,
        min=MIN_PIP_DEPTH,
        max=MAX_PIP_DEPTH,
        description="Negative values create holes, positive values create bumps",
        update=update_pip_depth
    )
    
    pip_radius_1: FloatProperty(name="Radius 1", default=0.88, min=MIN_PIP_RADIUS)
    pip_spread_1: FloatProperty(name="Spread 1", default=1.50, min=MIN_PIP_SPREAD, max=MAX_PIP_SPREAD)
    pip_color_1: FloatVectorProperty(name="Color 1", subtype='COLOR', default=(0.85, 0.25, 0.25), min=0, max=1)

    pip_radius_2: FloatProperty(name="Radius 2", default=0.91, min=MIN_PIP_RADIUS)
    pip_spread_2: FloatProperty(name="Spread 2", default=0.55, min=MIN_PIP_SPREAD, max=MAX_PIP_SPREAD)
    pip_color_2: FloatVectorProperty(name="Color 2", subtype='COLOR', default=(0.35, 0.75, 0.40), min=0, max=1)

    pip_radius_3: FloatProperty(name="Radius 3", default=0.52, min=MIN_PIP_RADIUS)
    pip_spread_3: FloatProperty(name="Spread 3", default=0.94, min=MIN_PIP_SPREAD, max=MAX_PIP_SPREAD)
    pip_color_3: FloatVectorProperty(name="Color 3", subtype='COLOR', default=(0.45, 0.55, 0.90), min=0, max=1)

    pip_radius_4: FloatProperty(name="Radius 4", default=0.67, min=MIN_PIP_RADIUS)
    pip_spread_4: FloatProperty(name="Spread 4", default=0.61, min=MIN_PIP_SPREAD, max=MAX_PIP_SPREAD)
    pip_color_4: FloatVectorProperty(name="Color 4", subtype='COLOR', default=(0.90, 0.85, 0.35), min=0, max=1)

    pip_radius_5: FloatProperty(name="Radius 5", default=0.73, min=MIN_PIP_RADIUS)
    pip_spread_5: FloatProperty(name="Spread 5", default=0.76, min=MIN_PIP_SPREAD, max=MAX_PIP_SPREAD)
    pip_color_5: FloatVectorProperty(name="Color 5", subtype='COLOR', default=(0.35, 0.80, 0.85), min=0, max=1)

    pip_radius_6: FloatProperty(name="Radius 6", default=0.70, min=MIN_PIP_RADIUS)
    pip_spread_6: FloatProperty(name="Spread 6", default=0.73, min=MIN_PIP_SPREAD, max=MAX_PIP_SPREAD)
    pip_color_6: FloatVectorProperty(name="Color 6", subtype='COLOR', default=(0.75, 0.45, 0.75), min=0, max=1)
    
    base_dice_name: StringProperty()
    cutter_collection_name: StringProperty()

class GROK_OT_CreateBaseDice(Operator):
    bl_idname = f"{get_prefix()}.create_base_dice"
    bl_label = "1. Create Base Dice"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        settings = context.scene.grok_dice_settings
        collection_name = f"{PREFIX}_Cutters"
        settings.cutter_collection_name = collection_name
        
        if collection_name in bpy.data.collections:
            coll = bpy.data.collections[collection_name]
            for obj in list(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:
            coll = bpy.data.collections.new(collection_name)
            context.scene.collection.children.link(coll)
        
        die_name = "Dice_Base"
        die_obj = bpy.data.objects.get(die_name)
        if die_obj:
            mesh = die_obj.data
            bpy.data.objects.remove(die_obj, do_unlink=True)
            if mesh:
                try: bpy.data.meshes.remove(mesh, do_unlink=True)
                except: pass
                
        mesh = bpy.data.meshes.new(f"{die_name}_Mesh")
        die_obj = bpy.data.objects.new(die_name, mesh)
        context.scene.collection.objects.link(die_obj)
        settings.base_dice_name = die_name
        
        bm = bmesh.new()
        bmesh.ops.create_cube(bm, size=settings.base_size)
        bm.to_mesh(mesh)
        bm.free()
        
        base_mat_name = f"{PREFIX}_Base_Material"
        mat = bpy.data.materials.get(base_mat_name)
        if not mat:
            mat = bpy.data.materials.new(name=base_mat_name)
            mat.use_nodes = True
        mat.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (*settings.base_color, 1.0)
        mat.diffuse_color = (*settings.base_color, 1.0)
        die_obj.data.materials.append(mat)
        
        bevel_mod = die_obj.modifiers.new(name="RoundEdges", type='BEVEL')
        bevel_mod.width = settings.roundness * settings.base_size
        bevel_mod.segments = 5
        subdiv_mod = die_obj.modifiers.new(name="Smooth", type='SUBSURF')
        subdiv_mod.levels = 2
        
        for p in mesh.polygons: p.use_smooth = True
        if hasattr(mesh, "use_auto_smooth"):
            mesh.use_auto_smooth = True
            
        return {'FINISHED'}

class GROK_OT_CreatePipCutter(Operator):
    bl_idname = f"{get_prefix()}.create_pip_cutter"
    bl_label = "2. Create All Pips & Combine"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        settings = context.scene.grok_dice_settings
        cutter_collection = bpy.data.collections.get(settings.cutter_collection_name)
        target_obj = bpy.data.objects.get(settings.base_dice_name)

        if not target_obj: return {'CANCELLED'}

        if cutter_collection:
            objects_to_remove = [obj for obj in cutter_collection.objects if obj.name.startswith("Pip_Cutters_")]
            for obj in objects_to_remove:
                mesh = obj.data
                bpy.data.objects.remove(obj, do_unlink=True)
                if mesh:
                    try: bpy.data.meshes.remove(mesh, do_unlink=True)
                    except: pass

        s = settings.base_size / 2
        physical_faces = {
            'BOTTOM': {'center': Vector((0, 0, -s)), 'rotation': ROT_FACE_BOTTOM}, 'BACK':   {'center': Vector((0, s, 0)),  'rotation': ROT_FACE_BACK},
            'LEFT':   {'center': Vector((-s, 0, 0)), 'rotation': ROT_FACE_LEFT},   'RIGHT':  {'center': Vector((s, 0, 0)),  'rotation': ROT_FACE_RIGHT},
            'FRONT':  {'center': Vector((0, -s, 0)),  'rotation': ROT_FACE_FRONT},  'TOP':    {'center': Vector((0, 0, s)),  'rotation': ROT_FACE_TOP},
        }
        pip_to_physical_map = {
            'RIGHT_HAND': {1: 'BOTTOM', 2: 'BACK', 3: 'LEFT', 4: 'RIGHT', 5: 'FRONT', 6: 'TOP'},
            'LEFT_HAND':  {1: 'BOTTOM', 2: 'BACK', 3: 'RIGHT', 4: 'LEFT', 5: 'FRONT', 6: 'TOP'},
        }
        
        for pip_count in range(1, 7):
            physical_face_key = pip_to_physical_map[settings.dice_system][pip_count]
            physical_data = physical_faces[physical_face_key]
            
            face_center = physical_data['center']
            base_rot_euler = Euler(physical_data['rotation'], 'XYZ')
            base_rot_quat = base_rot_euler.to_quaternion()
            user_rot_quat = Euler(settings.pip_rot, 'XYZ').to_quaternion()
            
            pip_radius = getattr(settings, f'pip_radius_{pip_count}')
            pip_spread = getattr(settings, f'pip_spread_{pip_count}')
            p = settings.base_size * 0.3 * pip_spread
            
            pip_patterns = {
                1: [(0,0,0)], 2: [(-p,-p,0),(p,p,0)], 3: [(-p,-p,0),(0,0,0),(p,p,0)], 4: [(-p,-p,0),(-p,p,0),(p,-p,0),(p,p,0)],
                5: [(-p,-p,0),(-p,p,0),(0,0,0),(p,-p,0),(p,p,0)], 6: [(-p,-p,0),(-p,0,0),(-p,p,0),(p,-p,0),(p,0,0),(p,p,0)],
            }
            
            mat_name = f"{PREFIX}_Pip_Material_{pip_count}"
            mat = bpy.data.materials.get(mat_name)
            if not mat:
                mat = bpy.data.materials.new(name=mat_name)
                mat.use_nodes = True
            color_val = getattr(settings, f"pip_color_{pip_count}")
            mat.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (*color_val, 1.0)
            mat.diffuse_color = (*color_val, 1.0)

            pip_positions = pip_patterns.get(pip_count, [])
            for i, pos in enumerate(pip_positions):
                location = face_center + (base_rot_quat @ user_rot_quat @ Vector(pos))
                
                mesh_name = f"Pip_Cutters_Face_{pip_count}_{i}"
                mesh = bpy.data.meshes.new(mesh_name + "_Mesh")
                cutter_obj = bpy.data.objects.new(mesh_name, mesh)
                
                if cutter_collection:
                    cutter_collection.objects.link(cutter_obj)
                
                bm = bmesh.new()
                bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
                bm.to_mesh(mesh)
                bm.free()
                
                cutter_obj.data.materials.append(mat)
                cutter_obj.location = location
                cutter_obj.rotation_euler = base_rot_euler
                
                safe_depth = abs(settings.pip_depth) if abs(settings.pip_depth) > 0.001 else 0.001
                cutter_obj.scale = (pip_radius, pip_radius, safe_depth)
                
                # ★ レンダーモードで穴がふさがるのを防ぐための設定 ★
                cutter_obj.hide_render = True        # F12レンダリングで非表示
                cutter_obj.hide_viewport = True      # レンダープレビューで非表示
                cutter_obj.hide_set(True)            # ビューポート全体で完全に非表示

                for poly in mesh.polygons: poly.use_smooth = True
                if hasattr(mesh, "use_auto_smooth"):
                    mesh.use_auto_smooth = True

        if cutter_collection:
            mod = target_obj.modifiers.get("Bool_All_Cutters")
            if not mod:
                mod = target_obj.modifiers.new(name="Bool_All_Cutters", type='BOOLEAN')
                mod.operand_type = 'COLLECTION'
                mod.collection = cutter_collection
                mod.solver = 'EXACT'
            mod.operation = 'DIFFERENCE' if settings.pip_depth < 0 else 'UNION'

            wn_mod = target_obj.modifiers.get("Fix_Shading")
            if not wn_mod:
                wn_mod = target_obj.modifiers.new(name="Fix_Shading", type='WEIGHTED_NORMAL')
                wn_mod.keep_sharp = True
            
            context.view_layer.objects.active = target_obj
            try:
                bpy.ops.object.modifier_move_to_bottom(modifier="Fix_Shading")
            except:
                pass

            target_mat_names = {m.name for m in target_obj.data.materials if m}
            for cutter_obj in cutter_collection.objects:
                for mat in cutter_obj.data.materials:
                    if mat and mat.name not in target_mat_names:
                        target_obj.data.materials.append(mat)
                        target_mat_names.add(mat.name)

        return {'FINISHED'}

class GROK_OT_ApplyAllModifiers(Operator):
    bl_idname = f"{get_prefix()}.apply_all_modifiers"
    bl_label = "3. Finalize Shape"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        obj = bpy.data.objects.get(context.scene.grok_dice_settings.base_dice_name)
        return obj and len(obj.modifiers) > 0

    def execute(self, context):
        settings = context.scene.grok_dice_settings
        target_obj = bpy.data.objects.get(settings.base_dice_name)
        if not target_obj: return {'CANCELLED'}
        
        depsgraph = context.evaluated_depsgraph_get()
        object_eval = target_obj.evaluated_get(depsgraph)
        mesh_from_eval = bpy.data.meshes.new_from_object(object_eval)
        
        target_obj.modifiers.clear()
        target_obj.data = mesh_from_eval
        
        collection_name = settings.cutter_collection_name
        if collection_name in bpy.data.collections:
            coll = bpy.data.collections[collection_name]
            for obj in list(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(coll)
        settings.cutter_collection_name = ""
        return {'FINISHED'}

class GROK_OT_OpenURL(Operator):
    bl_idname = f"{get_prefix()}.open_url"; bl_label = "Open URL"; url: StringProperty(default="")
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

class GROK_OT_RemoveAllPanels(Operator):
    bl_idname = f"{get_prefix()}.remove_all_panels"; bl_label = "Unregister Addon"
    def execute(self, context): unregister(); return {'FINISHED'}

class GROK_PT_DiceCreatorPanel(Panel):
    bl_label = "Manual Dice Creator"
    bl_idname = f"{PREFIX}_PT_dice_creator"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY_NAME
    bl_order = 0

    def draw(self, context):
        layout = self.layout
        settings = context.scene.grok_dice_settings
        
        box = layout.box()
        box.label(text="Step 1: Create Base", icon='MESH_CUBE')
        col = box.column(align=True)
        col.prop(settings, "base_size")
        col.prop(settings, "roundness")
        col.prop(settings, "base_color")
        col.prop(settings, "dice_system")
        box.operator(GROK_OT_CreateBaseDice.bl_idname)
        layout.separator()

        box = layout.box()
        box.label(text="Step 2: Setup Pips", icon='MESH_CYLINDER')
        
        col = box.column(align=True)
        col.prop(settings, "pip_rot")
        col.prop(settings, "pip_depth")
        
        row = box.row()
        row.label(text="← Negative(Hole)   Positive(Bump) →", icon='INFO')
        
        settings_box = box.box()
        header = settings_box.row(align=True)
        header.label(text="Face"); header.label(text="Radius"); header.label(text="Spread"); header.label(text="Color")
        for i in range(1, 7):
            r = settings_box.row(align=True)
            r.label(text=f"{i}")
            r.prop(settings, f"pip_radius_{i}", text="")
            r.prop(settings, f"pip_spread_{i}", text="")
            r.prop(settings, f"pip_color_{i}", text="")
            
        box.operator(GROK_OT_CreatePipCutter.bl_idname)
        box.label(text="Shading is automatically fixed.", icon='INFO')
        layout.separator()

        box = layout.box()
        box.label(text="Step 3: Finalize", icon='CHECKMARK')
        box.operator(GROK_OT_ApplyAllModifiers.bl_idname)

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 = ADDON_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 = ADDON_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_DiceSettings, GROK_OT_CreateBaseDice, GROK_OT_CreatePipCutter, GROK_OT_ApplyAllModifiers,
    GROK_OT_OpenURL, GROK_OT_RemoveAllPanels, GROK_PT_DiceCreatorPanel, GROK_PT_LinksPanel, GROK_PT_RemovePanel
)

def register():
    for cls in classes: bpy.utils.register_class(cls)
    bpy.types.Scene.grok_dice_settings = PointerProperty(type=GROK_DiceSettings)

def unregister():
    if hasattr(bpy.types.Scene, 'grok_dice_settings'): del bpy.types.Scene.grok_dice_settings
    for cls in reversed(classes):
        try: bpy.utils.unregister_class(cls)
        except: pass

if __name__ == "__main__": register()