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()