bl_info = {
"name": "zionad, オブジェクト統合ジェネレーター",
"author": "zionad & AI Assistant",
"version": (2, 3, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar",
"description": "全図形対応、テキストを形状ドロップダウンリストに統合(リアルタイム文字変更対応)",
"category": "3D View",
}
# 【作成日】 2026年7月12日
# 【初期設定・定数定義】
ADDON_TITLE = "図形 All-in-One"
PREFIX = "zukkei2026"
DEFAULT_COLLECTION_NAME = "Zion_Collection"
DEFAULT_SUBCOLLECTION_NAME = "Zion_SubCollection"
DEFAULT_OBJECT_NAME = "Zion_Object"
DEFAULT_COLOR = (0.1, 0.6, 0.8, 1.0)
DEFAULT_P1_X = 0.0
DEFAULT_P1_Y = 0.0
DEFAULT_P1_Z = 2.0
DEFAULT_P2_X = 0.0
DEFAULT_P2_Y = 0.0
DEFAULT_P2_Z = 0.0
DEFAULT_R1 = 0.5
DEFAULT_R2 = 1.0
DEFAULT_SEGMENTS = 32
import bpy
import bmesh
import math
import mathutils
PROP_NAME = f"{PREFIX}_props"
CTRL_FLAG = f"is_{PREFIX}_controlled"
MAT_NAME = f"{PREFIX}_Live_Material"
MESH_NAME = f"{PREFIX}_Live_Mesh"
# =========================================================
# コールバック関数 (リアルタイムメッシュ更新)
# =========================================================
def update_controlled_object(self, context):
obj = context.view_layer.objects.active
if not obj or obj.get(CTRL_FLAG) != True:
return
shape = self.shape_enum
# どの図形でもXYZの位置・回転・サイズ(スケール)を適用
obj.location = self.location
obj.rotation_euler = self.rotation
obj.scale = self.scale_vec
# メッシュ形状再構築
rebuild_mesh_geometry(obj, shape, self)
# メッシュ再構築後にマテリアルと色を確実にアサインする
mat = bpy.data.materials.get(MAT_NAME)
if not mat:
mat = bpy.data.materials.new(name=MAT_NAME)
if len(obj.data.materials) == 0:
obj.data.materials.append(mat)
elif obj.data.materials[0] != mat:
obj.data.materials[0] = mat
mat.use_nodes = True
mat.blend_method = 'BLEND' if self.color[3] < 1.0 else 'OPAQUE'
# ソリッド(ビューポート)モード用カラー
mat.diffuse_color = self.color
# ノード名ではなくノードタイプで確実にBSDFを取得
bsdf = next((node for node in mat.node_tree.nodes if node.type == 'BSDF_PRINCIPLED'), None)
if bsdf:
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = self.color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = self.color[3]
def update_shape_enum(self, context):
shape = self.shape_enum
if shape == 'CYLINDER':
self.p1, self.p2 = (0, 0, 2), (0, 0, 0)
self.r1, self.r2 = 1.0, 1.0
self.cap_top, self.cap_bottom = True, True
elif shape == 'CONE':
self.p1, self.p2 = (0, 0, 2), (0, 0, 0)
self.r1, self.r2 = 0.0, 1.0
self.cap_top, self.cap_bottom = False, True
elif shape == 'TRUNCATED_CONE':
self.p1, self.p2 = (0, 0, 2), (0, 0, 0)
self.r1, self.r2 = 0.5, 1.0
self.cap_top, self.cap_bottom = False, False
elif shape == 'SKEW_CONE':
self.p1, self.p2 = (1, 1, 2), (0, 0, 0)
self.r1, self.r2 = 0.5, 1.0
self.cap_top, self.cap_bottom = False, False
update_controlled_object(self, context)
# =========================================================
# 形状生成ロジック統合 (分割数・テキスト対応)
# =========================================================
def create_text_mesh_data(props):
font_curve = bpy.data.curves.new(name="TempTextPath", type='FONT')
font_curve.body = props.input_text if props.input_text else " "
font_curve.extrude = props.text_extrude
font_curve.align_x = 'CENTER'
font_curve.align_y = 'CENTER'
temp_obj = bpy.data.objects.new("TempTextObj", font_curve)
bpy.context.scene.collection.objects.link(temp_obj)
depsgraph = bpy.context.evaluated_depsgraph_get()
object_eval = temp_obj.evaluated_get(depsgraph)
mesh = bpy.data.meshes.new_from_object(object_eval)
bpy.data.objects.remove(temp_obj)
bpy.data.curves.remove(font_curve)
return mesh
def create_torus_mesh_data(shape_type, props):
seg = max(3, props.segments)
if shape_type == 'SQUARE_TORUS':
pts = [mathutils.Vector((-1,-1,0)), mathutils.Vector((1,-1,0)), mathutils.Vector((1,1,0)), mathutils.Vector((-1,1,0))]
elif shape_type == 'CIRCLE_TORUS':
pts = [mathutils.Vector((math.cos(a), math.sin(a), 0)) for a in [2*math.pi*i/seg for i in range(seg)]]
elif shape_type == 'ELLIPSE_TORUS':
pts = [mathutils.Vector((math.cos(a)*1.5, math.sin(a)*0.75, 0)) for a in [2*math.pi*i/seg for i in range(seg)]]
else: return None
curve_data = bpy.data.curves.new(name="TempTorusPath", type='CURVE')
curve_data.dimensions = '3D'
spline = curve_data.splines.new(type='BEZIER')
spline.use_cyclic_u = True
spline.bezier_points.add(len(pts) - 1)
for i, pt in enumerate(pts):
bp = spline.bezier_points[i]
bp.co = pt
bp.handle_left_type = 'VECTOR'
bp.handle_right_type = 'VECTOR'
curve_data.bevel_depth = props.torus_pipe_radius
curve_data.bevel_mode = 'ROUND'
curve_data.bevel_resolution = max(1, props.segments // 8)
temp_obj = bpy.data.objects.new("TempTorusObj", curve_data)
bpy.context.scene.collection.objects.link(temp_obj)
depsgraph = bpy.context.evaluated_depsgraph_get()
object_eval = temp_obj.evaluated_get(depsgraph)
mesh = bpy.data.meshes.new_from_object(object_eval)
bpy.data.objects.remove(temp_obj)
bpy.data.curves.remove(curve_data)
return mesh
def rebuild_mesh_geometry(obj, shape_type, props):
is_torus_or_text = shape_type in ('SQUARE_TORUS', 'CIRCLE_TORUS', 'ELLIPSE_TORUS', 'TEXT')
if is_torus_or_text:
if shape_type == 'TEXT':
new_mesh = create_text_mesh_data(props)
else:
new_mesh = create_torus_mesh_data(shape_type, props)
if new_mesh:
old_mesh = obj.data
obj.data = new_mesh
if old_mesh and old_mesh.name == MESH_NAME:
bpy.data.meshes.remove(old_mesh)
return
if obj.data.name != MESH_NAME:
old_mesh = obj.data
obj.data = bpy.data.meshes.new(MESH_NAME)
if old_mesh and old_mesh.users == 0:
bpy.data.meshes.remove(old_mesh)
mesh = obj.data
bm = bmesh.new()
is_line_shape = shape_type in {'CYLINDER', 'CONE', 'TRUNCATED_CONE', 'SKEW_CONE'}
if is_line_shape:
p1, p2 = mathutils.Vector(props.p1), mathutils.Vector(props.p2)
r1, r2 = props.r1, props.r2
segments = max(3, props.segments)
u = mathutils.Vector((1, 0, 0))
v = mathutils.Vector((0, 1, 0))
verts_p1, verts_p2 = [], []
for i in range(segments):
angle = 2.0 * math.pi * i / segments
cos_a, sin_a = math.cos(angle), math.sin(angle)
verts_p2.append(bm.verts.new(p2 + (u * cos_a + v * sin_a) * r2))
verts_p1.append(bm.verts.new(p1 + (u * cos_a + v * sin_a) * r1))
for i in range(segments):
next_i = (i + 1) % segments
bm.faces.new((verts_p2[i], verts_p2[next_i], verts_p1[next_i], verts_p1[i]))
if props.cap_bottom and r2 > 0.001: bm.faces.new(reversed(verts_p2))
if props.cap_top and r1 > 0.001: bm.faces.new(verts_p1)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
else:
seg = max(3, props.segments)
if shape_type == 'SQUARE_FACE':
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=0.5)
elif shape_type == 'CIRCLE_FACE':
bmesh.ops.create_circle(bm, cap_ends=True, cap_tris=False, radius=0.5, segments=seg)
elif shape_type == 'ELLIPSE_FACE':
bmesh.ops.create_circle(bm, cap_ends=True, cap_tris=False, radius=0.5, segments=seg)
for v in bm.verts: v.co.y *= 0.5
elif shape_type == 'MONKEY':
bmesh.ops.create_monkey(bm)
elif shape_type == 'UV_SPHERE':
bmesh.ops.create_uvsphere(bm, u_segments=seg, v_segments=max(3, seg//2), radius=0.5)
elif shape_type == 'CUBE':
bmesh.ops.create_cube(bm, size=1.0)
elif shape_type == 'ICO_SPHERE':
subdiv = max(1, min(6, seg // 10))
bmesh.ops.create_icosphere(bm, subdivisions=subdiv, radius=0.5)
elif shape_type == 'GRID':
grid_seg = max(2, seg)
bmesh.ops.create_grid(bm, x_segments=grid_seg, y_segments=grid_seg, size=1.0)
bm.to_mesh(mesh)
bm.free()
mesh.update()
# =========================================================
# プロパティグループ定義
# =========================================================
class AddonProperties(bpy.types.PropertyGroup):
center_x: bpy.props.FloatProperty(name="Center X", default=0.0)
center_y: bpy.props.FloatProperty(name="Center Y", default=0.0)
center_z: bpy.props.FloatProperty(name="Center Z", default=0.0)
view_width: bpy.props.FloatProperty(name="Width", default=0.0)
view_height: bpy.props.FloatProperty(name="Height", default=0.0)
view_dist: bpy.props.FloatProperty(name="Distance", default=0.0)
is_persp: bpy.props.BoolProperty(default=True)
col_name: bpy.props.StringProperty(name="Collection", default=DEFAULT_COLLECTION_NAME)
subcol_name: bpy.props.StringProperty(name="Sub Collection", default=DEFAULT_SUBCOLLECTION_NAME)
obj_name: bpy.props.StringProperty(name="Object Name", default=DEFAULT_OBJECT_NAME)
shape_enum: bpy.props.EnumProperty(
name="Shape",
items=[
('CYLINDER', "Cylinder (円柱)", ""),
('CONE', "Cone (円錐)", ""),
('TRUNCATED_CONE', "Truncated Cone (円錐台)", ""),
('SKEW_CONE', "Skew Cone (斜め円錐台)", ""),
('SQUARE_FACE', "Square Face (正方形の面)", ""),
('CIRCLE_FACE', "Circle Face (円の面)", ""),
('ELLIPSE_FACE', "Ellipse Face (楕円の面)", ""),
('SQUARE_TORUS', "Square Torus (正方形トーラス)", ""),
('CIRCLE_TORUS', "Circle Torus (円形トーラス)", ""),
('ELLIPSE_TORUS', "Ellipse Torus (楕円形トーラス)", ""),
('TEXT', "Text (テキスト)", ""),
('MONKEY', "Monkey (モンキー)", ""),
('UV_SPHERE', "UV Sphere (UV球)", ""),
('CUBE', "Cube (立方体)", ""),
('ICO_SPHERE', "Ico Sphere (ICO球)", ""),
('GRID', "Grid (グリッド)", ""),
],
default='CYLINDER',
update=update_shape_enum
)
color: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=DEFAULT_COLOR, min=0.0, max=1.0, update=update_controlled_object)
location: bpy.props.FloatVectorProperty(name="位置", default=(0,0,0), subtype='TRANSLATION', update=update_controlled_object)
rotation: bpy.props.FloatVectorProperty(name="回転", default=(0,0,0), subtype='EULER', update=update_controlled_object)
scale_vec: bpy.props.FloatVectorProperty(name="サイズ", default=(1.0, 1.0, 1.0), subtype='XYZ', update=update_controlled_object)
segments: bpy.props.IntProperty(name="分割数 / 頂点数", default=DEFAULT_SEGMENTS, min=3, max=512, update=update_controlled_object)
p1: bpy.props.FloatVectorProperty(name="上面中心", default=(DEFAULT_P1_X, DEFAULT_P1_Y, DEFAULT_P1_Z), update=update_controlled_object)
p2: bpy.props.FloatVectorProperty(name="下面中心", default=(DEFAULT_P2_X, DEFAULT_P2_Y, DEFAULT_P2_Z), update=update_controlled_object)
r1: bpy.props.FloatProperty(name="上面半径", default=DEFAULT_R1, min=0.0, update=update_controlled_object)
r2: bpy.props.FloatProperty(name="下面半径", default=DEFAULT_R2, min=0.0, update=update_controlled_object)
cap_top: bpy.props.BoolProperty(name="上面を塞ぐ", default=True, update=update_controlled_object)
cap_bottom: bpy.props.BoolProperty(name="下面を塞ぐ", default=True, update=update_controlled_object)
torus_pipe_radius: bpy.props.FloatProperty(name="Pipe Thickness", default=0.1, min=0.01, update=update_controlled_object)
# 【追加】テキスト用のプロパティ(リアルタイム連動)
input_text: bpy.props.StringProperty(name="文字", default="Sample Text", update=update_controlled_object)
text_extrude: bpy.props.FloatProperty(name="厚み", default=0.05, min=0.0, update=update_controlled_object)
image_filepath: bpy.props.StringProperty(name="Image Path", subtype='FILE_PATH', default="")
# =========================================================
# オペレーター (機能)
# =========================================================
def get_or_create_collection(context, main_name, sub_name):
main_col = bpy.data.collections.get(main_name)
if not main_col:
main_col = bpy.data.collections.new(main_name)
context.scene.collection.children.link(main_col)
sub_col = bpy.data.collections.get(sub_name)
if not sub_col:
sub_col = bpy.data.collections.new(sub_name)
main_col.children.link(sub_col)
return sub_col
class OT_MoveToCenter(bpy.types.Operator):
bl_idname = f"{PREFIX}.move_to_center"
bl_label = "選択オブジェクトを画面中央へ"
def execute(self, context):
obj = context.active_object
if obj and context.region_data:
obj.location = context.region_data.view_location
self.report({'INFO'}, "画面中央に移動しました")
return {'FINISHED'}
class OT_UpdateViewInfo(bpy.types.Operator):
bl_idname = f"{PREFIX}.update_view_info"
bl_label = "画面情報を更新"
def execute(self, context):
props = getattr(context.scene, PROP_NAME)
region = context.region
region_data = context.region_data
space = context.space_data
if not region_data: return {'CANCELLED'}
loc = region_data.view_location
props.center_x, props.center_y, props.center_z = loc.x, loc.y, loc.z
props.view_dist = region_data.view_distance
props.is_persp = (region_data.view_perspective == 'PERSP')
if props.is_persp:
lens = space.camera.data.lens if (space.camera and space.region_3d.view_perspective == 'CAMERA') else space.lens
sensor = space.camera.data.sensor_width if (space.camera and space.region_3d.view_perspective == 'CAMERA') else 32.0
fov = 2 * math.atan(sensor / (2 * lens))
props.view_width = 2 * props.view_dist * math.tan(fov / 2)
props.view_height = props.view_width * (region.height / region.width)
else:
props.view_width = props.view_dist * 2
props.view_height = props.view_width * (region.height / region.width)
return {'FINISHED'}
class OT_CopyInfo(bpy.types.Operator):
bl_idname = f"{PREFIX}.copy_info"
bl_label = "情報をコピー"
def execute(self, context):
props = getattr(context.scene, PROP_NAME)
text = (
f"中央位置: {props.center_x:.1f}, {props.center_y:.1f}, {props.center_z:.1f}\n"
f"水平サイズ: {props.view_width:.1f}\n"
f"上下サイズ: {props.view_height:.1f}\n"
)
text += f"視座距離: {props.view_dist:.1f}" if props.is_persp else "視座距離: (平行投影)"
context.window_manager.clipboard = text
self.report({'INFO'}, "情報をクリップボードにコピーしました")
return {'FINISHED'}
class OT_Create(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_object"
bl_label = "作成・同期"
def execute(self, context):
props = getattr(context.scene, PROP_NAME)
for obj in context.scene.objects:
if obj.get(CTRL_FLAG) == True: obj[CTRL_FLAG] = False
mesh = bpy.data.meshes.new(MESH_NAME)
obj = bpy.data.objects.new(props.obj_name, mesh)
col = get_or_create_collection(context, props.col_name, props.subcol_name)
col.objects.link(obj)
obj[CTRL_FLAG] = True
context.view_layer.objects.active = obj
obj.select_set(True)
update_controlled_object(props, context)
self.report({'INFO'}, "オブジェクトを生成しました")
return {'FINISHED'}
class OT_Detach(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_object"
bl_label = "切り離す"
def execute(self, context):
obj = context.active_object
if not obj or obj.get(CTRL_FLAG) != True:
return {'CANCELLED'}
obj[CTRL_FLAG] = False
props = getattr(context.scene, PROP_NAME)
if obj.data.materials:
live_mat = obj.data.materials[0]
if live_mat and live_mat.name == MAT_NAME:
c = props.color
rgb_str = f"{round(c[0],2)}_{round(c[1],2)}_{round(c[2],2)}"
new_mat_name = f"{PREFIX}_Mat_{rgb_str}"
exist_mat = bpy.data.materials.get(new_mat_name)
if exist_mat: obj.data.materials[0] = exist_mat
else:
new_mat = live_mat.copy()
new_mat.name = new_mat_name
obj.data.materials[0] = new_mat
if obj.data.name == MESH_NAME:
new_mesh = obj.data.copy()
new_mesh.name = f"Mesh_{obj.name}"
obj.data = new_mesh
self.report({'INFO'}, "完全に切り離しました")
return {'FINISHED'}
def load_image_helper(self, context, mode):
props = getattr(context.scene, PROP_NAME)
path = bpy.path.abspath(props.image_filepath)
if not path: return {'CANCELLED'}
try:
img = bpy.data.images.load(path)
col = get_or_create_collection(context, props.col_name, props.subcol_name)
if mode in ('REF', 'BG'):
empty = bpy.data.objects.new(f"{props.obj_name}_{mode}", img)
col.objects.link(empty)
empty.empty_display_type = 'IMAGE'
if mode == 'BG': empty.empty_image_depth = 'BACK'
empty.location, empty.rotation_euler = props.location, props.rotation
elif mode == 'PLANE':
mesh = bpy.data.meshes.new("ImgPlane")
obj = bpy.data.objects.new(f"{props.obj_name}_Plane", mesh)
col.objects.link(obj)
aspect = img.size[0] / img.size[1] if img.size[1] > 0 else 1.0
bm = bmesh.new()
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=0.5)
for v in bm.verts: v.co.x *= aspect
bm.to_mesh(mesh)
bm.free()
mat = bpy.data.materials.new("ImgMat")
mat.use_nodes = True
tex = mat.node_tree.nodes.new("ShaderNodeTexImage")
tex.image = img
bsdf = next((node for node in mat.node_tree.nodes if node.type == 'BSDF_PRINCIPLED'), None)
if bsdf:
mat.node_tree.links.new(tex.outputs['Color'], bsdf.inputs['Base Color'])
obj.data.materials.append(mat)
obj.location, obj.rotation_euler = props.location, props.rotation
elif mode == 'CAM':
cam = context.scene.camera
if not cam:
cam = bpy.data.objects.new("Camera", bpy.data.cameras.new("Camera"))
context.scene.collection.objects.link(cam)
context.scene.camera = cam
cam.data.show_background_images = True
cam.data.background_images.new().image = img
except Exception as e:
self.report({'ERROR'}, str(e))
return {'FINISHED'}
class OT_AddImageRef(bpy.types.Operator): bl_idname, bl_label, execute = f"{PREFIX}.add_img_ref", "参照画像", lambda self, context: load_image_helper(self, context, 'REF')
class OT_AddImageBg(bpy.types.Operator): bl_idname, bl_label, execute = f"{PREFIX}.add_img_bg", "背景画像", lambda self, context: load_image_helper(self, context, 'BG')
class OT_AddImagePlane(bpy.types.Operator): bl_idname, bl_label, execute = f"{PREFIX}.add_img_plane", "画像プレーン", lambda self, context: load_image_helper(self, context, 'PLANE')
class OT_AddImageCam(bpy.types.Operator): bl_idname, bl_label, execute = f"{PREFIX}.add_img_cam", "カメラ背景", lambda self, context: load_image_helper(self, context, 'CAM')
class OT_ResetDefaults(bpy.types.Operator):
bl_idname = f"{PREFIX}.reset_defaults"
bl_label = "初期値に戻す"
def execute(self, context):
props = getattr(context.scene, PROP_NAME)
for k in props.bl_rna.properties.keys():
if k not in {"rna_type", "image_filepath"}: props.property_unset(k)
return {'FINISHED'}
class OT_DisableAddon(bpy.types.Operator):
bl_idname = f"{PREFIX}.disable_addon"
bl_label = "Unregister Addon"
bl_description = "このアドオンの登録を解除し、UIから消去します"
def execute(self, context):
bpy.app.timers.register(unregister, first_interval=0.01)
self.report({'INFO'}, f"[{ADDON_TITLE}] を無効化・解除しました。")
return {'FINISHED'}
# =========================================================
# UI パネル定義
# =========================================================
class PT_Base(bpy.types.Panel):
bl_space_type, bl_region_type, bl_category = 'VIEW_3D', 'UI', ADDON_TITLE
class PT_1_View(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_1", "1. 視点・座標制御", 0
def draw(self, context):
props = getattr(context.scene, PROP_NAME)
layout = self.layout
layout.operator(f"{PREFIX}.move_to_center", icon='VIEWZOOM')
box = layout.box()
row = box.row()
row.label(text=f"中央: ({props.center_x:.1f}, {props.center_y:.1f}, {props.center_z:.1f})")
row.operator(f"{PREFIX}.update_view_info", text="", icon='FILE_REFRESH')
box.label(text=f"水平: {props.view_width:.1f} / 上下: {props.view_height:.1f}")
box.label(text=f"距離: {props.view_dist:.1f} (透視投影)" if props.is_persp else "距離: --- (平行投影)")
box.operator(f"{PREFIX}.copy_info", icon='COPYDOWN')
class PT_2_Generate(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_2", "2. 生成・切り離し", 1
def draw(self, context):
props = getattr(context.scene, PROP_NAME)
layout = self.layout
layout.prop(props, "shape_enum", text="形状選択")
col = layout.column(align=True)
col.scale_y = 1.1
col.operator(f"{PREFIX}.create_object", text="作成 / 同期", icon='ADD')
col.operator(f"{PREFIX}.detach_object", text="アドオンから切り離す", icon='UNLINKED')
class PT_3_Realtime(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_3", "3. リアルタイム調整", 2
def draw(self, context):
props = getattr(context.scene, PROP_NAME)
layout = self.layout
box = layout.box()
box.prop(props, "color")
is_line = props.shape_enum in {'CYLINDER', 'CONE', 'TRUNCATED_CONE', 'SKEW_CONE'}
is_torus = props.shape_enum in {'SQUARE_TORUS', 'CIRCLE_TORUS', 'ELLIPSE_TORUS'}
box.label(text="[ トランスフォーム ]", icon='OUTLINER_OB_EMPTY')
box.prop(props, "location")
box.prop(props, "rotation")
box.prop(props, "scale_vec")
if is_line:
box.label(text="[ 2点軸パラメータ ]", icon='TRACKING')
box.prop(props, "p1")
box.prop(props, "p2")
row = box.row()
row.prop(props, "r1")
row.prop(props, "r2")
box.prop(props, "segments")
row2 = box.row()
row2.prop(props, "cap_top")
row2.prop(props, "cap_bottom")
elif props.shape_enum == 'TEXT':
box.label(text="[ テキストパラメータ ]", icon='SORTALPHA')
box.prop(props, "input_text")
box.prop(props, "text_extrude")
else:
if is_torus: box.prop(props, "torus_pipe_radius")
if props.shape_enum not in {'CUBE', 'MONKEY', 'SQUARE_FACE', 'SQUARE_TORUS'}:
box.prop(props, "segments")
class PT_4_IO(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_4", "4. 画像・背景追加", 3
def draw(self, context):
props = getattr(context.scene, PROP_NAME)
layout = self.layout
box = layout.box()
box.prop(props, "image_filepath", text="画像パス")
grid = box.grid_flow(columns=2, align=True)
grid.operator(f"{PREFIX}.add_img_ref", icon='EMPTY_DATA')
grid.operator(f"{PREFIX}.add_img_bg", icon='IMAGE_BACKGROUND')
grid.operator(f"{PREFIX}.add_img_plane", icon='MESH_PLANE')
grid.operator(f"{PREFIX}.add_img_cam", icon='OUTLINER_OB_CAMERA')
class PT_5_Settings(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_5", "5. 初期設定", 4
def draw(self, context):
props = getattr(context.scene, PROP_NAME)
layout = self.layout
layout.prop(props, "col_name")
layout.prop(props, "subcol_name")
layout.prop(props, "obj_name")
layout.operator(f"{PREFIX}.reset_defaults", icon='LOOP_BACK')
class PT_6_Link(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_6", "6. リンク", 5
def draw(self, context):
self.layout.operator("wm.url_open", text="公式ドキュメント", icon='URL').url = "<https://www.blender.org/>"
class PT_7_Delete(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_7", "7. アドオン削除パネル", 6
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
self.layout.label(text="このアドオンを直ちに消去します", icon='ERROR')
self.layout.operator(f"{PREFIX}.disable_addon", icon='CANCEL')
# =========================================================
# 登録処理
# =========================================================
classes = (
AddonProperties,
OT_MoveToCenter, OT_UpdateViewInfo, OT_CopyInfo,
OT_Create, OT_Detach,
OT_AddImageRef, OT_AddImageBg, OT_AddImagePlane, OT_AddImageCam,
OT_ResetDefaults, OT_DisableAddon,
PT_1_View, PT_2_Generate, PT_3_Realtime, PT_4_IO, PT_5_Settings, PT_6_Link, PT_7_Delete,
)
def register():
for cls in classes: bpy.utils.register_class(cls)
setattr(bpy.types.Scene, PROP_NAME, bpy.props.PointerProperty(type=AddonProperties))
def unregister():
if hasattr(bpy.types.Scene, PROP_NAME):
delattr(bpy.types.Scene, PROP_NAME)
for cls in reversed(classes):
try:
bpy.utils.unregister_class(cls)
except:
pass
if __name__ == "__main__":
register()
bl_info = {
"name": "zionad, オブジェクト統合ジェネレーター",
"author": "zionad & AI Assistant",
"version": (2, 0, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar",
"description": "全図形対応、バグ完全修正(色反映・メモリリーク対策済み)、安全解除機能",
"category": "3D View",
}
# 【作成日】 2026年7月12日
# 【初期設定・定数定義】
ADDON_TITLE = "図形 All-in-One"
PREFIX = "zukkei2026"
DEFAULT_COLLECTION_NAME = "Zion_Collection"
DEFAULT_SUBCOLLECTION_NAME = "Zion_SubCollection"
DEFAULT_OBJECT_NAME = "Zion_Object"
DEFAULT_COLOR = (0.1, 0.6, 0.8, 1.0)
DEFAULT_P1_X = 0.0
DEFAULT_P1_Y = 0.0
DEFAULT_P1_Z = 2.0
DEFAULT_P2_X = 0.0
DEFAULT_P2_Y = 0.0
DEFAULT_P2_Z = 0.0
DEFAULT_R1 = 0.5
DEFAULT_R2 = 1.0
DEFAULT_SEGMENTS = 32
import bpy
import bmesh
import math
import mathutils
PROP_NAME = f"{PREFIX}_props"
CTRL_FLAG = f"is_{PREFIX}_controlled"
MAT_NAME = f"{PREFIX}_Live_Material"
MESH_NAME = f"{PREFIX}_Live_Mesh"
# =========================================================
# コールバック関数 (リアルタイムメッシュ更新)
# =========================================================
def update_controlled_object(self, context):
obj = context.view_layer.objects.active
if not obj or obj.get(CTRL_FLAG) != True:
return
shape = self.shape_enum
# どの図形でもXYZの位置・回転・サイズ(スケール)を適用
obj.location = self.location
obj.rotation_euler = self.rotation
obj.scale = self.scale_vec
# 【修正1】 先にメッシュ形状を再構築する(トーラスなどでメッシュがすり替わるため)
rebuild_mesh_geometry(obj, shape, self)
# 【修正1】 メッシュ再構築後にマテリアルと色を確実にアサインする
mat = bpy.data.materials.get(MAT_NAME)
if not mat:
mat = bpy.data.materials.new(name=MAT_NAME)
if len(obj.data.materials) == 0:
obj.data.materials.append(mat)
elif obj.data.materials[0] != mat:
obj.data.materials[0] = mat
mat.use_nodes = True
mat.blend_method = 'BLEND' if self.color[3] < 1.0 else 'OPAQUE'
# ソリッド(ビューポート)モード用カラー
mat.diffuse_color = self.color
# ノード名ではなくノードタイプで確実にBSDFを取得
bsdf = next((node for node in mat.node_tree.nodes if node.type == 'BSDF_PRINCIPLED'), None)
if bsdf:
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = self.color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = self.color[3]
def update_shape_enum(self, context):
shape = self.shape_enum
if shape == 'CYLINDER':
self.p1, self.p2 = (0, 0, 2), (0, 0, 0)
self.r1, self.r2 = 1.0, 1.0
self.cap_top, self.cap_bottom = True, True
elif shape == 'CONE':
self.p1, self.p2 = (0, 0, 2), (0, 0, 0)
self.r1, self.r2 = 0.0, 1.0
self.cap_top, self.cap_bottom = False, True
elif shape == 'TRUNCATED_CONE':
self.p1, self.p2 = (0, 0, 2), (0, 0, 0)
self.r1, self.r2 = 0.5, 1.0
self.cap_top, self.cap_bottom = False, False
elif shape == 'SKEW_CONE':
self.p1, self.p2 = (1, 1, 2), (0, 0, 0)
self.r1, self.r2 = 0.5, 1.0
self.cap_top, self.cap_bottom = False, False
update_controlled_object(self, context)
# =========================================================
# 形状生成ロジック統合 (分割数対応)
# =========================================================
def create_torus_mesh_data(shape_type, props):
seg = max(3, props.segments)
if shape_type == 'SQUARE_TORUS':
pts = [mathutils.Vector((-1,-1,0)), mathutils.Vector((1,-1,0)), mathutils.Vector((1,1,0)), mathutils.Vector((-1,1,0))]
elif shape_type == 'CIRCLE_TORUS':
pts = [mathutils.Vector((math.cos(a), math.sin(a), 0)) for a in [2*math.pi*i/seg for i in range(seg)]]
elif shape_type == 'ELLIPSE_TORUS':
pts = [mathutils.Vector((math.cos(a)*1.5, math.sin(a)*0.75, 0)) for a in [2*math.pi*i/seg for i in range(seg)]]
else: return None
curve_data = bpy.data.curves.new(name="TempTorusPath", type='CURVE')
curve_data.dimensions = '3D'
spline = curve_data.splines.new(type='BEZIER')
spline.use_cyclic_u = True
spline.bezier_points.add(len(pts) - 1)
for i, pt in enumerate(pts):
bp = spline.bezier_points[i]
bp.co = pt
bp.handle_left_type = 'VECTOR'
bp.handle_right_type = 'VECTOR'
curve_data.bevel_depth = props.torus_pipe_radius
curve_data.bevel_mode = 'ROUND'
curve_data.bevel_resolution = max(1, props.segments // 8)
temp_obj = bpy.data.objects.new("TempTorusObj", curve_data)
bpy.context.scene.collection.objects.link(temp_obj)
depsgraph = bpy.context.evaluated_depsgraph_get()
object_eval = temp_obj.evaluated_get(depsgraph)
mesh = bpy.data.meshes.new_from_object(object_eval)
# 【修正2】 メモリリーク防止。不要になったオブジェクトとカーブのデータ両方を確実に消去する
bpy.data.objects.remove(temp_obj)
bpy.data.curves.remove(curve_data)
return mesh
def rebuild_mesh_geometry(obj, shape_type, props):
is_torus = shape_type in ('SQUARE_TORUS', 'CIRCLE_TORUS', 'ELLIPSE_TORUS')
if is_torus:
new_mesh = create_torus_mesh_data(shape_type, props)
if new_mesh:
old_mesh = obj.data
obj.data = new_mesh
if old_mesh and old_mesh.name == MESH_NAME:
bpy.data.meshes.remove(old_mesh)
return
if obj.data.name != MESH_NAME:
old_mesh = obj.data
obj.data = bpy.data.meshes.new(MESH_NAME)
if old_mesh and old_mesh.users == 0:
bpy.data.meshes.remove(old_mesh)
mesh = obj.data
bm = bmesh.new()
is_line_shape = shape_type in {'CYLINDER', 'CONE', 'TRUNCATED_CONE', 'SKEW_CONE'}
if is_line_shape:
p1, p2 = mathutils.Vector(props.p1), mathutils.Vector(props.p2)
r1, r2 = props.r1, props.r2
segments = max(3, props.segments)
u = mathutils.Vector((1, 0, 0))
v = mathutils.Vector((0, 1, 0))
verts_p1, verts_p2 = [], []
for i in range(segments):
angle = 2.0 * math.pi * i / segments
cos_a, sin_a = math.cos(angle), math.sin(angle)
verts_p2.append(bm.verts.new(p2 + (u * cos_a + v * sin_a) * r2))
verts_p1.append(bm.verts.new(p1 + (u * cos_a + v * sin_a) * r1))
for i in range(segments):
next_i = (i + 1) % segments
bm.faces.new((verts_p2[i], verts_p2[next_i], verts_p1[next_i], verts_p1[i]))
if props.cap_bottom and r2 > 0.001: bm.faces.new(reversed(verts_p2))
if props.cap_top and r1 > 0.001: bm.faces.new(verts_p1)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
else:
seg = max(3, props.segments)
if shape_type == 'SQUARE_FACE':
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=0.5)
elif shape_type == 'CIRCLE_FACE':
# 【修正4】 中心に不要な頂点を作らず、綺麗な1枚の面(N-gon)にする
bmesh.ops.create_circle(bm, cap_ends=True, cap_tris=False, radius=0.5, segments=seg)
elif shape_type == 'ELLIPSE_FACE':
# 【修正4】 同じく綺麗な1枚の面にする
bmesh.ops.create_circle(bm, cap_ends=True, cap_tris=False, radius=0.5, segments=seg)
for v in bm.verts: v.co.y *= 0.5
elif shape_type == 'MONKEY':
bmesh.ops.create_monkey(bm)
elif shape_type == 'UV_SPHERE':
bmesh.ops.create_uvsphere(bm, u_segments=seg, v_segments=max(3, seg//2), radius=0.5)
elif shape_type == 'CUBE':
bmesh.ops.create_cube(bm, size=1.0)
elif shape_type == 'ICO_SPHERE':
subdiv = max(1, min(6, seg // 10))
bmesh.ops.create_icosphere(bm, subdivisions=subdiv, radius=0.5)
elif shape_type == 'GRID':
grid_seg = max(2, seg)
bmesh.ops.create_grid(bm, x_segments=grid_seg, y_segments=grid_seg, size=1.0)
bm.to_mesh(mesh)
bm.free()
mesh.update()
# =========================================================
# プロパティグループ定義
# =========================================================
class AddonProperties(bpy.types.PropertyGroup):
center_x: bpy.props.FloatProperty(name="Center X", default=0.0)
center_y: bpy.props.FloatProperty(name="Center Y", default=0.0)
center_z: bpy.props.FloatProperty(name="Center Z", default=0.0)
view_width: bpy.props.FloatProperty(name="Width", default=0.0)
view_height: bpy.props.FloatProperty(name="Height", default=0.0)
view_dist: bpy.props.FloatProperty(name="Distance", default=0.0)
is_persp: bpy.props.BoolProperty(default=True)
col_name: bpy.props.StringProperty(name="Collection", default=DEFAULT_COLLECTION_NAME)
subcol_name: bpy.props.StringProperty(name="Sub Collection", default=DEFAULT_SUBCOLLECTION_NAME)
obj_name: bpy.props.StringProperty(name="Object Name", default=DEFAULT_OBJECT_NAME)
shape_enum: bpy.props.EnumProperty(
name="Shape",
items=[
('CYLINDER', "Cylinder (円柱)", ""),
('CONE', "Cone (円錐)", ""),
('TRUNCATED_CONE', "Truncated Cone (円錐台)", ""),
('SKEW_CONE', "Skew Cone (斜め円錐台)", ""),
('SQUARE_FACE', "Square Face (正方形の面)", ""),
('CIRCLE_FACE', "Circle Face (円の面)", ""),
('ELLIPSE_FACE', "Ellipse Face (楕円の面)", ""),
('SQUARE_TORUS', "Square Torus (正方形トーラス)", ""),
('CIRCLE_TORUS', "Circle Torus (円形トーラス)", ""),
('ELLIPSE_TORUS', "Ellipse Torus (楕円形トーラス)", ""),
('MONKEY', "Monkey (モンキー)", ""),
('UV_SPHERE', "UV Sphere (UV球)", ""),
('CUBE', "Cube (立方体)", ""),
('ICO_SPHERE', "Ico Sphere (ICO球)", ""),
('GRID', "Grid (グリッド)", ""),
],
default='CYLINDER',
update=update_shape_enum
)
color: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=DEFAULT_COLOR, min=0.0, max=1.0, update=update_controlled_object)
location: bpy.props.FloatVectorProperty(name="位置", default=(0,0,0), subtype='TRANSLATION', update=update_controlled_object)
rotation: bpy.props.FloatVectorProperty(name="回転", default=(0,0,0), subtype='EULER', update=update_controlled_object)
scale_vec: bpy.props.FloatVectorProperty(name="サイズ", default=(1.0, 1.0, 1.0), subtype='XYZ', update=update_controlled_object)
segments: bpy.props.IntProperty(name="分割数 / 頂点数", default=DEFAULT_SEGMENTS, min=3, max=512, update=update_controlled_object)
p1: bpy.props.FloatVectorProperty(name="上面中心", default=(DEFAULT_P1_X, DEFAULT_P1_Y, DEFAULT_P1_Z), update=update_controlled_object)
p2: bpy.props.FloatVectorProperty(name="下面中心", default=(DEFAULT_P2_X, DEFAULT_P2_Y, DEFAULT_P2_Z), update=update_controlled_object)
r1: bpy.props.FloatProperty(name="上面半径", default=DEFAULT_R1, min=0.0, update=update_controlled_object)
r2: bpy.props.FloatProperty(name="下面半径", default=DEFAULT_R2, min=0.0, update=update_controlled_object)
cap_top: bpy.props.BoolProperty(name="上面を塞ぐ", default=True, update=update_controlled_object)
cap_bottom: bpy.props.BoolProperty(name="下面を塞ぐ", default=True, update=update_controlled_object)
torus_pipe_radius: bpy.props.FloatProperty(name="Pipe Thickness", default=0.1, min=0.01, update=update_controlled_object)
image_filepath: bpy.props.StringProperty(name="Image Path", subtype='FILE_PATH', default="")
# =========================================================
# オペレーター (機能)
# =========================================================
def get_or_create_collection(context, main_name, sub_name):
main_col = bpy.data.collections.get(main_name)
if not main_col:
main_col = bpy.data.collections.new(main_name)
context.scene.collection.children.link(main_col)
sub_col = bpy.data.collections.get(sub_name)
if not sub_col:
sub_col = bpy.data.collections.new(sub_name)
main_col.children.link(sub_col)
return sub_col
class OT_MoveToCenter(bpy.types.Operator):
bl_idname = f"{PREFIX}.move_to_center"
bl_label = "選択オブジェクトを画面中央へ"
def execute(self, context):
obj = context.active_object
if obj and context.region_data:
obj.location = context.region_data.view_location
self.report({'INFO'}, "画面中央に移動しました")
return {'FINISHED'}
class OT_UpdateViewInfo(bpy.types.Operator):
bl_idname = f"{PREFIX}.update_view_info"
bl_label = "画面情報を更新"
def execute(self, context):
props = getattr(context.scene, PROP_NAME)
region = context.region
region_data = context.region_data
space = context.space_data
if not region_data: return {'CANCELLED'}
loc = region_data.view_location
props.center_x, props.center_y, props.center_z = loc.x, loc.y, loc.z
props.view_dist = region_data.view_distance
props.is_persp = (region_data.view_perspective == 'PERSP')
if props.is_persp:
lens = space.camera.data.lens if (space.camera and space.region_3d.view_perspective == 'CAMERA') else space.lens
sensor = space.camera.data.sensor_width if (space.camera and space.region_3d.view_perspective == 'CAMERA') else 32.0
fov = 2 * math.atan(sensor / (2 * lens))
props.view_width = 2 * props.view_dist * math.tan(fov / 2)
props.view_height = props.view_width * (region.height / region.width)
else:
props.view_width = props.view_dist * 2
props.view_height = props.view_width * (region.height / region.width)
return {'FINISHED'}
class OT_CopyInfo(bpy.types.Operator):
bl_idname = f"{PREFIX}.copy_info"
bl_label = "情報をコピー"
def execute(self, context):
props = getattr(context.scene, PROP_NAME)
text = (
f"中央位置: {props.center_x:.1f}, {props.center_y:.1f}, {props.center_z:.1f}\n"
f"水平サイズ: {props.view_width:.1f}\n"
f"上下サイズ: {props.view_height:.1f}\n"
)
text += f"視座距離: {props.view_dist:.1f}" if props.is_persp else "視座距離: (平行投影)"
context.window_manager.clipboard = text
self.report({'INFO'}, "情報をクリップボードにコピーしました")
return {'FINISHED'}
class OT_Create(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_object"
bl_label = "作成・同期"
def execute(self, context):
props = getattr(context.scene, PROP_NAME)
for obj in context.scene.objects:
if obj.get(CTRL_FLAG) == True: obj[CTRL_FLAG] = False
mesh = bpy.data.meshes.new(MESH_NAME)
obj = bpy.data.objects.new(props.obj_name, mesh)
col = get_or_create_collection(context, props.col_name, props.subcol_name)
col.objects.link(obj)
obj[CTRL_FLAG] = True
context.view_layer.objects.active = obj
obj.select_set(True)
update_controlled_object(props, context)
self.report({'INFO'}, "オブジェクトを生成しました")
return {'FINISHED'}
class OT_Detach(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_object"
bl_label = "切り離す"
def execute(self, context):
obj = context.active_object
if not obj or obj.get(CTRL_FLAG) != True:
return {'CANCELLED'}
obj[CTRL_FLAG] = False
props = getattr(context.scene, PROP_NAME)
if obj.data.materials:
live_mat = obj.data.materials[0]
if live_mat and live_mat.name == MAT_NAME:
c = props.color
rgb_str = f"{round(c[0],2)}_{round(c[1],2)}_{round(c[2],2)}"
new_mat_name = f"{PREFIX}_Mat_{rgb_str}"
exist_mat = bpy.data.materials.get(new_mat_name)
if exist_mat: obj.data.materials[0] = exist_mat
else:
new_mat = live_mat.copy()
new_mat.name = new_mat_name
obj.data.materials[0] = new_mat
if obj.data.name == MESH_NAME:
new_mesh = obj.data.copy()
new_mesh.name = f"Mesh_{obj.name}"
obj.data = new_mesh
self.report({'INFO'}, "完全に切り離しました")
return {'FINISHED'}
class OT_PasteText(bpy.types.Operator):
bl_idname = f"{PREFIX}.paste_text"
bl_label = "Paste Clipboard as Text"
def execute(self, context):
props = getattr(context.scene, PROP_NAME)
txt = context.window_manager.clipboard
if not txt or txt.strip() == "": return {'CANCELLED'}
font_curve = bpy.data.curves.new(name="Text_Curve", type='FONT')
font_curve.body = txt
font_curve.extrude = 0.05
obj = bpy.data.objects.new(f"{props.obj_name}_Text", font_curve)
get_or_create_collection(context, props.col_name, props.subcol_name).objects.link(obj)
obj.location = props.location
obj.rotation_euler = props.rotation
obj.scale = props.scale_vec
return {'FINISHED'}
def load_image_helper(self, context, mode):
props = getattr(context.scene, PROP_NAME)
path = bpy.path.abspath(props.image_filepath)
if not path: return {'CANCELLED'}
try:
img = bpy.data.images.load(path)
col = get_or_create_collection(context, props.col_name, props.subcol_name)
if mode in ('REF', 'BG'):
# 【修正3】 Emptyに安全に画像をセットする
empty = bpy.data.objects.new(f"{props.obj_name}_{mode}", img)
col.objects.link(empty)
empty.empty_display_type = 'IMAGE'
if mode == 'BG': empty.empty_image_depth = 'BACK'
empty.location, empty.rotation_euler = props.location, props.rotation
elif mode == 'PLANE':
mesh = bpy.data.meshes.new("ImgPlane")
obj = bpy.data.objects.new(f"{props.obj_name}_Plane", mesh)
col.objects.link(obj)
aspect = img.size[0] / img.size[1] if img.size[1] > 0 else 1.0
bm = bmesh.new()
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=0.5)
for v in bm.verts: v.co.x *= aspect
bm.to_mesh(mesh)
bm.free()
mat = bpy.data.materials.new("ImgMat")
mat.use_nodes = True
tex = mat.node_tree.nodes.new("ShaderNodeTexImage")
tex.image = img
# 【修正3】 日本語UIなどでも確実にPrincipled BSDFノードを取得しエラーを防ぐ
bsdf = next((node for node in mat.node_tree.nodes if node.type == 'BSDF_PRINCIPLED'), None)
if bsdf:
mat.node_tree.links.new(tex.outputs['Color'], bsdf.inputs['Base Color'])
obj.data.materials.append(mat)
obj.location, obj.rotation_euler = props.location, props.rotation
elif mode == 'CAM':
cam = context.scene.camera
if not cam:
cam = bpy.data.objects.new("Camera", bpy.data.cameras.new("Camera"))
context.scene.collection.objects.link(cam)
context.scene.camera = cam
cam.data.show_background_images = True
cam.data.background_images.new().image = img
except Exception as e:
self.report({'ERROR'}, str(e))
return {'FINISHED'}
class OT_AddImageRef(bpy.types.Operator): bl_idname, bl_label, execute = f"{PREFIX}.add_img_ref", "参照画像", lambda self, context: load_image_helper(self, context, 'REF')
class OT_AddImageBg(bpy.types.Operator): bl_idname, bl_label, execute = f"{PREFIX}.add_img_bg", "背景画像", lambda self, context: load_image_helper(self, context, 'BG')
class OT_AddImagePlane(bpy.types.Operator): bl_idname, bl_label, execute = f"{PREFIX}.add_img_plane", "画像プレーン", lambda self, context: load_image_helper(self, context, 'PLANE')
class OT_AddImageCam(bpy.types.Operator): bl_idname, bl_label, execute = f"{PREFIX}.add_img_cam", "カメラ背景", lambda self, context: load_image_helper(self, context, 'CAM')
class OT_ResetDefaults(bpy.types.Operator):
bl_idname = f"{PREFIX}.reset_defaults"
bl_label = "初期値に戻す"
def execute(self, context):
props = getattr(context.scene, PROP_NAME)
for k in props.bl_rna.properties.keys():
if k not in {"rna_type", "image_filepath"}: props.property_unset(k)
return {'FINISHED'}
class OT_DisableAddon(bpy.types.Operator):
bl_idname = f"{PREFIX}.disable_addon"
bl_label = "Unregister Addon"
bl_description = "このアドオンの登録を解除し、UIから消去します"
def execute(self, context):
# Timer経由で安全に解除
bpy.app.timers.register(unregister, first_interval=0.01)
self.report({'INFO'}, f"[{ADDON_TITLE}] を無効化・解除しました。")
return {'FINISHED'}
# =========================================================
# UI パネル定義
# =========================================================
class PT_Base(bpy.types.Panel):
bl_space_type, bl_region_type, bl_category = 'VIEW_3D', 'UI', ADDON_TITLE
class PT_1_View(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_1", "1. 視点・座標制御", 0
def draw(self, context):
props = getattr(context.scene, PROP_NAME)
layout = self.layout
layout.operator(f"{PREFIX}.move_to_center", icon='VIEWZOOM')
box = layout.box()
row = box.row()
row.label(text=f"中央: ({props.center_x:.1f}, {props.center_y:.1f}, {props.center_z:.1f})")
row.operator(f"{PREFIX}.update_view_info", text="", icon='FILE_REFRESH')
box.label(text=f"水平: {props.view_width:.1f} / 上下: {props.view_height:.1f}")
box.label(text=f"距離: {props.view_dist:.1f} (透視投影)" if props.is_persp else "距離: --- (平行投影)")
box.operator(f"{PREFIX}.copy_info", icon='COPYDOWN')
class PT_2_Generate(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_2", "2. 生成・切り離し", 1
def draw(self, context):
props = getattr(context.scene, PROP_NAME)
layout = self.layout
layout.prop(props, "shape_enum", text="形状選択")
col = layout.column(align=True)
col.scale_y = 1.1
col.operator(f"{PREFIX}.create_object", text="作成 / 同期", icon='ADD')
col.operator(f"{PREFIX}.detach_object", text="アドオンから切り離す", icon='UNLINKED')
class PT_3_Realtime(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_3", "3. リアルタイム調整", 2
def draw(self, context):
props = getattr(context.scene, PROP_NAME)
layout = self.layout
box = layout.box()
box.prop(props, "color")
is_line = props.shape_enum in {'CYLINDER', 'CONE', 'TRUNCATED_CONE', 'SKEW_CONE'}
is_torus = props.shape_enum in {'SQUARE_TORUS', 'CIRCLE_TORUS', 'ELLIPSE_TORUS'}
box.label(text="[ トランスフォーム ]", icon='OUTLINER_OB_EMPTY')
box.prop(props, "location")
box.prop(props, "rotation")
box.prop(props, "scale_vec")
if is_line:
box.label(text="[ 2点軸パラメータ ]", icon='TRACKING')
box.prop(props, "p1")
box.prop(props, "p2")
row = box.row()
row.prop(props, "r1")
row.prop(props, "r2")
box.prop(props, "segments")
row2 = box.row()
row2.prop(props, "cap_top")
row2.prop(props, "cap_bottom")
else:
if is_torus: box.prop(props, "torus_pipe_radius")
if props.shape_enum not in {'CUBE', 'MONKEY', 'SQUARE_FACE', 'SQUARE_TORUS'}:
box.prop(props, "segments")
class PT_4_IO(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_4", "4. テキスト & 画像追加", 3
def draw(self, context):
props = getattr(context.scene, PROP_NAME)
layout = self.layout
layout.operator(f"{PREFIX}.paste_text", text="テキストをペースト", icon='PASTEDOWN')
box = layout.box()
box.prop(props, "image_filepath", text="画像パス")
grid = box.grid_flow(columns=2, align=True)
grid.operator(f"{PREFIX}.add_img_ref", icon='EMPTY_DATA')
grid.operator(f"{PREFIX}.add_img_bg", icon='IMAGE_BACKGROUND')
grid.operator(f"{PREFIX}.add_img_plane", icon='MESH_PLANE')
grid.operator(f"{PREFIX}.add_img_cam", icon='OUTLINER_OB_CAMERA')
class PT_5_Settings(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_5", "5. 初期設定", 4
def draw(self, context):
props = getattr(context.scene, PROP_NAME)
layout = self.layout
layout.prop(props, "col_name")
layout.prop(props, "subcol_name")
layout.prop(props, "obj_name")
layout.operator(f"{PREFIX}.reset_defaults", icon='LOOP_BACK')
class PT_6_Link(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_6", "6. リンク", 5
def draw(self, context):
self.layout.operator("wm.url_open", text="公式ドキュメント", icon='URL').url = "<https://www.blender.org/>"
class PT_7_Delete(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_7", "7. アドオン削除パネル", 6
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
self.layout.label(text="このアドオンを直ちに消去します", icon='ERROR')
self.layout.operator(f"{PREFIX}.disable_addon", icon='CANCEL')
# =========================================================
# 登録処理
# =========================================================
classes = (
AddonProperties,
OT_MoveToCenter, OT_UpdateViewInfo, OT_CopyInfo,
OT_Create, OT_Detach, OT_PasteText,
OT_AddImageRef, OT_AddImageBg, OT_AddImagePlane, OT_AddImageCam,
OT_ResetDefaults, OT_DisableAddon,
PT_1_View, PT_2_Generate, PT_3_Realtime, PT_4_IO, PT_5_Settings, PT_6_Link, PT_7_Delete,
)
def register():
for cls in classes: bpy.utils.register_class(cls)
setattr(bpy.types.Scene, PROP_NAME, bpy.props.PointerProperty(type=AddonProperties))
def unregister():
if hasattr(bpy.types.Scene, PROP_NAME):
delattr(bpy.types.Scene, PROP_NAME)
for cls in reversed(classes):
try:
bpy.utils.unregister_class(cls)
except:
pass
if __name__ == "__main__":
register()
bl_info = {
"name": "zionad, オブジェクト統合ジェネレーター",
"author": "zionad & AI Assistant",
"version": (1, 6, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar",
"description": "全図形対応、画像・テキスト追加、色反映と分割数調整が完璧に連動する完全版",
"category": "3D View",
}
# 【作成日】 2026年7月12日 06:45
# 【初期設定・定数定義】 (1行ずつ定義)
ADDON_TITLE = "図形 All-in-One"
PREFIX = "zukkei2026" # ※アンダースコアや英小文字のみ推奨
DEFAULT_COLLECTION_NAME = "Zion_Collection"
DEFAULT_SUBCOLLECTION_NAME = "Zion_SubCollection"
DEFAULT_OBJECT_NAME = "Zion_Object"
DEFAULT_COLOR = (0.1, 0.6, 0.8, 1.0)
DEFAULT_P1_X = 0.0
DEFAULT_P1_Y = 0.0
DEFAULT_P1_Z = 2.0
DEFAULT_P2_X = 0.0
DEFAULT_P2_Y = 0.0
DEFAULT_P2_Z = 0.0
DEFAULT_R1 = 0.5
DEFAULT_R2 = 1.0
DEFAULT_SEGMENTS = 32
import bpy
import bmesh
import math
import mathutils
import webbrowser
import addon_utils
PROP_NAME = f"{PREFIX}_props"
CTRL_FLAG = f"is_{PREFIX}_controlled"
MAT_NAME = f"{PREFIX}_Live_Material"
MESH_NAME = f"{PREFIX}_Live_Mesh"
# =========================================================
# コールバック関数 (リアルタイムメッシュ更新)
# =========================================================
def update_controlled_object(self, context):
obj = context.view_layer.objects.active
if not obj or obj.get(CTRL_FLAG) != True:
return
shape = self.shape_enum
is_line_shape = shape in {'CYLINDER', 'CONE', 'TRUNCATED_CONE', 'SKEW_CONE'}
if not is_line_shape:
obj.location = self.location
obj.rotation_euler = self.rotation
obj.scale = (self.size, self.size, self.size)
else:
obj.location = (0.0, 0.0, 0.0)
obj.rotation_euler = (0.0, 0.0, 0.0)
obj.scale = (1.0, 1.0, 1.0)
# ---------------------------------------------------------
# 【修正】マテリアルと色の確実な反映(日本語UI対応)
# ---------------------------------------------------------
mat = bpy.data.materials.get(MAT_NAME)
if not mat:
mat = bpy.data.materials.new(name=MAT_NAME)
if len(obj.data.materials) == 0:
obj.data.materials.append(mat)
elif obj.data.materials[0] != mat:
obj.data.materials[0] = mat
mat.use_nodes = True
mat.blend_method = 'BLEND' if self.color[3] < 1.0 else 'OPAQUE'
# ソリッド(ビューポート)モード用カラー
mat.diffuse_color = self.color
# ノード名ではなくノードタイプで確実にBSDFを取得
bsdf = next((node for node in mat.node_tree.nodes if node.type == 'BSDF_PRINCIPLED'), None)
if bsdf:
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = self.color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = self.color[3]
# メッシュ形状再構築
rebuild_mesh_geometry(obj, shape, self)
def update_shape_enum(self, context):
shape = self.shape_enum
if shape == 'CYLINDER':
self.p1, self.p2 = (0, 0, 2), (0, 0, 0)
self.r1, self.r2 = 1.0, 1.0
elif shape == 'CONE':
self.p1, self.p2 = (0, 0, 2), (0, 0, 0)
self.r1, self.r2 = 0.0, 1.0
elif shape == 'TRUNCATED_CONE':
self.p1, self.p2 = (0, 0, 2), (0, 0, 0)
self.r1, self.r2 = 0.5, 1.0
elif shape == 'SKEW_CONE':
self.p1, self.p2 = (1, 1, 2), (0, 0, 0)
self.r1, self.r2 = 0.5, 1.0
update_controlled_object(self, context)
# =========================================================
# 形状生成ロジック統合 (分割数対応)
# =========================================================
def create_torus_mesh_data(shape_type, props):
seg = max(3, props.segments) # 分割数を適用
if shape_type == 'SQUARE_TORUS':
pts = [mathutils.Vector((-1,-1,0)), mathutils.Vector((1,-1,0)), mathutils.Vector((1,1,0)), mathutils.Vector((-1,1,0))]
elif shape_type == 'CIRCLE_TORUS':
pts = [mathutils.Vector((math.cos(a), math.sin(a), 0)) for a in [2*math.pi*i/seg for i in range(seg)]]
elif shape_type == 'ELLIPSE_TORUS':
pts = [mathutils.Vector((math.cos(a)*1.5, math.sin(a)*0.75, 0)) for a in [2*math.pi*i/seg for i in range(seg)]]
else: return None
curve_data = bpy.data.curves.new(name="TempTorusPath", type='CURVE')
curve_data.dimensions = '3D'
spline = curve_data.splines.new(type='BEZIER')
spline.use_cyclic_u = True
spline.bezier_points.add(len(pts) - 1)
for i, pt in enumerate(pts):
bp = spline.bezier_points[i]
bp.co = pt
bp.handle_left_type = 'VECTOR'
bp.handle_right_type = 'VECTOR'
curve_data.bevel_depth = props.torus_pipe_radius
curve_data.bevel_mode = 'ROUND'
curve_data.bevel_resolution = max(1, props.segments // 8) # 管の解像度も分割数に連動
temp_obj = bpy.data.objects.new("TempTorusObj", curve_data)
bpy.context.scene.collection.objects.link(temp_obj)
depsgraph = bpy.context.evaluated_depsgraph_get()
object_eval = temp_obj.evaluated_get(depsgraph)
mesh = bpy.data.meshes.new_from_object(object_eval)
bpy.data.objects.remove(temp_obj)
return mesh
def rebuild_mesh_geometry(obj, shape_type, props):
is_torus = shape_type in ('SQUARE_TORUS', 'CIRCLE_TORUS', 'ELLIPSE_TORUS')
if is_torus:
new_mesh = create_torus_mesh_data(shape_type, props)
if new_mesh:
old_mesh = obj.data
obj.data = new_mesh
if old_mesh and old_mesh.name == MESH_NAME:
bpy.data.meshes.remove(old_mesh)
return
if obj.data.name != MESH_NAME:
old_mesh = obj.data
obj.data = bpy.data.meshes.new(MESH_NAME)
if old_mesh and old_mesh.users == 0:
bpy.data.meshes.remove(old_mesh)
mesh = obj.data
bm = bmesh.new()
is_line_shape = shape_type in {'CYLINDER', 'CONE', 'TRUNCATED_CONE', 'SKEW_CONE'}
if is_line_shape:
p1, p2 = mathutils.Vector(props.p1), mathutils.Vector(props.p2)
r1, r2 = props.r1, props.r2
segments = max(3, props.segments)
axis = p1 - p2
if axis.length < 0.0001: axis = mathutils.Vector((0, 0, 1))
axis_norm = axis.normalized()
up = mathutils.Vector((0, 0, 1)) if abs(axis_norm.z) < 0.99 else mathutils.Vector((1, 0, 0))
u = up.cross(axis_norm).normalized()
v = axis_norm.cross(u).normalized()
verts_p1, verts_p2 = [], []
for i in range(segments):
angle = 2.0 * math.pi * i / segments
cos_a, sin_a = math.cos(angle), math.sin(angle)
verts_p2.append(bm.verts.new(p2 + (u * cos_a + v * sin_a) * r2))
verts_p1.append(bm.verts.new(p1 + (u * cos_a + v * sin_a) * r1))
for i in range(segments):
next_i = (i + 1) % segments
bm.faces.new((verts_p2[i], verts_p2[next_i], verts_p1[next_i], verts_p1[i]))
if props.cap_bottom and r2 > 0.001: bm.faces.new(reversed(verts_p2))
if props.cap_top and r1 > 0.001: bm.faces.new(verts_p1)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
else:
# 他の図形に分割数 (segments) を反映
seg = max(3, props.segments)
if shape_type == 'SQUARE_FACE':
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=0.5)
elif shape_type == 'CIRCLE_FACE':
bmesh.ops.create_circle(bm, cap_ends=True, cap_tris=True, radius=0.5, segments=seg)
elif shape_type == 'ELLIPSE_FACE':
bmesh.ops.create_circle(bm, cap_ends=True, cap_tris=True, radius=0.5, segments=seg)
for v in bm.verts: v.co.y *= 0.5
elif shape_type == 'MONKEY':
bmesh.ops.create_monkey(bm)
elif shape_type == 'UV_SPHERE':
bmesh.ops.create_uvsphere(bm, u_segments=seg, v_segments=max(3, seg//2), radius=0.5)
elif shape_type == 'CUBE':
bmesh.ops.create_cube(bm, size=1.0)
elif shape_type == 'ICO_SPHERE':
# Subdivisions は重くなりすぎるのを防ぐため、分割数をスケール変換
subdiv = max(1, min(6, seg // 10))
bmesh.ops.create_icosphere(bm, subdivisions=subdiv, radius=0.5)
elif shape_type == 'GRID':
grid_seg = max(2, seg)
bmesh.ops.create_grid(bm, x_segments=grid_seg, y_segments=grid_seg, size=1.0)
bm.to_mesh(mesh)
bm.free()
mesh.update()
# =========================================================
# プロパティグループ定義
# =========================================================
class AddonProperties(bpy.types.PropertyGroup):
center_x: bpy.props.FloatProperty(name="Center X", default=0.0)
center_y: bpy.props.FloatProperty(name="Center Y", default=0.0)
center_z: bpy.props.FloatProperty(name="Center Z", default=0.0)
view_width: bpy.props.FloatProperty(name="Width", default=0.0)
view_height: bpy.props.FloatProperty(name="Height", default=0.0)
view_dist: bpy.props.FloatProperty(name="Distance", default=0.0)
is_persp: bpy.props.BoolProperty(default=True)
col_name: bpy.props.StringProperty(name="Collection", default=DEFAULT_COLLECTION_NAME)
subcol_name: bpy.props.StringProperty(name="Sub Collection", default=DEFAULT_SUBCOLLECTION_NAME)
obj_name: bpy.props.StringProperty(name="Object Name", default=DEFAULT_OBJECT_NAME)
shape_enum: bpy.props.EnumProperty(
name="Shape",
items=[
('CYLINDER', "Cylinder (円柱)", ""),
('CONE', "Cone (円錐)", ""),
('TRUNCATED_CONE', "Truncated Cone (円錐台)", ""),
('SKEW_CONE', "Skew Cone (斜め円錐台)", ""),
('SQUARE_FACE', "Square Face (正方形の面)", ""),
('CIRCLE_FACE', "Circle Face (円の面)", ""),
('ELLIPSE_FACE', "Ellipse Face (楕円の面)", ""),
('SQUARE_TORUS', "Square Torus (正方形トーラス)", ""),
('CIRCLE_TORUS', "Circle Torus (円形トーラス)", ""),
('ELLIPSE_TORUS', "Ellipse Torus (楕円形トーラス)", ""),
('MONKEY', "Monkey (モンキー)", ""),
('UV_SPHERE', "UV Sphere (UV球)", ""),
('CUBE', "Cube (立方体)", ""),
('ICO_SPHERE', "Ico Sphere (ICO球)", ""),
('GRID', "Grid (グリッド)", ""),
],
default='CYLINDER',
update=update_shape_enum
)
# 色とトランスフォーム
color: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', size=4, default=DEFAULT_COLOR, min=0.0, max=1.0, update=update_controlled_object)
size: bpy.props.FloatProperty(name="Size", default=1.0, min=0.1, update=update_controlled_object)
location: bpy.props.FloatVectorProperty(name="Location", default=(0,0,0), subtype='TRANSLATION', update=update_controlled_object)
rotation: bpy.props.FloatVectorProperty(name="Rotation", default=(0,0,0), subtype='EULER', update=update_controlled_object)
# 汎用:分割数 (最大512まで設定可能)
segments: bpy.props.IntProperty(name="分割数 / 頂点数", default=DEFAULT_SEGMENTS, min=3, max=512, update=update_controlled_object)
# 2点指定用パラメータ
p1: bpy.props.FloatVectorProperty(name="上面中心", default=(DEFAULT_P1_X, DEFAULT_P1_Y, DEFAULT_P1_Z), update=update_controlled_object)
p2: bpy.props.FloatVectorProperty(name="下面中心", default=(DEFAULT_P2_X, DEFAULT_P2_Y, DEFAULT_P2_Z), update=update_controlled_object)
r1: bpy.props.FloatProperty(name="上面半径", default=DEFAULT_R1, min=0.0, update=update_controlled_object)
r2: bpy.props.FloatProperty(name="下面半径", default=DEFAULT_R2, min=0.0, update=update_controlled_object)
cap_top: bpy.props.BoolProperty(name="上面を塞ぐ", default=True, update=update_controlled_object)
cap_bottom: bpy.props.BoolProperty(name="下面を塞ぐ", default=True, update=update_controlled_object)
torus_pipe_radius: bpy.props.FloatProperty(name="Pipe Thickness", default=0.1, min=0.01, update=update_controlled_object)
image_filepath: bpy.props.StringProperty(name="Image Path", subtype='FILE_PATH', default="")
# =========================================================
# オペレーター (機能)
# =========================================================
def get_or_create_collection(context, main_name, sub_name):
main_col = bpy.data.collections.get(main_name)
if not main_col:
main_col = bpy.data.collections.new(main_name)
context.scene.collection.children.link(main_col)
sub_col = bpy.data.collections.get(sub_name)
if not sub_col:
sub_col = bpy.data.collections.new(sub_name)
main_col.children.link(sub_col)
return sub_col
class OT_MoveToCenter(bpy.types.Operator):
bl_idname = f"{PREFIX}.move_to_center"
bl_label = "選択オブジェクトを画面中央へ"
def execute(self, context):
obj = context.active_object
if obj and context.region_data:
obj.location = context.region_data.view_location
self.report({'INFO'}, "画面中央に移動しました")
return {'FINISHED'}
class OT_UpdateViewInfo(bpy.types.Operator):
bl_idname = f"{PREFIX}.update_view_info"
bl_label = "画面情報を更新"
def execute(self, context):
props = getattr(context.scene, PROP_NAME)
region = context.region
region_data = context.region_data
space = context.space_data
if not region_data: return {'CANCELLED'}
loc = region_data.view_location
props.center_x, props.center_y, props.center_z = loc.x, loc.y, loc.z
props.view_dist = region_data.view_distance
props.is_persp = (region_data.view_perspective == 'PERSP')
if props.is_persp:
lens = space.camera.data.lens if (space.camera and space.region_3d.view_perspective == 'CAMERA') else space.lens
sensor = space.camera.data.sensor_width if (space.camera and space.region_3d.view_perspective == 'CAMERA') else 32.0
fov = 2 * math.atan(sensor / (2 * lens))
props.view_width = 2 * props.view_dist * math.tan(fov / 2)
props.view_height = props.view_width * (region.height / region.width)
else:
props.view_width = props.view_dist * 2
props.view_height = props.view_width * (region.height / region.width)
return {'FINISHED'}
class OT_CopyInfo(bpy.types.Operator):
bl_idname = f"{PREFIX}.copy_info"
bl_label = "情報をコピー"
def execute(self, context):
props = getattr(context.scene, PROP_NAME)
text = (
f"中央位置: {props.center_x:.1f}, {props.center_y:.1f}, {props.center_z:.1f}\n"
f"水平サイズ: {props.view_width:.1f}\n"
f"上下サイズ: {props.view_height:.1f}\n"
)
text += f"視座距離: {props.view_dist:.1f}" if props.is_persp else "視座距離: (平行投影)"
context.window_manager.clipboard = text
self.report({'INFO'}, "情報をクリップボードにコピーしました")
return {'FINISHED'}
class OT_Create(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_object"
bl_label = "作成・同期"
def execute(self, context):
props = getattr(context.scene, PROP_NAME)
for obj in context.scene.objects:
if obj.get(CTRL_FLAG) == True: obj[CTRL_FLAG] = False
mesh = bpy.data.meshes.new(MESH_NAME)
obj = bpy.data.objects.new(props.obj_name, mesh)
col = get_or_create_collection(context, props.col_name, props.subcol_name)
col.objects.link(obj)
obj[CTRL_FLAG] = True
context.view_layer.objects.active = obj
obj.select_set(True)
update_controlled_object(props, context)
self.report({'INFO'}, "オブジェクトを生成しました")
return {'FINISHED'}
class OT_Detach(bpy.types.Operator):
bl_idname = f"{PREFIX}.detach_object"
bl_label = "切り離す"
def execute(self, context):
obj = context.active_object
if not obj or obj.get(CTRL_FLAG) != True:
return {'CANCELLED'}
obj[CTRL_FLAG] = False
props = getattr(context.scene, PROP_NAME)
if obj.data.materials:
live_mat = obj.data.materials[0]
if live_mat and live_mat.name == MAT_NAME:
c = props.color
rgb_str = f"{round(c[0],2)}_{round(c[1],2)}_{round(c[2],2)}"
new_mat_name = f"{PREFIX}_Mat_{rgb_str}"
exist_mat = bpy.data.materials.get(new_mat_name)
if exist_mat: obj.data.materials[0] = exist_mat
else:
new_mat = live_mat.copy()
new_mat.name = new_mat_name
obj.data.materials[0] = new_mat
if obj.data.name == MESH_NAME:
new_mesh = obj.data.copy()
new_mesh.name = f"Mesh_{obj.name}"
obj.data = new_mesh
self.report({'INFO'}, "完全に切り離しました")
return {'FINISHED'}
class OT_PasteText(bpy.types.Operator):
bl_idname = f"{PREFIX}.paste_text"
bl_label = "Paste Clipboard as Text"
def execute(self, context):
props = getattr(context.scene, PROP_NAME)
txt = context.window_manager.clipboard
if not txt or txt.strip() == "": return {'CANCELLED'}
font_curve = bpy.data.curves.new(name="Text_Curve", type='FONT')
font_curve.body = txt
font_curve.extrude = 0.05
obj = bpy.data.objects.new(f"{props.obj_name}_Text", font_curve)
get_or_create_collection(context, props.col_name, props.subcol_name).objects.link(obj)
obj.location = props.location
obj.rotation_euler = props.rotation
obj.scale = (props.size, props.size, props.size)
return {'FINISHED'}
def load_image_helper(self, context, mode):
props = getattr(context.scene, PROP_NAME)
path = bpy.path.abspath(props.image_filepath)
if not path: return {'CANCELLED'}
try:
img = bpy.data.images.load(path)
col = get_or_create_collection(context, props.col_name, props.subcol_name)
if mode in ('REF', 'BG'):
empty = bpy.data.objects.new(f"{props.obj_name}_{mode}", None)
col.objects.link(empty)
empty.empty_display_type = 'IMAGE'
empty.data = img
if mode == 'BG': empty.empty_image_depth = 'BACK'
empty.location, empty.rotation_euler = props.location, props.rotation
elif mode == 'PLANE':
mesh = bpy.data.meshes.new("ImgPlane")
obj = bpy.data.objects.new(f"{props.obj_name}_Plane", mesh)
col.objects.link(obj)
aspect = img.size[0] / img.size[1] if img.size[1] > 0 else 1.0
bm = bmesh.new()
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=0.5)
for v in bm.verts: v.co.x *= aspect
bm.to_mesh(mesh)
bm.free()
mat = bpy.data.materials.new("ImgMat")
mat.use_nodes = True
tex = mat.node_tree.nodes.new("ShaderNodeTexImage")
tex.image = img
mat.node_tree.links.new(tex.outputs['Color'], mat.node_tree.nodes["Principled BSDF"].inputs['Base Color'])
obj.data.materials.append(mat)
obj.location, obj.rotation_euler = props.location, props.rotation
elif mode == 'CAM':
cam = context.scene.camera
if not cam:
cam = bpy.data.objects.new("Camera", bpy.data.cameras.new("Camera"))
context.scene.collection.objects.link(cam)
context.scene.camera = cam
cam.data.show_background_images = True
cam.data.background_images.new().image = img
except Exception as e:
self.report({'ERROR'}, str(e))
return {'FINISHED'}
class OT_AddImageRef(bpy.types.Operator): bl_idname, bl_label, execute = f"{PREFIX}.add_img_ref", "参照画像", lambda self, context: load_image_helper(self, context, 'REF')
class OT_AddImageBg(bpy.types.Operator): bl_idname, bl_label, execute = f"{PREFIX}.add_img_bg", "背景画像", lambda self, context: load_image_helper(self, context, 'BG')
class OT_AddImagePlane(bpy.types.Operator): bl_idname, bl_label, execute = f"{PREFIX}.add_img_plane", "画像プレーン", lambda self, context: load_image_helper(self, context, 'PLANE')
class OT_AddImageCam(bpy.types.Operator): bl_idname, bl_label, execute = f"{PREFIX}.add_img_cam", "カメラ背景", lambda self, context: load_image_helper(self, context, 'CAM')
class OT_ResetDefaults(bpy.types.Operator):
bl_idname = f"{PREFIX}.reset_defaults"
bl_label = "初期値に戻す"
def execute(self, context):
props = getattr(context.scene, PROP_NAME)
for k in props.bl_rna.properties.keys():
if k not in {"rna_type", "image_filepath"}: props.property_unset(k)
return {'FINISHED'}
class OT_DisableAddon(bpy.types.Operator):
bl_idname = f"{PREFIX}.disable_addon"
bl_label = "アドオンを完全に無効化"
def execute(self, context):
try:
addon_utils.disable(__name__)
self.report({'INFO'}, f"[{ADDON_TITLE}] を無効化しました。")
except:
self.report({'ERROR'}, "無効化に失敗しました。")
return {'FINISHED'}
# =========================================================
# UI パネル定義
# =========================================================
class PT_Base(bpy.types.Panel):
bl_space_type, bl_region_type, bl_category = 'VIEW_3D', 'UI', ADDON_TITLE
class PT_1_View(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_1", "1. 視点・座標制御", 0
def draw(self, context):
props = getattr(context.scene, PROP_NAME)
layout = self.layout
layout.operator(f"{PREFIX}.move_to_center", icon='VIEWZOOM')
box = layout.box()
row = box.row()
row.label(text=f"中央: ({props.center_x:.1f}, {props.center_y:.1f}, {props.center_z:.1f})")
row.operator(f"{PREFIX}.update_view_info", text="", icon='FILE_REFRESH')
box.label(text=f"水平: {props.view_width:.1f} / 上下: {props.view_height:.1f}")
box.label(text=f"距離: {props.view_dist:.1f} (透視投影)" if props.is_persp else "距離: --- (平行投影)")
box.operator(f"{PREFIX}.copy_info", icon='COPYDOWN')
class PT_2_Generate(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_2", "2. 生成・切り離し", 1
def draw(self, context):
props = getattr(context.scene, PROP_NAME)
layout = self.layout
layout.prop(props, "shape_enum", text="形状選択")
col = layout.column(align=True)
col.scale_y = 1.1
col.operator(f"{PREFIX}.create_object", text="作成 / 同期", icon='ADD')
col.operator(f"{PREFIX}.detach_object", text="アドオンから切り離す", icon='UNLINKED')
class PT_3_Realtime(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_3", "3. リアルタイム調整", 2
def draw(self, context):
props = getattr(context.scene, PROP_NAME)
layout = self.layout
box = layout.box()
box.prop(props, "color")
is_line = props.shape_enum in {'CYLINDER', 'CONE', 'TRUNCATED_CONE', 'SKEW_CONE'}
is_torus = props.shape_enum in {'SQUARE_TORUS', 'CIRCLE_TORUS', 'ELLIPSE_TORUS'}
if is_line:
box.label(text="[ 2点軸パラメータ ]", icon='TRACKING')
box.prop(props, "p1")
box.prop(props, "p2")
row = box.row()
row.prop(props, "r1")
row.prop(props, "r2")
box.prop(props, "segments")
row2 = box.row()
row2.prop(props, "cap_top")
row2.prop(props, "cap_bottom")
else:
box.label(text="[ トランスフォーム ]", icon='OUTLINER_OB_EMPTY')
box.prop(props, "size")
box.prop(props, "location")
box.prop(props, "rotation")
if is_torus: box.prop(props, "torus_pipe_radius")
# 分割数を持たない一部の図形以外では「分割数」を表示
if props.shape_enum not in {'CUBE', 'MONKEY', 'SQUARE_FACE', 'SQUARE_TORUS'}:
box.prop(props, "segments")
class PT_4_IO(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_4", "4. テキスト & 画像追加", 3
def draw(self, context):
props = getattr(context.scene, PROP_NAME)
layout = self.layout
layout.operator(f"{PREFIX}.paste_text", text="テキストをペースト", icon='PASTEDOWN')
box = layout.box()
box.prop(props, "image_filepath", text="画像パス")
grid = box.grid_flow(columns=2, align=True)
grid.operator(f"{PREFIX}.add_img_ref", icon='EMPTY_DATA')
grid.operator(f"{PREFIX}.add_img_bg", icon='IMAGE_BACKGROUND')
grid.operator(f"{PREFIX}.add_img_plane", icon='MESH_PLANE')
grid.operator(f"{PREFIX}.add_img_cam", icon='OUTLINER_OB_CAMERA')
class PT_5_Settings(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_5", "5. 初期設定", 4
def draw(self, context):
props = getattr(context.scene, PROP_NAME)
layout = self.layout
layout.prop(props, "col_name")
layout.prop(props, "subcol_name")
layout.prop(props, "obj_name")
layout.operator(f"{PREFIX}.reset_defaults", icon='LOOP_BACK')
class PT_6_Link(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_6", "6. リンク", 5
def draw(self, context):
self.layout.operator("wm.url_open", text="公式ドキュメント", icon='URL').url = "<https://www.blender.org/>"
class PT_7_Delete(PT_Base):
bl_idname, bl_label, bl_order = f"VIEW3D_PT_{PREFIX}_7", "7. アドオン削除パネル", 6
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
self.layout.label(text="このアドオンを直ちに無効化します", icon='INFO')
self.layout.operator(f"{PREFIX}.disable_addon", text="アドオンを無効化する", icon='CANCEL')
# =========================================================
# 登録処理
# =========================================================
classes = (
AddonProperties,
OT_MoveToCenter, OT_UpdateViewInfo, OT_CopyInfo,
OT_Create, OT_Detach, OT_PasteText,
OT_AddImageRef, OT_AddImageBg, OT_AddImagePlane, OT_AddImageCam,
OT_ResetDefaults, OT_DisableAddon,
PT_1_View, PT_2_Generate, PT_3_Realtime, PT_4_IO, PT_5_Settings, PT_6_Link, PT_7_Delete,
)
def register():
for cls in classes: bpy.utils.register_class(cls)
setattr(bpy.types.Scene, PROP_NAME, bpy.props.PointerProperty(type=AddonProperties))
def unregister():
for cls in reversed(classes): bpy.utils.unregister_class(cls)
if hasattr(bpy.types.Scene, PROP_NAME): delattr(bpy.types.Scene, PROP_NAME)
if __name__ == "__main__":
register()