# ============================================================
# 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, 0, 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.10
DEFAULT_Y_RADIUS = 0.10
DEFAULT_X_SPACING = 1.0
DEFAULT_Y_SPACING = 1.0
DEFAULT_X_LENGTH = 5.0
DEFAULT_Y_LENGTH = 5.0
DEFAULT_X_COLOR = (0.08, 0.32, 1.0, 1.0)
DEFAULT_X_ALPHA = 1.0
DEFAULT_X_BACK_COLOR = (0.02, 0.10, 0.35, 1.0)
DEFAULT_X_BACK_ALPHA = 1.0
DEFAULT_Y_COLOR = (0.10, 0.80, 0.35, 1.0)
DEFAULT_Y_ALPHA = 1.0
DEFAULT_Y_BACK_COLOR = (0.02, 0.30, 0.12, 1.0)
DEFAULT_Y_BACK_ALPHA = 1.0
DEFAULT_TOP_COLOR = (0.15, 0.65, 1.0, 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.0
DEFAULT_CENTER_Y = 0.0
DEFAULT_CENTER_Z = 0.0
# ============================================================
# 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):
# Blender 5系での変更に備えた安全な設定。
# 利用可能なAPIだけを使用する。
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])
# Front/Back切替
links.new(geometry.outputs["Backfacing"], color_mix.inputs[0])
# AlphaもBackfacingに応じて独立切替
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,
):
"""
bpy.ops.mesh.primitive_cylinder_add を使わず、
bmeshで円柱メッシュを生成する。
"""
mesh = bpy.data.meshes.new(
unique_mesh_name(f"{PREFIX_VAL}MESH_{axis}")
)
bm = bmesh.new()
# 円柱はZ軸で作成し、最後に回転。
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):
"""
円柱端部用の独立蓋。
蓋は別Objectとして生成する。
"""
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
# create_circleはXY平面。
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):
"""
現在の生成対象だけを再構築する。
完成Objectは絶対に触らない。
"""
delete_current_objects()
current, _fixed = ensure_main_collections()
session_id = int(props.session_id)
# --------------------------------------------------------
# X方向円柱
# X軸に沿って配置し、Y方向に等間隔で並べる
# --------------------------------------------------------
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軸に沿って配置し、X方向に等間隔で並べる
# --------------------------------------------------------
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方向
# --------------------------------------------------------
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方向
# --------------------------------------------------------
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'}
# Materialを完全独立化
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'}
# 完成Objectだけを選択
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_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 = "cyl_grid.remove_addon"
bl_label = "アドオン削除"
bl_description = "Cylinder Grid Floorアドオンを無効化します"
def invoke(self, context, event):
return context.window_manager.invoke_confirm(
self,
event,
)
def execute(self, context):
try:
unregister()
except Exception as exc:
self.report({'ERROR'}, f"アドオン削除に失敗しました: {exc}")
return {'CANCELLED'}
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="リアルタイム更新中")
# ----------------------------------------------------
# X方向
# ----------------------------------------------------
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)
# ----------------------------------------------------
# Y方向
# ----------------------------------------------------
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',
)
# ----------------------------------------------------
# Reset
# ----------------------------------------------------
layout.operator(
"cyl_grid.reset_settings",
text="現在設定を初期値へ戻す",
icon='LOOP_BACK',
)
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}")
# 参照コードで使えるようにリンク欄を固定。
# 必要なURLはここへ追加できる。
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.label(
text="Cylinder Grid Floor",
icon='PANEL_CLOSE',
)
layout.label(
text="アドオンを無効化します。",
icon='INFO',
)
layout.operator(
"cyl_grid.remove_addon",
text="アドオン削除",
icon='TRASH',
)
# ============================================================
# 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_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():
"""
BlenderのUI状態を壊さないよう、timerから安全にNパネルを開く。
"""
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':
# Nパネルはbpy.opsではなくcontext overrideで開く。
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()
# UI更新が完了してからNパネルを開く。
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
# Material Preview / N-panel
try:
bpy.app.timers.register(
startup_ui,
first_interval=0.5,
)
except Exception:
pass
def unregister():
# Scene Property
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__":
register()