# ============================================================
# Blender 5.0+ Cylinder Grid Floor
# ============================================================
# X/Y方向の円柱グリッド床を生成するアドオン
#
# 主な特徴
# - Blender 5.0+ 専用
# - X方向 / Y方向の円柱本数・太さ・間隔・長さを独立設定
# - X方向 / Y方向の表面色・裏面色・透明度を独立設定
# - 天井蓋 / 床面蓋を独立Objectとして生成
# - 天井蓋 / 床面蓋はそれぞれ独立した削除チェック
# - 円柱削除ボタンは設けない
# - リアルタイム更新
# - 「完成Objectとして切離し」
# - 「次のObjectを作成」
# - 完成Objectだけを対象にしたJoin確認ダイアログ
# - 位置情報表示 / コピー
# - 現在の設定を初期値コードとしてコピー
# - 完成ObjectのMaterial完全独立化
# - Linksは最後から2番目、Addon Removeは最後
# ============================================================
bl_info = {
"name": "Cylinder Grid Floor",
"author": "zionad / ChatGPT",
"version": (1, 1, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar > Cylinder Grid",
"description": "X/Y方向の独立円柱グリッド床を生成するBlender 5.0+専用アドオン",
"category": "Add Mesh",
}
# ============================================================
# 作成・更新情報
# ============================================================
CREATED_DATE = "2026-10-06"
UPDATED_DATE = "2026-10-06"
# ============================================================
# imports
# ============================================================
import bpy
import bmesh
import math
from mathutils import Vector
# ============================================================
# 共通定義
# ============================================================
PREFIX_VAL = "CYL_GRID_FLOOR_"
MAIN_COLLECTION_NAME = f"{PREFIX_VAL}CURRENT"
FIXED_COLLECTION_NAME = f"{PREFIX_VAL}COMPLETED"
CURRENT_WALL_PREFIX = f"{PREFIX_VAL}WALL_"
CURRENT_TOP_PREFIX = f"{PREFIX_VAL}TOP_CAP_"
CURRENT_BOTTOM_PREFIX = f"{PREFIX_VAL}BOTTOM_CAP_"
FIXED_PREFIX = f"{PREFIX_VAL}FIXED_"
PANEL_CATEGORY = "Cylinder Grid"
X_AXIS = "X"
Y_AXIS = "Y"
# ============================================================
# 初期値 (ここをコピーしたコードで書き換え可能)
# ============================================================
DEFAULT_X_COUNT = 5
DEFAULT_Y_COUNT = 5
DEFAULT_X_RADIUS = 0.1000
DEFAULT_Y_RADIUS = 0.1000
DEFAULT_X_SPACING = 1.0000
DEFAULT_Y_SPACING = 1.0000
DEFAULT_X_LENGTH = 5.0000
DEFAULT_Y_LENGTH = 5.0000
DEFAULT_X_COLOR = (0.08, 0.32, 1.00, 1.0)
DEFAULT_X_ALPHA = 1.00
DEFAULT_X_BACK_COLOR = (0.02, 0.10, 0.35, 1.0)
DEFAULT_X_BACK_ALPHA = 1.00
DEFAULT_Y_COLOR = (0.10, 0.80, 0.35, 1.0)
DEFAULT_Y_ALPHA = 1.00
DEFAULT_Y_BACK_COLOR = (0.02, 0.30, 0.12, 1.0)
DEFAULT_Y_BACK_ALPHA = 1.00
DEFAULT_TOP_COLOR = (0.15, 0.65, 1.00, 1.0)
DEFAULT_TOP_ALPHA = 0.35
DEFAULT_BOTTOM_COLOR = (0.05, 0.45, 0.20, 1.0)
DEFAULT_BOTTOM_ALPHA = 0.35
DEFAULT_SEGMENTS = 32
DEFAULT_CREATE_TOP_CAP = True
DEFAULT_CREATE_BOTTOM_CAP = True
DEFAULT_CENTER_X = 0.0000
DEFAULT_CENTER_Y = 0.0000
DEFAULT_CENTER_Z = 0.0000
# ============================================================
# Utility
# ============================================================
def ensure_collection(name, parent=None):
collection = bpy.data.collections.get(name)
if collection is None:
collection = bpy.data.collections.new(name)
if parent is None:
scene_collection = bpy.context.scene.collection
scene_collection.children.link(collection)
else:
parent.children.link(collection)
return collection
def ensure_main_collections():
current = ensure_collection(MAIN_COLLECTION_NAME)
fixed = ensure_collection(FIXED_COLLECTION_NAME)
return current, fixed
def unlink_from_all_collections(obj):
for collection in list(obj.users_collection):
collection.objects.unlink(obj)
def link_to_collection(obj, collection):
if obj.name not in collection.objects:
collection.objects.link(obj)
def safe_remove_object(obj):
if obj is not None and obj.name in bpy.data.objects:
bpy.data.objects.remove(obj, do_unlink=True)
def unique_object_name(base_name):
if bpy.data.objects.get(base_name) is None:
return base_name
index = 1
while bpy.data.objects.get(f"{base_name}_{index:03d}") is not None:
index += 1
return f"{base_name}_{index:03d}"
def make_material_name(kind, session_id, axis=None):
if axis:
return f"{PREFIX_VAL}MAT_{kind}_{axis}_{session_id}"
return f"{PREFIX_VAL}MAT_{kind}_{session_id}"
def set_material_blend(mat):
try:
if hasattr(mat, "surface_render_method"):
mat.surface_render_method = 'DITHERED'
except Exception:
pass
try:
mat.use_transparency_overlap = False
except Exception:
pass
def create_two_sided_material(
name,
front_color,
front_alpha,
back_color,
back_alpha,
):
mat = bpy.data.materials.get(name)
if mat is None:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
set_material_blend(mat)
nodes = mat.node_tree.nodes
links = mat.node_tree.links
nodes.clear()
output = nodes.new("ShaderNodeOutputMaterial")
output.location = (500, 0)
bsdf = nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (220, 0)
geometry = nodes.new("ShaderNodeNewGeometry")
geometry.location = (-500, -180)
color_mix = nodes.new("ShaderNodeMixRGB")
color_mix.blend_type = 'MIX'
color_mix.location = (-180, 100)
alpha_mix = nodes.new("ShaderNodeMath")
alpha_mix.operation = 'MULTIPLY'
alpha_mix.location = (-180, -100)
color_mix.inputs[1].default_value = tuple(front_color[:4])
color_mix.inputs[2].default_value = tuple(back_color[:4])
links.new(geometry.outputs["Backfacing"], color_mix.inputs[0])
alpha_mix2 = nodes.new("ShaderNodeMix")
alpha_mix2.data_type = 'FLOAT'
alpha_mix2.location = (-20, -100)
alpha_mix2.inputs[2].default_value = float(front_alpha)
alpha_mix2.inputs[3].default_value = float(back_alpha)
links.new(geometry.outputs["Backfacing"], alpha_mix2.inputs[0])
links.new(color_mix.outputs["Color"], bsdf.inputs["Base Color"])
links.new(alpha_mix2.outputs[0], bsdf.inputs["Alpha"])
links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
return mat
def create_simple_material(name, color, alpha):
mat = bpy.data.materials.get(name)
if mat is None:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
set_material_blend(mat)
nodes = mat.node_tree.nodes
links = mat.node_tree.links
nodes.clear()
output = nodes.new("ShaderNodeOutputMaterial")
output.location = (300, 0)
bsdf = nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (0, 0)
bsdf.inputs["Base Color"].default_value = tuple(color[:4])
bsdf.inputs["Alpha"].default_value = max(0.0, min(1.0, float(alpha)))
links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
return mat
def clear_material_slots(obj):
obj.data.materials.clear()
def assign_single_material(obj, mat):
clear_material_slots(obj)
obj.data.materials.append(mat)
def make_material_independent(obj):
if obj is None or obj.data is None:
return
new_materials = []
for mat in obj.data.materials:
if mat is None:
new_materials.append(None)
continue
copied = mat.copy()
copied.name = unique_material_name(copied.name)
new_materials.append(copied)
obj.data.materials.clear()
for mat in new_materials:
if mat is not None:
obj.data.materials.append(mat)
def unique_material_name(base_name):
if bpy.data.materials.get(base_name) is None:
return base_name
index = 1
while bpy.data.materials.get(f"{base_name}_{index:03d}") is not None:
index += 1
return f"{base_name}_{index:03d}"
def create_cylinder_mesh(
radius,
length,
axis,
segments,
center,
):
mesh = bpy.data.meshes.new(unique_mesh_name(f"{PREFIX_VAL}MESH_{axis}"))
bm = bmesh.new()
bmesh.ops.create_cone(
bm,
cap_ends=True,
segments=max(8, int(segments)),
radius1=max(0.0001, float(radius)),
radius2=max(0.0001, float(radius)),
depth=max(0.0001, float(length)),
)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(unique_object_name(f"{PREFIX_VAL}TEMP"), mesh)
obj.location = center
if axis == X_AXIS:
obj.rotation_euler[1] = math.radians(90.0)
elif axis == Y_AXIS:
obj.rotation_euler[0] = math.radians(90.0)
return obj
def unique_mesh_name(base_name):
if bpy.data.meshes.get(base_name) is None:
return base_name
index = 1
while bpy.data.meshes.get(f"{base_name}_{index:03d}") is not None:
index += 1
return f"{base_name}_{index:03d}"
def create_disk_mesh(radius, axis, center, segments):
mesh = bpy.data.meshes.new(unique_mesh_name(f"{PREFIX_VAL}CAP_MESH_{axis}"))
bm = bmesh.new()
bmesh.ops.create_circle(
bm,
cap_ends=True,
segments=max(8, int(segments)),
radius=max(0.0001, float(radius)),
)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(unique_object_name(f"{PREFIX_VAL}CAP_TEMP"), mesh)
obj.location = center
if axis == X_AXIS:
obj.rotation_euler[1] = math.radians(90.0)
elif axis == Y_AXIS:
obj.rotation_euler[0] = math.radians(90.0)
return obj
def set_object_metadata(obj, session_id, role, axis=None):
obj["cyl_grid_floor_generated"] = True
obj["cyl_grid_floor_session"] = int(session_id)
obj["cyl_grid_floor_role"] = role
if axis:
obj["cyl_grid_floor_axis"] = axis
obj["cyl_grid_floor_completed"] = False
def get_current_objects():
current, _fixed = ensure_main_collections()
return [
obj for obj in current.objects
if obj.get("cyl_grid_floor_generated", False)
and not obj.get("cyl_grid_floor_completed", False)
]
def get_completed_objects():
_current, fixed = ensure_main_collections()
return [
obj for obj in fixed.objects
if obj.get("cyl_grid_floor_generated", False)
and obj.get("cyl_grid_floor_completed", False)
]
def delete_current_objects():
for obj in list(get_current_objects()):
safe_remove_object(obj)
def assign_material_to_new_object(obj, mat):
if obj is None:
return
assign_single_material(obj, mat)
def get_or_replace_mesh_object(
name, mesh_obj, collection, session_id, role, axis=None, material=None
):
old = bpy.data.objects.get(name)
if old is not None:
safe_remove_object(old)
mesh_obj.name = unique_object_name(name)
mesh_obj.data.name = unique_mesh_name(f"{name}_MESH")
unlink_from_all_collections(mesh_obj)
collection.objects.link(mesh_obj)
set_object_metadata(mesh_obj, session_id, role, axis)
if material:
assign_material_to_new_object(mesh_obj, material)
return mesh_obj
def build_wall(props, session_id, axis, index, x, y, z, collection):
if axis == X_AXIS:
radius = props.x_radius
length = props.x_length
color = props.x_color
alpha = props.x_alpha
back_color = props.x_back_color
back_alpha = props.x_back_alpha
else:
radius = props.y_radius
length = props.y_length
color = props.y_color
alpha = props.y_alpha
back_color = props.y_back_color
back_alpha = props.y_back_alpha
center = Vector((x, y, z))
mesh_obj = create_cylinder_mesh(
radius=radius, length=length, axis=axis, segments=props.segments, center=center
)
name = f"{CURRENT_WALL_PREFIX}{axis}_{session_id:04d}_{index:03d}"
material_name = make_material_name("WALL", session_id, axis)
mat = create_two_sided_material(
material_name, color, alpha, back_color, back_alpha
)
return get_or_replace_mesh_object(
name, mesh_obj, collection, session_id, "WALL", axis, mat
)
def build_cap(props, session_id, axis, index, x, y, z, collection, top=True):
radius = props.x_radius if axis == X_AXIS else props.y_radius
length = props.x_length if axis == X_AXIS else props.y_length
half_length = length * 0.5
if top:
z_offset = half_length
role = "TOP_CAP"
color = props.top_color
alpha = props.top_alpha
prefix = CURRENT_TOP_PREFIX
else:
z_offset = -half_length
role = "BOTTOM_CAP"
color = props.bottom_color
alpha = props.bottom_alpha
prefix = CURRENT_BOTTOM_PREFIX
if axis == X_AXIS:
cap_center = Vector((x + z_offset, y, z))
else:
cap_center = Vector((x, y + z_offset, z))
mesh_obj = create_disk_mesh(
radius=radius, axis=axis, center=cap_center, segments=props.segments
)
name = f"{prefix}{axis}_{session_id:04d}_{index:03d}"
material_name = make_material_name("TOP" if top else "BOTTOM", session_id, axis)
mat = create_simple_material(material_name, color, alpha)
return get_or_replace_mesh_object(
name, mesh_obj, collection, session_id, role, axis, mat
)
def rebuild_scene(props):
delete_current_objects()
current, _fixed = ensure_main_collections()
session_id = int(props.session_id)
# X方向
x_count = max(1, int(props.x_count))
for i in range(x_count):
y = props.center_y + (i - (x_count - 1) * 0.5) * props.x_spacing
build_wall(props, session_id, X_AXIS, i, props.center_x, y, props.center_z, current)
if props.create_top_cap:
build_cap(props, session_id, X_AXIS, i, props.center_x, y, props.center_z, current, top=True)
if props.create_bottom_cap:
build_cap(props, session_id, X_AXIS, i, props.center_x, y, props.center_z, current, top=False)
# Y方向
y_count = max(1, int(props.y_count))
for i in range(y_count):
x = props.center_x + (i - (y_count - 1) * 0.5) * props.y_spacing
build_wall(props, session_id, Y_AXIS, i, x, props.center_y, props.center_z, current)
if props.create_top_cap:
build_cap(props, session_id, Y_AXIS, i, x, props.center_y, props.center_z, current, top=True)
if props.create_bottom_cap:
build_cap(props, session_id, Y_AXIS, i, x, props.center_y, props.center_z, current, top=False)
def schedule_rebuild(self, context):
if getattr(self, "_updating", False):
return
self._updating = True
try:
if not self.detached:
rebuild_scene(self)
finally:
self._updating = False
def update_property(self, context):
schedule_rebuild(self, context)
# ============================================================
# PropertyGroup
# ============================================================
class CYLGRID_PG_settings(bpy.types.PropertyGroup):
session_id: bpy.props.IntProperty(name="Object番号", default=1, min=1)
detached: bpy.props.BoolProperty(name="切離し済み", default=False)
segments: bpy.props.IntProperty(name="分割数", default=DEFAULT_SEGMENTS, min=8, max=256, update=update_property)
x_count: bpy.props.IntProperty(name="本数", default=DEFAULT_X_COUNT, min=1, max=200, update=update_property)
x_radius: bpy.props.FloatProperty(name="太さ", default=DEFAULT_X_RADIUS, min=0.001, soft_max=10.0, precision=4, update=update_property)
x_spacing: bpy.props.FloatProperty(name="間隔", default=DEFAULT_X_SPACING, min=0.001, soft_max=100.0, precision=4, update=update_property)
x_length: bpy.props.FloatProperty(name="長さ", default=DEFAULT_X_LENGTH, min=0.001, soft_max=1000.0, precision=4, update=update_property)
x_color: bpy.props.FloatVectorProperty(name="表", subtype='COLOR', size=4, default=DEFAULT_X_COLOR, min=0.0, max=1.0, update=update_property)
x_alpha: bpy.props.FloatProperty(name="表透明度", default=DEFAULT_X_ALPHA, min=0.0, max=1.0, subtype='FACTOR', update=update_property)
x_back_color: bpy.props.FloatVectorProperty(name="裏", subtype='COLOR', size=4, default=DEFAULT_X_BACK_COLOR, min=0.0, max=1.0, update=update_property)
x_back_alpha: bpy.props.FloatProperty(name="裏透明度", default=DEFAULT_X_BACK_ALPHA, min=0.0, max=1.0, subtype='FACTOR', update=update_property)
y_count: bpy.props.IntProperty(name="本数", default=DEFAULT_Y_COUNT, min=1, max=200, update=update_property)
y_radius: bpy.props.FloatProperty(name="太さ", default=DEFAULT_Y_RADIUS, min=0.001, soft_max=10.0, precision=4, update=update_property)
y_spacing: bpy.props.FloatProperty(name="間隔", default=DEFAULT_Y_SPACING, min=0.001, soft_max=100.0, precision=4, update=update_property)
y_length: bpy.props.FloatProperty(name="長さ", default=DEFAULT_Y_LENGTH, min=0.001, soft_max=1000.0, precision=4, update=update_property)
y_color: bpy.props.FloatVectorProperty(name="表", subtype='COLOR', size=4, default=DEFAULT_Y_COLOR, min=0.0, max=1.0, update=update_property)
y_alpha: bpy.props.FloatProperty(name="表透明度", default=DEFAULT_Y_ALPHA, min=0.0, max=1.0, subtype='FACTOR', update=update_property)
y_back_color: bpy.props.FloatVectorProperty(name="裏", subtype='COLOR', size=4, default=DEFAULT_Y_BACK_COLOR, min=0.0, max=1.0, update=update_property)
y_back_alpha: bpy.props.FloatProperty(name="裏透明度", default=DEFAULT_Y_BACK_ALPHA, min=0.0, max=1.0, subtype='FACTOR', update=update_property)
create_top_cap: bpy.props.BoolProperty(name="天井蓋を作る", default=DEFAULT_CREATE_TOP_CAP, update=update_property)
top_color: bpy.props.FloatVectorProperty(name="天井蓋色", subtype='COLOR', size=4, default=DEFAULT_TOP_COLOR, min=0.0, max=1.0, update=update_property)
top_alpha: bpy.props.FloatProperty(name="天井蓋透明度", default=DEFAULT_TOP_ALPHA, min=0.0, max=1.0, subtype='FACTOR', update=update_property)
create_bottom_cap: bpy.props.BoolProperty(name="床面蓋を作る", default=DEFAULT_CREATE_BOTTOM_CAP, update=update_property)
bottom_color: bpy.props.FloatVectorProperty(name="床面蓋色", subtype='COLOR', size=4, default=DEFAULT_BOTTOM_COLOR, min=0.0, max=1.0, update=update_property)
bottom_alpha: bpy.props.FloatProperty(name="床面蓋透明度", default=DEFAULT_BOTTOM_ALPHA, min=0.0, max=1.0, subtype='FACTOR', update=update_property)
center_x: bpy.props.FloatProperty(name="X", default=DEFAULT_CENTER_X, precision=4, update=update_property)
center_y: bpy.props.FloatProperty(name="Y", default=DEFAULT_CENTER_Y, precision=4, update=update_property)
center_z: bpy.props.FloatProperty(name="Z", default=DEFAULT_CENTER_Z, precision=4, update=update_property)
# ============================================================
# Operators
# ============================================================
class CYLGRID_OT_create(bpy.types.Operator):
bl_idname = "cyl_grid.create"
bl_label = "作成 / 更新"
bl_description = "現在の設定で円柱グリッドを作成・更新します"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.cyl_grid_settings
if props.detached:
self.report({'WARNING'}, "現在のObjectは切離し済みです。「次のObjectを作成」を使用してください。")
return {'CANCELLED'}
rebuild_scene(props)
self.report({'INFO'}, "円柱グリッドを作成 / 更新しました。")
return {'FINISHED'}
class CYLGRID_OT_detach(bpy.types.Operator):
bl_idname = "cyl_grid.detach"
bl_label = "完成Objectとして切離し"
bl_description = "現在の生成物を完成Objectとして切離し、以後の更新対象から外します"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.cyl_grid_settings
current, fixed = ensure_main_collections()
current_objects = get_current_objects()
if not current_objects:
self.report({'WARNING'}, "切離す対象の生成Objectがありません。")
return {'CANCELLED'}
for obj in current_objects:
make_material_independent(obj)
obj["cyl_grid_floor_completed"] = True
obj["cyl_grid_floor_fixed"] = True
unlink_from_all_collections(obj)
fixed.objects.link(obj)
props.detached = True
self.report({'INFO'}, f"{len(current_objects)}個のObjectを完成Objectとして切離しました。")
return {'FINISHED'}
class CYLGRID_OT_next_object(bpy.types.Operator):
bl_idname = "cyl_grid.next_object"
bl_label = "次のObjectを作成"
bl_description = "完成済みObjectを保持したまま、新しい生成Objectを作成します"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.cyl_grid_settings
props.session_id += 1
props.detached = False
rebuild_scene(props)
self.report({'INFO'}, f"Object {props.session_id} の新規生成を開始しました。")
return {'FINISHED'}
class CYLGRID_OT_confirm_join(bpy.types.Operator):
bl_idname = "cyl_grid.confirm_join"
bl_label = "完成品を一体化するか確認"
bl_description = "完成Objectだけを対象として、一体化するか確認します"
def invoke(self, context, event):
completed = get_completed_objects()
if len(completed) <= 1:
self.report({'INFO'}, "完成Objectが1個以下のため、一体化する必要はありません。")
return {'CANCELLED'}
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
completed = get_completed_objects()
layout.label(text=f"完成Object {len(completed)}個を一体化しますか?", icon='QUESTION')
layout.separator()
layout.label(text="対象は完成Objectのみです。")
layout.label(text="未完成Objectは対象外です。")
layout.separator()
layout.label(text="「一体化する」でJoinを実行します。")
def execute(self, context):
return bpy.ops.cyl_grid.join_completed('INVOKE_DEFAULT')
class CYLGRID_OT_join_completed(bpy.types.Operator):
bl_idname = "cyl_grid.join_completed"
bl_label = "一体化する"
bl_description = "完成ObjectだけをJoinします"
bl_options = {'REGISTER', 'UNDO'}
def invoke(self, context, event):
completed = get_completed_objects()
if len(completed) <= 1:
self.report({'INFO'}, "完成Objectが1個以下のため、Joinは実行しません。")
return {'CANCELLED'}
return context.window_manager.invoke_confirm(self, event)
def execute(self, context):
completed = get_completed_objects()
if len(completed) <= 1:
self.report({'INFO'}, "完成Objectが1個以下のため、Joinは実行しません。")
return {'CANCELLED'}
bpy.ops.object.select_all(action='DESELECT')
for obj in completed:
if obj.name in bpy.context.view_layer.objects:
obj.select_set(True)
active = completed[0]
context.view_layer.objects.active = active
try:
bpy.ops.object.join()
except Exception as exc:
self.report({'ERROR'}, f"Joinに失敗しました: {exc}")
return {'CANCELLED'}
active["cyl_grid_floor_completed"] = True
active["cyl_grid_floor_joined"] = True
self.report({'INFO'}, f"{len(completed)}個の完成Objectを一体化しました。")
return {'FINISHED'}
class CYLGRID_OT_copy_position(bpy.types.Operator):
bl_idname = "cyl_grid.copy_position"
bl_label = "位置情報をコピー"
bl_description = "現在の位置・配置情報をクリップボードへコピーします"
def execute(self, context):
props = context.scene.cyl_grid_settings
x_span = max(0, props.x_count - 1) * props.x_spacing
y_span = max(0, props.y_count - 1) * props.y_spacing
text = (
"Cylinder Grid Floor\n"
f"Object番号: {props.session_id}\n"
f"中心 X: {props.center_x:.4f}\n"
f"中心 Y: {props.center_y:.4f}\n"
f"中心 Z: {props.center_z:.4f}\n"
f"X方向本数: {props.x_count}\n"
f"X方向間隔: {props.x_spacing:.4f}\n"
f"X方向配置幅: {x_span:.4f}\n"
f"X方向長さ: {props.x_length:.4f}\n"
f"Y方向本数: {props.y_count}\n"
f"Y方向間隔: {props.y_spacing:.4f}\n"
f"Y方向配置幅: {y_span:.4f}\n"
f"Y方向長さ: {props.y_length:.4f}\n"
f"天井蓋: {'ON' if props.create_top_cap else 'OFF'}\n"
f"床面蓋: {'ON' if props.create_bottom_cap else 'OFF'}\n"
)
context.window_manager.clipboard = text
self.report({'INFO'}, "位置情報をクリップボードへコピーしました。")
return {'FINISHED'}
class CYLGRID_OT_copy_code(bpy.types.Operator):
bl_idname = "cyl_grid.copy_code"
bl_label = "設定をコード形式でコピー"
bl_description = "現在の数値をPythonの初期値コードとしてクリップボードへコピーします"
def execute(self, context):
props = context.scene.cyl_grid_settings
def fmt_c(c):
return f"({c[0]:.2f}, {c[1]:.2f}, {c[2]:.2f}, 1.0)"
lines = [
"# ============================================================",
"# 初期値 (ここをコピーしたコードで書き換え可能)",
"# ============================================================",
f"DEFAULT_X_COUNT = {props.x_count}",
f"DEFAULT_Y_COUNT = {props.y_count}",
"",
f"DEFAULT_X_RADIUS = {props.x_radius:.4f}",
f"DEFAULT_Y_RADIUS = {props.y_radius:.4f}",
"",
f"DEFAULT_X_SPACING = {props.x_spacing:.4f}",
f"DEFAULT_Y_SPACING = {props.y_spacing:.4f}",
"",
f"DEFAULT_X_LENGTH = {props.x_length:.4f}",
f"DEFAULT_Y_LENGTH = {props.y_length:.4f}",
"",
f"DEFAULT_X_COLOR = {fmt_c(props.x_color)}",
f"DEFAULT_X_ALPHA = {props.x_alpha:.2f}",
f"DEFAULT_X_BACK_COLOR = {fmt_c(props.x_back_color)}",
f"DEFAULT_X_BACK_ALPHA = {props.x_back_alpha:.2f}",
"",
f"DEFAULT_Y_COLOR = {fmt_c(props.y_color)}",
f"DEFAULT_Y_ALPHA = {props.y_alpha:.2f}",
f"DEFAULT_Y_BACK_COLOR = {fmt_c(props.y_back_color)}",
f"DEFAULT_Y_BACK_ALPHA = {props.y_back_alpha:.2f}",
"",
f"DEFAULT_TOP_COLOR = {fmt_c(props.top_color)}",
f"DEFAULT_TOP_ALPHA = {props.top_alpha:.2f}",
"",
f"DEFAULT_BOTTOM_COLOR = {fmt_c(props.bottom_color)}",
f"DEFAULT_BOTTOM_ALPHA = {props.bottom_alpha:.2f}",
"",
f"DEFAULT_SEGMENTS = {props.segments}",
"",
f"DEFAULT_CREATE_TOP_CAP = {props.create_top_cap}",
f"DEFAULT_CREATE_BOTTOM_CAP = {props.create_bottom_cap}",
"",
f"DEFAULT_CENTER_X = {props.center_x:.4f}",
f"DEFAULT_CENTER_Y = {props.center_y:.4f}",
f"DEFAULT_CENTER_Z = {props.center_z:.4f}",
]
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "現在の数値をコード形式でコピーしました")
return {'FINISHED'}
class CYLGRID_OT_reset_settings(bpy.types.Operator):
bl_idname = "cyl_grid.reset_settings"
bl_label = "設定を初期値へ戻す"
bl_description = "現在の生成設定だけを初期値へ戻します"
def execute(self, context):
props = context.scene.cyl_grid_settings
props.x_count = DEFAULT_X_COUNT
props.y_count = DEFAULT_Y_COUNT
props.x_radius = DEFAULT_X_RADIUS
props.y_radius = DEFAULT_Y_RADIUS
props.x_spacing = DEFAULT_X_SPACING
props.y_spacing = DEFAULT_Y_SPACING
props.x_length = DEFAULT_X_LENGTH
props.y_length = DEFAULT_Y_LENGTH
props.x_color = DEFAULT_X_COLOR
props.x_alpha = DEFAULT_X_ALPHA
props.x_back_color = DEFAULT_X_BACK_COLOR
props.x_back_alpha = DEFAULT_X_BACK_ALPHA
props.y_color = DEFAULT_Y_COLOR
props.y_alpha = DEFAULT_Y_ALPHA
props.y_back_color = DEFAULT_Y_BACK_COLOR
props.y_back_alpha = DEFAULT_Y_BACK_ALPHA
props.create_top_cap = DEFAULT_CREATE_TOP_CAP
props.top_color = DEFAULT_TOP_COLOR
props.top_alpha = DEFAULT_TOP_ALPHA
props.create_bottom_cap = DEFAULT_CREATE_BOTTOM_CAP
props.bottom_color = DEFAULT_BOTTOM_COLOR
props.bottom_alpha = DEFAULT_BOTTOM_ALPHA
props.center_x = DEFAULT_CENTER_X
props.center_y = DEFAULT_CENTER_Y
props.center_z = DEFAULT_CENTER_Z
props.segments = DEFAULT_SEGMENTS
self.report({'INFO'}, "設定を初期値へ戻しました。")
return {'FINISHED'}
class CYLGRID_OT_remove_addon(bpy.types.Operator):
bl_idname = "wm.cyl_grid_remove_addon"
bl_label = "アドオン削除"
def execute(self, context):
unregister()
return {'FINISHED'}
# ============================================================
# Panels
# ============================================================
class CYLGRID_PT_main(bpy.types.Panel):
bl_label = "Cylinder Grid Floor"
bl_idname = "CYLGRID_PT_main"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = PANEL_CATEGORY
bl_order = 1
def draw(self, context):
layout = self.layout
props = context.scene.cyl_grid_settings
if props.detached:
box = layout.box()
box.label(text="完成Object", icon='CHECKMARK')
box.label(text="アドオン制御から切離し済み")
box.label(text="Materialも完全独立済み")
else:
box = layout.box()
box.label(text=f"生成中 Object {props.session_id}", icon='MESH_CYLINDER')
box.label(text="リアルタイム更新中")
box_x = layout.box()
box_x.label(text="X方向円柱", icon='AXIS_TOP')
row = box_x.row()
row.prop(props, "x_count", text="本数")
row = box_x.row()
row.prop(props, "x_radius", text="太さ")
row = box_x.row()
row.prop(props, "x_spacing", text="間隔")
row = box_x.row()
row.prop(props, "x_length", text="長さ")
row = box_x.row(align=True)
row.label(text="表")
row.prop(props, "x_color", text="")
row.prop(props, "x_alpha", text="透明度", slider=True)
row = box_x.row(align=True)
row.label(text="裏")
row.prop(props, "x_back_color", text="")
row.prop(props, "x_back_alpha", text="透明度", slider=True)
box_y = layout.box()
box_y.label(text="Y方向円柱", icon='AXIS_FRONT')
row = box_y.row()
row.prop(props, "y_count", text="本数")
row = box_y.row()
row.prop(props, "y_radius", text="太さ")
row = box_y.row()
row.prop(props, "y_spacing", text="間隔")
row = box_y.row()
row.prop(props, "y_length", text="長さ")
row = box_y.row(align=True)
row.label(text="表")
row.prop(props, "y_color", text="")
row.prop(props, "y_alpha", text="透明度", slider=True)
row = box_y.row(align=True)
row.label(text="裏")
row.prop(props, "y_back_color", text="")
row.prop(props, "y_back_alpha", text="透明度", slider=True)
box_mat = layout.box()
box_mat.label(text="蓋・マテリアル設定(完全独立)", icon='MATERIAL')
box_mat.prop(props, "create_top_cap", text="天井蓋を作る / 削除", toggle=True)
if props.create_top_cap:
row = box_mat.row(align=True)
row.label(text="天井蓋")
row.prop(props, "top_color", text="")
row.prop(props, "top_alpha", text="透明度", slider=True)
box_mat.separator()
box_mat.prop(props, "create_bottom_cap", text="床面蓋を作る / 削除", toggle=True)
if props.create_bottom_cap:
row = box_mat.row(align=True)
row.label(text="床面蓋")
row.prop(props, "bottom_color", text="")
row.prop(props, "bottom_alpha", text="透明度", slider=True)
box_common = layout.box()
box_common.label(text="共通設定", icon='SETTINGS')
box_common.prop(props, "segments", text="分割数")
box_pos = layout.box()
box_pos.label(text="位置情報", icon='EMPTY_AXIS')
row = box_pos.row()
row.prop(props, "center_x", text="X")
row = box_pos.row()
row.prop(props, "center_y", text="Y")
row = box_pos.row()
row.prop(props, "center_z", text="Z")
x_span = max(0, props.x_count - 1) * props.x_spacing
y_span = max(0, props.y_count - 1) * props.y_spacing
box_pos.label(text=f"X方向配置幅: {x_span:.4f}")
box_pos.label(text=f"Y方向配置幅: {y_span:.4f}")
box_pos.operator("cyl_grid.copy_position", text="位置情報をコピー", icon='COPYDOWN')
box_action = layout.box()
box_action.label(text="生成・完成Object管理", icon='OBJECT_DATA')
if not props.detached:
box_action.operator("cyl_grid.create", text="作成 / 更新", icon='MESH_CYLINDER')
box_action.operator("cyl_grid.detach", text="完成Objectとして切離し", icon='UNLINKED')
else:
box_action.label(text="完成品は以後の設定変更から保護されています。", icon='LOCKED')
box_action.operator("cyl_grid.next_object", text="次のObjectを作成", icon='ADD')
completed_count = len(get_completed_objects())
box_join = layout.box()
box_join.label(text="完成Objectの一体化", icon='AUTOMERGE_ON')
box_join.label(text=f"完成Object数: {completed_count}")
box_join.operator("cyl_grid.confirm_join", text="完成品を一体化するか確認", icon='AUTOMERGE_ON')
layout.separator()
layout.operator("cyl_grid.reset_settings", text="現在設定を初期値へ戻す", icon='LOOP_BACK')
# コードコピー
box_out = layout.box()
box_out.label(text="現在の数値を初期値に", icon='TEXT')
box_out.operator("cyl_grid.copy_code", text="コード形式でコピー", icon='COPYDOWN')
class CYLGRID_PT_links(bpy.types.Panel):
bl_label = "Links"
bl_idname = "CYLGRID_PT_links"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = PANEL_CATEGORY
bl_order = 998
def draw(self, context):
layout = self.layout
layout.label(text="Cylinder Grid Floor", icon='URL')
layout.label(text=f"Created: {CREATED_DATE}")
layout.label(text=f"Updated: {UPDATED_DATE}")
layout.label(text="Blender 5.0+ 専用")
class CYLGRID_PT_remove(bpy.types.Panel):
bl_label = "Addon Remove"
bl_idname = "CYLGRID_PT_remove"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = PANEL_CATEGORY
bl_order = 999
def draw(self, context):
layout = self.layout
layout.operator("wm.cyl_grid_remove_addon", icon='CANCEL')
# ============================================================
# register / unregister
# ============================================================
classes = (
CYLGRID_PG_settings,
CYLGRID_OT_create,
CYLGRID_OT_detach,
CYLGRID_OT_next_object,
CYLGRID_OT_confirm_join,
CYLGRID_OT_join_completed,
CYLGRID_OT_copy_position,
CYLGRID_OT_copy_code,
CYLGRID_OT_reset_settings,
CYLGRID_OT_remove_addon,
CYLGRID_PT_main,
CYLGRID_PT_links,
CYLGRID_PT_remove,
)
def set_material_preview():
try:
for window in bpy.context.window_manager.windows:
screen = window.screen
for area in screen.areas:
if area.type == 'VIEW_3D':
space = area.spaces.active
if hasattr(space, "shading"):
space.shading.type = 'MATERIAL'
except Exception:
pass
def open_n_panel():
try:
for window in bpy.context.window_manager.windows:
screen = window.screen
for area in screen.areas:
if area.type == 'VIEW_3D':
for region in area.regions:
if region.type == 'WINDOW':
with bpy.context.temp_override(window=window, area=area, region=region):
try:
bpy.ops.view3d.sidebar('INVOKE_DEFAULT')
except Exception:
pass
return
except Exception:
pass
def startup_ui():
set_material_preview()
try:
open_n_panel()
except Exception:
pass
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.cyl_grid_settings = bpy.props.PointerProperty(
type=CYLGRID_PG_settings,
)
ensure_main_collections()
props = bpy.context.scene.cyl_grid_settings
try:
rebuild_scene(props)
except Exception:
pass
try:
bpy.app.timers.register(startup_ui, first_interval=0.5)
except Exception:
pass
def unregister():
if hasattr(bpy.types.Scene, "cyl_grid_settings"):
del bpy.types.Scene.cyl_grid_settings
for cls in reversed(classes):
try:
bpy.utils.unregister_class(cls)
except Exception:
pass
# ============================================================
# Direct execution
# ============================================================
if __name__ == "__main__":
try:
unregister()
except Exception:
pass
register()