
# ================================================================
# Blender 5.0+
# Cylinder Grid Floor Replacement
#
# Floorの代わりに、X方向・Y方向の円柱を直交させて
# 格子状の床を生成するアドオン
#
# deepdeep design:
# 生成
# ↓
# リアルタイム更新
# ↓
# 完成Objectとして切離し
# ↓
# 完成品を固定
# ↓
# 次のObject
# ↓
# 必要なら一体化
# ================================================================
bl_info = {
"name": "Cylinder Grid Floor",
"author": "zionad",
"description": "Floor replacement using perpendicular X/Y cylinder grids",
"version": (1, 0, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar > Cylinder Grid Floor",
"category": "3D View",
}
# ================================================================
# 1. 作成日・更新日
# ================================================================
CREATED_DATE = "2026-10-05"
UPDATED_DATE = "2026-10-05"
# ================================================================
# 2. Imports
# ================================================================
import bpy
import math
from mathutils import Vector
from bpy.props import (
IntProperty,
FloatProperty,
FloatVectorProperty,
BoolProperty,
StringProperty,
)
# ================================================================
# 3. 共通定義
# ================================================================
# アドオンの目的から自動設計したPrefix
PREFIX_VAL = "CYL_GRID_"
MAIN_COLLECTION_NAME = "CylinderGrid_Floor"
FIXED_COLLECTION_NAME = "CylinderGrid_Fixed"
CURRENT_SET_PROPERTY = "cyl_grid_current_set"
COMPLETED_PROPERTY = "cyl_grid_completed"
UPDATE_GUARD = False
# ================================================================
# 4. 初期パラメータ
# ================================================================
# ------------------------------------------------
# X方向
# ------------------------------------------------
DEFAULT_X_COUNT = 11
DEFAULT_X_RADIUS = 0.05
DEFAULT_X_LENGTH = 10.0
DEFAULT_X_COLOR = (0.15, 0.45, 1.0, 1.0)
DEFAULT_X_ALPHA = 1.0
# ------------------------------------------------
# Y方向
# ------------------------------------------------
DEFAULT_Y_COUNT = 11
DEFAULT_Y_RADIUS = 0.05
DEFAULT_Y_LENGTH = 10.0
DEFAULT_Y_COLOR = (1.0, 0.25, 0.15, 1.0)
DEFAULT_Y_ALPHA = 1.0
# ------------------------------------------------
# 円柱生成
# ------------------------------------------------
CYLINDER_VERTICES = 32
# ================================================================
# 5. Collection Utility
# ================================================================
def get_or_create_collection(name, parent=None):
"""
同名Collectionを重複作成しない。
"""
collection = bpy.data.collections.get(name)
if collection is None:
collection = bpy.data.collections.new(name)
if parent is None:
bpy.context.scene.collection.children.link(collection)
else:
parent.children.link(collection)
return collection
def get_main_collection():
return get_or_create_collection(MAIN_COLLECTION_NAME)
def get_fixed_collection():
return get_or_create_collection(FIXED_COLLECTION_NAME)
def move_object_to_collection(obj, target_collection):
"""
Objectを指定Collectionへ移動。
"""
for collection in list(obj.users_collection):
collection.objects.unlink(obj)
target_collection.objects.link(obj)
# ================================================================
# 6. Name Utility
# ================================================================
def make_name(kind, index):
return f"{PREFIX_VAL}{kind}_{index:03d}"
# ================================================================
# 7. Material Utility
# ================================================================
def create_grid_material(name, color, alpha):
"""
円柱専用Materialを作成。
"""
material = bpy.data.materials.new(name=name)
material.use_nodes = True
nodes = material.node_tree.nodes
bsdf = nodes.get("Principled BSDF")
if bsdf:
rgba = (
color[0],
color[1],
color[2],
alpha,
)
bsdf.inputs["Base Color"].default_value = rgba
bsdf.inputs["Alpha"].default_value = alpha
material.diffuse_color = (
color[0],
color[1],
color[2],
alpha,
)
# Blender 5系で使用可能な場合のみ設定
if hasattr(material, "surface_render_method"):
if alpha < 0.999:
try:
material.surface_render_method = 'DITHERED'
except Exception:
pass
return material
def update_material(material, color, alpha):
"""
既存Materialを更新。
"""
if material is None:
return
material.diffuse_color = (
color[0],
color[1],
color[2],
alpha,
)
if material.use_nodes:
bsdf = material.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = (
color[0],
color[1],
color[2],
alpha,
)
bsdf.inputs["Alpha"].default_value = alpha
if hasattr(material, "surface_render_method"):
if alpha < 0.999:
try:
material.surface_render_method = 'DITHERED'
except Exception:
pass
def make_independent_material(obj):
"""
完成Objectとして切離す際、
MaterialをObject専用としてコピーする。
"""
if obj.type != 'MESH':
return
for slot in obj.material_slots:
if slot.material:
slot.material = slot.material.copy()
# ================================================================
# 8. Cylinder Creation
# ================================================================
def create_cylinder_object(
name,
radius,
length,
location,
rotation,
material,
):
"""
円柱を1本生成する。
"""
bpy.ops.mesh.primitive_cylinder_add(
vertices=CYLINDER_VERTICES,
radius=radius,
depth=length,
location=location,
rotation=rotation,
)
obj = bpy.context.object
obj.name = name
obj.data.name = f"{name}_Mesh"
if material:
obj.data.materials.append(material)
return obj
# ================================================================
# 9. Current Object Management
# ================================================================
def get_current_objects():
"""
現在リアルタイム制御対象のObjectを取得。
"""
result = []
for obj in bpy.data.objects:
if obj.get(CURRENT_SET_PROPERTY, False):
result.append(obj)
return result
def clear_current_objects():
"""
現在の生成対象だけを削除。
完成済みObjectは削除しない。
"""
for obj in list(get_current_objects()):
if obj.name in bpy.data.objects:
bpy.data.objects.remove(obj, do_unlink=True)
def mark_current_object(obj):
obj[CURRENT_SET_PROPERTY] = True
obj[COMPLETED_PROPERTY] = False
def mark_completed_object(obj):
obj[CURRENT_SET_PROPERTY] = False
obj[COMPLETED_PROPERTY] = True
# ================================================================
# 10. Grid Generation
# ================================================================
def generate_grid(settings):
"""
X方向円柱 + Y方向円柱による格子を生成。
X方向円柱:
X軸に沿って配置
Y方向へ均等配置
Y方向円柱:
Y軸に沿って配置
X方向へ均等配置
"""
global UPDATE_GUARD
if UPDATE_GUARD:
return
UPDATE_GUARD = True
try:
clear_current_objects()
main_collection = get_main_collection()
# --------------------------------------------------------
# Material
# --------------------------------------------------------
x_material = create_grid_material(
f"{PREFIX_VAL}Material_X",
settings.x_color,
settings.x_alpha,
)
y_material = create_grid_material(
f"{PREFIX_VAL}Material_Y",
settings.y_color,
settings.y_alpha,
)
# --------------------------------------------------------
# X方向
# --------------------------------------------------------
x_count = max(1, settings.x_count)
for i in range(x_count):
if x_count == 1:
y = 0.0
else:
t = i / (x_count - 1)
y = -settings.y_length / 2.0 + (
settings.y_length * t
)
obj = create_cylinder_object(
name=make_name("X", i + 1),
radius=settings.x_radius,
length=settings.x_length,
location=(0.0, y, 0.0),
rotation=(0.0, math.pi / 2.0, 0.0),
material=x_material,
)
move_object_to_collection(obj, main_collection)
mark_current_object(obj)
# --------------------------------------------------------
# Y方向
# --------------------------------------------------------
y_count = max(1, settings.y_count)
for i in range(y_count):
if y_count == 1:
x = 0.0
else:
t = i / (y_count - 1)
x = -settings.x_length / 2.0 + (
settings.x_length * t
)
obj = create_cylinder_object(
name=make_name("Y", i + 1),
radius=settings.y_radius,
length=settings.y_length,
location=(x, 0.0, 0.0),
rotation=(math.pi / 2.0, 0.0, 0.0),
material=y_material,
)
move_object_to_collection(obj, main_collection)
mark_current_object(obj)
finally:
UPDATE_GUARD = False
# ================================================================
# 11. Update Callback
# ================================================================
def update_grid(self, context):
if UPDATE_GUARD:
return
try:
generate_grid(self)
except Exception as exc:
print(
f"[Cylinder Grid Floor] Update Error: {exc}"
)
# ================================================================
# 12. Property Group
# ================================================================
class CYLGRID_PG_Settings(bpy.types.PropertyGroup):
# ============================================================
# X方向
# ============================================================
x_count: IntProperty(
name="本数",
description="X方向に並べる円柱の本数",
default=DEFAULT_X_COUNT,
min=1,
max=500,
update=update_grid,
)
x_radius: FloatProperty(
name="太さ",
description="X方向円柱の半径",
default=DEFAULT_X_RADIUS,
min=0.001,
max=100.0,
precision=4,
update=update_grid,
)
x_length: FloatProperty(
name="長さ",
description="X方向円柱の長さ",
default=DEFAULT_X_LENGTH,
min=0.001,
max=1000.0,
precision=3,
update=update_grid,
)
x_color: FloatVectorProperty(
name="色",
subtype='COLOR',
size=4,
default=DEFAULT_X_COLOR,
min=0.0,
max=1.0,
update=update_grid,
)
x_alpha: FloatProperty(
name="透明度",
description="X方向円柱の透明度",
default=DEFAULT_X_ALPHA,
min=0.0,
max=1.0,
subtype='FACTOR',
update=update_grid,
)
# ============================================================
# Y方向
# ============================================================
y_count: IntProperty(
name="本数",
description="Y方向に並べる円柱の本数",
default=DEFAULT_Y_COUNT,
min=1,
max=500,
update=update_grid,
)
y_radius: FloatProperty(
name="太さ",
description="Y方向円柱の半径",
default=DEFAULT_Y_RADIUS,
min=0.001,
max=100.0,
precision=4,
update=update_grid,
)
y_length: FloatProperty(
name="長さ",
description="Y方向円柱の長さ",
default=DEFAULT_Y_LENGTH,
min=0.001,
max=1000.0,
precision=3,
update=update_grid,
)
y_color: FloatVectorProperty(
name="色",
subtype='COLOR',
size=4,
default=DEFAULT_Y_COLOR,
min=0.0,
max=1.0,
update=update_grid,
)
y_alpha: FloatProperty(
name="透明度",
description="Y方向円柱の透明度",
default=DEFAULT_Y_ALPHA,
min=0.0,
max=1.0,
subtype='FACTOR',
update=update_grid,
)
# ================================================================
# 13. Operator
# ================================================================
class CYLGRID_OT_create(bpy.types.Operator):
bl_idname = "cylgrid.create"
bl_label = "格子を生成 / 更新"
bl_description = "X方向・Y方向の円柱格子を生成または更新"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
settings = context.scene.cyl_grid_settings
generate_grid(settings)
self.report(
{'INFO'},
"円柱格子を生成しました"
)
return {'FINISHED'}
# ================================================================
# 14. Next Object
# ================================================================
class CYLGRID_OT_next_object(bpy.types.Operator):
bl_idname = "cylgrid.next_object"
bl_label = "次のObjectを作成"
bl_description = "現在の生成物を完成品として残し、次のObjectを作成"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
current_objects = get_current_objects()
if not current_objects:
self.report(
{'WARNING'},
"現在の生成Objectがありません"
)
return {'CANCELLED'}
# --------------------------------------------------------
# 現在のObjectを完成品として固定
# --------------------------------------------------------
fixed_collection = get_fixed_collection()
for obj in current_objects:
make_independent_material(obj)
mark_completed_object(obj)
move_object_to_collection(
obj,
fixed_collection
)
# --------------------------------------------------------
# 新しいObjectを生成
# --------------------------------------------------------
settings = context.scene.cyl_grid_settings
generate_grid(settings)
self.report(
{'INFO'},
"前の完成Objectを保持して、次のObjectを作成しました"
)
return {'FINISHED'}
# ================================================================
# 15. Detach / Complete
# ================================================================
class CYLGRID_OT_detach(bpy.types.Operator):
bl_idname = "cylgrid.detach"
bl_label = "完成Objectとして切離し"
bl_description = "現在の格子を完成Objectとして固定"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
current_objects = get_current_objects()
if not current_objects:
self.report(
{'WARNING'},
"現在の生成Objectがありません"
)
return {'CANCELLED'}
fixed_collection = get_fixed_collection()
for obj in current_objects:
make_independent_material(obj)
mark_completed_object(obj)
move_object_to_collection(
obj,
fixed_collection
)
self.report(
{'INFO'},
"完成Objectとして切離しました"
)
return {'FINISHED'}
# ================================================================
# 16. Join Completed Objects
# ================================================================
class CYLGRID_OT_join_completed(bpy.types.Operator):
bl_idname = "cylgrid.join_completed"
bl_label = "完成品を一体化"
bl_description = "Cylinder Grid Floorで作成した完成Objectだけを一体化"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
completed = [
obj
for obj in bpy.data.objects
if obj.get(COMPLETED_PROPERTY, False)
]
if len(completed) < 2:
self.report(
{'WARNING'},
"一体化する完成Objectが2つ以上ありません"
)
return {'CANCELLED'}
# --------------------------------------------------------
# 対象を明確に限定
# --------------------------------------------------------
bpy.ops.object.select_all(action='DESELECT')
for obj in completed:
obj.select_set(True)
context.view_layer.objects.active = completed[0]
# --------------------------------------------------------
# Join
# --------------------------------------------------------
bpy.ops.object.join()
joined = context.view_layer.objects.active
joined.name = f"{PREFIX_VAL}Completed_Joined"
joined[CURRENT_SET_PROPERTY] = False
joined[COMPLETED_PROPERTY] = True
self.report(
{'INFO'},
"Cylinder Gridの完成品だけを一体化しました"
)
return {'FINISHED'}
# ================================================================
# 17. Position Information
# ================================================================
class CYLGRID_OT_copy_position(bpy.types.Operator):
bl_idname = "cylgrid.copy_position"
bl_label = "位置情報をコピー"
bl_description = "現在の格子の位置・サイズ情報をコピー"
def execute(self, context):
settings = context.scene.cyl_grid_settings
text = (
"Cylinder Grid Floor\n"
f"X方向 本数: {settings.x_count}\n"
f"X方向 太さ: {settings.x_radius}\n"
f"X方向 長さ: {settings.x_length}\n"
f"Y方向 本数: {settings.y_count}\n"
f"Y方向 太さ: {settings.y_radius}\n"
f"Y方向 長さ: {settings.y_length}\n"
"中心位置: (0.000, 0.000, 0.000)\n"
"格子面: XY\n"
"高さ Z: 0.000\n"
)
context.window_manager.clipboard = text
self.report(
{'INFO'},
"位置・サイズ情報をコピーしました"
)
return {'FINISHED'}
# ================================================================
# 18. Reset
# ================================================================
class CYLGRID_OT_reset(bpy.types.Operator):
bl_idname = "cylgrid.reset"
bl_label = "設定を初期値に戻す"
bl_description = "現在の設定値を初期値に戻す"
def execute(self, context):
settings = context.scene.cyl_grid_settings
settings.x_count = DEFAULT_X_COUNT
settings.x_radius = DEFAULT_X_RADIUS
settings.x_length = DEFAULT_X_LENGTH
settings.x_color = DEFAULT_X_COLOR
settings.x_alpha = DEFAULT_X_ALPHA
settings.y_count = DEFAULT_Y_COUNT
settings.y_radius = DEFAULT_Y_RADIUS
settings.y_length = DEFAULT_Y_LENGTH
settings.y_color = DEFAULT_Y_COLOR
settings.y_alpha = DEFAULT_Y_ALPHA
generate_grid(settings)
self.report(
{'INFO'},
"設定を初期値に戻しました"
)
return {'FINISHED'}
# ================================================================
# 19. Links Panel
# deepdeep rule:
# Panelsの最後から2番目
# ================================================================
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 = "Cylinder Grid Floor"
bl_order = 998
def draw(self, context):
layout = self.layout
layout.label(
text="Cylinder Grid Floor"
)
# --------------------------------------------------------
# Links
#
# ここは参照アドオンのLinks Panelを
# 後からそのまま移植する位置。
# --------------------------------------------------------
layout.label(
text="Documentation"
)
layout.label(
text=f"Created: {CREATED_DATE}"
)
layout.label(
text=f"Updated: {UPDATED_DATE}"
)
# ================================================================
# 20. Addon Remove Panel
# deepdeep rule:
# Panelsの最後
# ================================================================
class CYLGRID_OT_remove_addon(bpy.types.Operator):
bl_idname = "cylgrid.remove_addon"
bl_label = "アドオン削除"
bl_description = "Cylinder Grid Floorアドオンを無効化"
def execute(self, context):
# --------------------------------------------------------
# deepdeep / blender5 standard
# --------------------------------------------------------
unregister()
self.report(
{'INFO'},
"Cylinder Grid Floorを削除しました"
)
return {'FINISHED'}
class CYLGRID_PT_remove(bpy.types.Panel):
bl_label = "アドオン削除"
bl_idname = "CYLGRID_PT_remove"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Cylinder Grid Floor"
bl_order = 999
def draw(self, context):
layout = self.layout
layout.label(
text="Cylinder Grid Floor"
)
layout.operator(
"cylgrid.remove_addon",
icon='TRASH'
)
# ================================================================
# 21. Main Panel
# ================================================================
class CYLGRID_PT_main(bpy.types.Panel):
bl_label = "Floor代替・円柱格子"
bl_idname = "CYLGRID_PT_main"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Cylinder Grid Floor"
def draw(self, context):
layout = self.layout
settings = context.scene.cyl_grid_settings
# ========================================================
# X方向
# ========================================================
box = layout.box()
box.label(
text="X軸方向の円柱",
icon='AXIS_X'
)
box.prop(
settings,
"x_count"
)
box.prop(
settings,
"x_radius"
)
box.prop(
settings,
"x_length"
)
box.prop(
settings,
"x_color"
)
box.prop(
settings,
"x_alpha"
)
# ========================================================
# Y方向
# ========================================================
box = layout.box()
box.label(
text="Y軸方向の円柱",
icon='AXIS_Y'
)
box.prop(
settings,
"y_count"
)
box.prop(
settings,
"y_radius"
)
box.prop(
settings,
"y_length"
)
box.prop(
settings,
"y_color"
)
box.prop(
settings,
"y_alpha"
)
# ========================================================
# Generate
# ========================================================
box = layout.box()
box.label(
text="生成"
)
box.operator(
"cylgrid.create",
icon='MESH_GRID'
)
box.operator(
"cylgrid.reset",
icon='FILE_REFRESH'
)
# ========================================================
# Completed Object
# ========================================================
box = layout.box()
box.label(
text="完成Object管理"
)
box.operator(
"cylgrid.detach",
icon='CHECKMARK'
)
box.operator(
"cylgrid.next_object",
icon='DUPLICATE'
)
box.separator()
box.label(
text="完成品を一体化する場合のみ実行"
)
box.operator(
"cylgrid.join_completed",
icon='AUTOMERGE'
)
# ========================================================
# Position
# ========================================================
box = layout.box()
box.label(
text="位置情報"
)
box.label(
text="中心: X 0 / Y 0 / Z 0"
)
box.label(
text=f"X長さ: {settings.x_length}"
)
box.label(
text=f"Y長さ: {settings.y_length}"
)
box.operator(
"cylgrid.copy_position",
icon='COPYDOWN'
)
# ================================================================
# 22. Classes
# ================================================================
classes = (
CYLGRID_PG_Settings,
CYLGRID_OT_create,
CYLGRID_OT_next_object,
CYLGRID_OT_detach,
CYLGRID_OT_join_completed,
CYLGRID_OT_copy_position,
CYLGRID_OT_reset,
CYLGRID_OT_remove_addon,
CYLGRID_PT_main,
CYLGRID_PT_links,
CYLGRID_PT_remove,
)
# ================================================================
# 23. Register
# ================================================================
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.cyl_grid_settings = bpy.props.PointerProperty(
type=CYLGRID_PG_Settings
)
# ------------------------------------------------------------
# 初回生成
# ------------------------------------------------------------
try:
settings = bpy.context.scene.cyl_grid_settings
if not get_current_objects():
generate_grid(settings)
except Exception as exc:
print(
f"[Cylinder Grid Floor] Initial generation error: {exc}"
)
# ================================================================
# 24. Unregister
# ================================================================
def unregister():
global UPDATE_GUARD
UPDATE_GUARD = True
try:
# --------------------------------------------------------
# Scene Property
# --------------------------------------------------------
if hasattr(
bpy.types.Scene,
"cyl_grid_settings"
):
del bpy.types.Scene.cyl_grid_settings
# --------------------------------------------------------
# Classes
# --------------------------------------------------------
for cls in reversed(classes):
try:
bpy.utils.unregister_class(cls)
except RuntimeError:
pass
finally:
UPDATE_GUARD = False
# ================================================================
# 25. Direct execution
# ================================================================
if __name__ == "__main__":
register()