ダミー人形
import bpy
from bpy.types import Operator, Panel
from bpy.props import IntProperty
# =========================================================
# アドオン宣言
# =========================================================
bl_info = {
"name": "Addon Code Container",
"author": "AI",
"version": (4, 1, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar > Code Container",
"description": "ソースコード内に貼り付けた複数のアドオンコードを管理するコンテナ",
"category": "Development",
}
PREFIX_VAL = "zionad_container"
ADDON_TITLE = "Code Container"
# =========================================================
# 【手動設定 & コード貼り付けエリア】
# =========================================================
# ---------------------------------
# 1番登録アドオン
TITLE_1 = "1番のアドオン名をここに入力"
CODE_1 = '''
'''
# ---------------------------------
# ---------------------------------
# 2番登録アドオン
TITLE_2 = ""
CODE_2 = '''
'''
# ---------------------------------
# ---------------------------------
# 3番登録アドオン
TITLE_3 = ""
CODE_3 = '''
'''
# ---------------------------------
# ---------------------------------
# 4番登録アドオン
TITLE_4 = ""
CODE_4 = '''
'''
# ---------------------------------
# ---------------------------------
# 5番登録アドオン
TITLE_5 = ""
CODE_5 = '''
'''
# ---------------------------------
# ---------------------------------
# 6番登録アドオン
TITLE_6 = ""
CODE_6 = '''
'''
# ---------------------------------
# ---------------------------------
# 7番登録アドオン
TITLE_7 = ""
CODE_7 = '''
'''
# ---------------------------------
# ---------------------------------
# 8番登録アドオン
TITLE_8 = ""
CODE_8 = '''
'''
# ---------------------------------
# ---------------------------------
# 9番登録アドオン
TITLE_9 = ""
CODE_9 = '''
'''
# ---------------------------------
# ---------------------------------
# 10番登録アドオン
TITLE_10 = "ダミー人形"
CODE_10 = '''
bl_info = {
"name": "zionad_Dummy Sightline Gen",
"author": "Your Name",
"version": (1, 4, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "Grid格子設定の追加、現在の設定を初期設定コードとしてコピーする機能の追加",
"category": "3D View",
}
# 【作成日】 2024年5月22日 17:00
import bpy
import bmesh
import math
import mathutils
import webbrowser
# ===================================
# 【初期設定・定数定義】 (1行ずつ定義)
# ===================================
ADDON_TITLE = "Dummy Sightline Gen"
PREFIX = "dummy_sight"
COLLECTION_MAIN = "Dummy_Circle_Sightline_Group"
OBJ_NAME_ROOT = "Dummy_Root"
OBJ_NAME_DUMMY = "Dummy"
OBJ_NAME_TORUS = "Torus"
OBJ_NAME_SIGHTLINE = "Sightline_Arrow"
# 寸法初期値 (画像ベース)
INIT_TORUS_RADIUS = 10.00
INIT_SIGHTLINE_LENGTH = 10.00
# 太さ初期値 (画像ベース)
INIT_DUMMY_THICKNESS = 0.40
INIT_TORUS_THICKNESS = 0.30
INIT_SIGHTLINE_THICKNESS = 0.40
# カラー初期値 (R, G, B, Alpha) - 画像の色味に近似
INIT_COLOR_DUMMY = (0.05, 0.92, 0.06, 1.00)
INIT_COLOR_TORUS = (0.91, 0.86, 0.36, 1.00)
INIT_COLOR_SIGHT = (0.12, 0.00, 0.06, 1.00)
INIT_SIGHTLINE_EMISSION = 5.0
LINK_NAME_1 = "ダミー人形 20260713"
LINK_URL_1 = "<https://note.com/zionadmillion/n/ndebba8a8d691>"
# ===================================
# 【ビュー情報取得 関数】
# ===================================
def get_rv3d(context):
for a in context.window.screen.areas:
if a.type == 'VIEW_3D':
return a.spaces.active.region_3d
return None
def get_view_info_data(context):
rv3d = None
area = None
space = None
for a in context.window.screen.areas:
if a.type == 'VIEW_3D':
rv3d = a.spaces.active.region_3d
area = a
space = a.spaces.active
break
if not rv3d or not area or not space: return None
mode = rv3d.view_perspective
target_pos = rv3d.view_location
view_dist = rv3d.view_distance
cam_pos = rv3d.view_matrix.inverted().translation
view_dir = rv3d.view_rotation @ mathutils.Vector((0.0, 0.0, -1.0))
aspect = area.height / area.width if area.width > 0 else 1.0
sensor = 32.0
if mode == 'PERSP':
proj_text = "透視投影"
is_persp = True
elif mode == 'ORTHO':
proj_text = "平行投影"
is_persp = False
elif mode == 'CAMERA':
proj_text = "カメラビュー"
is_persp = True
else:
proj_text = "不明"
is_persp = False
if is_persp:
fov_h = 2.0 * math.atan(sensor / (2.0 * space.lens))
w = 2.0 * view_dist * math.tan(fov_h / 2.0)
h = w * aspect
else:
w = view_dist * (sensor / space.lens)
h = w * aspect
return {
'proj_text': proj_text,
'cam_pos': cam_pos,
'view_dir': view_dir,
'target_pos': target_pos,
'dist': view_dist,
'w': w,
'h': h,
'is_persp': is_persp
}
# ===================================
# 【メッシュ生成 ヘルパー関数】
# ===================================
def add_cylinder_between(bm, p1, p2, radius):
p1 = mathutils.Vector(p1)
p2 = mathutils.Vector(p2)
v = p2 - p1
dist = v.length
if dist < 0.0001: return
rot = v.to_track_quat('Z', 'Y').to_matrix().to_4x4()
loc = mathutils.Matrix.Translation((p1 + p2) / 2.0)
bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=radius, radius2=radius, depth=dist, matrix=loc @ rot)
def rebuild_dummy_mesh(bm, scale, dummy_thickness):
R = INIT_TORUS_RADIUS * scale
Z_c = 1.0 * scale
pelvis = (0, 0, Z_c)
neck = (0, 0, Z_c + R * 0.4)
head = (0, 0, Z_c + R * 0.55)
r_hand = (R, 0, Z_c + R * 0.25)
l_hand = (-R, 0, Z_c + R * 0.25)
r_foot = (R * math.sin(math.radians(40)), 0, Z_c - R * math.cos(math.radians(40)))
l_foot = (-R * math.sin(math.radians(40)), 0, Z_c - R * math.cos(math.radians(40)))
mat_head = mathutils.Matrix.Translation(head)
head_size = dummy_thickness * scale * 2.0
bmesh.ops.create_uvsphere(bm, u_segments=16, v_segments=8, radius=head_size, matrix=mat_head)
thickness = dummy_thickness * scale
add_cylinder_between(bm, pelvis, neck, thickness * 1.5)
add_cylinder_between(bm, neck, r_hand, thickness)
add_cylinder_between(bm, neck, l_hand, thickness)
add_cylinder_between(bm, pelvis, r_foot, thickness)
add_cylinder_between(bm, pelvis, l_foot, thickness)
def rebuild_torus_mesh(bm, scale, torus_thickness):
r_major = INIT_TORUS_RADIUS * scale
r_minor = torus_thickness * scale
segments_major = 48
segments_minor = 12
verts = []
for i in range(segments_major):
u = i * 2.0 * math.pi / segments_major
cos_u = math.cos(u)
sin_u = math.sin(u)
row_verts = []
for j in range(segments_minor):
v = j * 2.0 * math.pi / segments_minor
cos_v = math.cos(v)
sin_v = math.sin(v)
x = (r_major + r_minor * cos_v) * cos_u
y = (r_major + r_minor * cos_v) * sin_u
z = r_minor * sin_v
px = x
py = -z
pz = y + (1.0 * scale)
row_verts.append(bm.verts.new((px, py, pz)))
verts.append(row_verts)
bm.verts.ensure_lookup_table()
for i in range(segments_major):
i_next = (i + 1) % segments_major
for j in range(segments_minor):
j_next = (j + 1) % segments_minor
bm.faces.new((verts[i][j], verts[i_next][j], verts[i_next][j_next], verts[i][j_next]))
def rebuild_sightline_mesh(bm, scale, length, sight_thickness):
R = INIT_TORUS_RADIUS * scale
Z_c = 1.0 * scale
head = mathutils.Vector((0, 0.15 * scale, Z_c + R * 0.55))
thickness = sight_thickness * scale
cone_length = thickness * 15.0
rot = mathutils.Matrix.Rotation(-math.pi/2, 4, 'X')
loc_cyl = mathutils.Matrix.Translation(head + mathutils.Vector((0, length / 2.0, 0)))
bmesh.ops.create_cone(bm, cap_ends=True, segments=8, radius1=thickness, radius2=thickness, depth=length, matrix=loc_cyl @ rot)
loc_tip = mathutils.Matrix.Translation(head + mathutils.Vector((0, length + cone_length / 2.0, 0)))
bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=thickness * 3.0, radius2=0.0, depth=cone_length, matrix=loc_tip @ rot)
# ===================================
# 【リアルタイム更新 処理】
# ===================================
def update_models(self, context):
if self.is_detached: return
obj_dummy = bpy.data.objects.get(OBJ_NAME_DUMMY)
obj_torus = bpy.data.objects.get(OBJ_NAME_TORUS)
obj_sight = bpy.data.objects.get(OBJ_NAME_SIGHTLINE)
scale = self.torus_radius / INIT_TORUS_RADIUS
if obj_dummy and obj_dummy.data:
bm = bmesh.new(); rebuild_dummy_mesh(bm, scale, self.dummy_thickness); bm.to_mesh(obj_dummy.data); bm.free()
if obj_torus and obj_torus.data:
bm = bmesh.new(); rebuild_torus_mesh(bm, scale, self.torus_thickness); bm.to_mesh(obj_torus.data); bm.free()
if obj_sight and obj_sight.data:
bm = bmesh.new(); rebuild_sightline_mesh(bm, scale, self.sight_length, self.sight_thickness); bm.to_mesh(obj_sight.data); bm.free()
context.view_layer.update()
def update_materials(self, context):
if self.is_detached: return
def set_mat_color(mat_name, color_val, is_emission=False):
mat = bpy.data.materials.get(mat_name)
if mat and mat.use_nodes:
principled = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None)
if principled:
principled.inputs['Base Color'].default_value = color_val
if 'Alpha' in principled.inputs:
principled.inputs['Alpha'].default_value = color_val[3]
if is_emission:
if 'Emission Color' in principled.inputs:
principled.inputs['Emission Color'].default_value = (color_val[0], color_val[1], color_val[2], 1.0)
principled.inputs['Emission Strength'].default_value = INIT_SIGHTLINE_EMISSION
set_mat_color(f"{PREFIX}_Mat_Dummy", self.color_dummy)
set_mat_color(f"{PREFIX}_Mat_Torus", self.color_torus)
set_mat_color(f"{PREFIX}_Mat_Sight", self.color_sight, is_emission=True)
# ===================================
# 【プロパティ定義】
# ===================================
class DummySight_Props(bpy.types.PropertyGroup):
torus_radius: bpy.props.FloatProperty(name="基準サイズ", default=INIT_TORUS_RADIUS, min=0.1, update=update_models)
sight_length: bpy.props.FloatProperty(name="視線の長さ", default=INIT_SIGHTLINE_LENGTH, min=0.1, update=update_models)
dummy_thickness: bpy.props.FloatProperty(name="ダミーの太さ", default=INIT_DUMMY_THICKNESS, min=0.01, update=update_models)
torus_thickness: bpy.props.FloatProperty(name="トーラスの太さ", default=INIT_TORUS_THICKNESS, min=0.01, update=update_models)
sight_thickness: bpy.props.FloatProperty(name="視線の太さ", default=INIT_SIGHTLINE_THICKNESS, min=0.001, update=update_models)
is_detached: bpy.props.BoolProperty(name="切り離しフラグ", default=False)
color_dummy: bpy.props.FloatVectorProperty(name="ダミーの色", subtype='COLOR', size=4, default=INIT_COLOR_DUMMY, min=0.0, max=1.0, update=update_materials)
color_torus: bpy.props.FloatVectorProperty(name="トーラスの色", subtype='COLOR', size=4, default=INIT_COLOR_TORUS, min=0.0, max=1.0, update=update_materials)
color_sight: bpy.props.FloatVectorProperty(name="視線の色", subtype='COLOR', size=4, default=INIT_COLOR_SIGHT, min=0.0, max=1.0, update=update_materials)
custom_grid_scale: bpy.props.FloatProperty(name="Grid指定値", default=10.0, min=0.001)
# ===================================
# 【オペレーター群】
# ===================================
class OT_reset_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_reset_view"
bl_label = "選択オブジェクトを画面中央にする"
def execute(self, context):
rv3d = get_rv3d(context)
if not rv3d: return {'CANCELLED'}
target = context.active_object
rv3d.view_location = target.matrix_world.to_translation() if target else rv3d.view_location.copy()
rv3d.view_distance = max(target.dimensions.length * 1.5, 10.0) if target else 30.0
context.view_layer.update()
return {'FINISHED'}
class OT_update_info(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_update_info"
bl_label = "更新"
def execute(self, context):
for area in context.screen.areas:
if area.type == 'VIEW_3D': area.tag_redraw()
return {'FINISHED'}
class OT_copy_info(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_copy_info"
bl_label = "情報をコピー"
def execute(self, context):
info = get_view_info_data(context)
if not info: return {'CANCELLED'}
lines = [
f"投影モード: {info['proj_text']}",
f"架空の視座位置 X: {info['cam_pos'].x:.1f}, Y: {info['cam_pos'].y:.1f}, Z: {info['cam_pos'].z:.1f}",
f"画面中央への視線方向 X: {info['view_dir'].x:.1f}, Y: {info['view_dir'].y:.1f}, Z: {info['view_dir'].z:.1f}",
f"画面中央位置 X: {info['target_pos'].x:.1f}, Y: {info['target_pos'].y:.1f}, Z: {info['target_pos'].z:.1f}",
f"画面水平の大きさ: {info['w']:.1f}",
f"画面上下の大きさ: {info['h']:.1f}"
]
if info['is_persp']:
lines.append(f"視座からの距離: {info['dist']:.1f}")
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "ビュー情報をクリップボードにコピーしました")
return {'FINISHED'}
class OT_set_grid_scale(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_set_grid_scale"
bl_label = "Grid間隔を設定"
scale_val: bpy.props.FloatProperty(name="Scale")
use_custom: bpy.props.BoolProperty(name="カスタム値", default=False)
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
val = props.custom_grid_scale if self.use_custom else self.scale_val
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.overlay.grid_scale = val
self.report({'INFO'}, f"Grid間隔を {val} に設定しました")
return {'FINISHED'}
class OT_create_set(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_create_set"
bl_label = "セットを作成"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
props.is_detached = False
col = bpy.data.collections.get(COLLECTION_MAIN)
if not col:
col = bpy.data.collections.new(COLLECTION_MAIN)
context.scene.collection.children.link(col)
for name in [OBJ_NAME_ROOT, OBJ_NAME_DUMMY, OBJ_NAME_TORUS, OBJ_NAME_SIGHTLINE]:
obj = bpy.data.objects.get(name)
if obj: bpy.data.objects.remove(obj, do_unlink=True)
root = bpy.data.objects.new(OBJ_NAME_ROOT, None)
root.empty_display_type = 'PLAIN_AXES'
col.objects.link(root)
def create_obj(name, mat_name):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
col.objects.link(obj)
obj.parent = root
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
if not obj.data.materials:
obj.data.materials.append(mat)
return obj
create_obj(OBJ_NAME_DUMMY, f"{PREFIX}_Mat_Dummy")
create_obj(OBJ_NAME_TORUS, f"{PREFIX}_Mat_Torus")
create_obj(OBJ_NAME_SIGHTLINE, f"{PREFIX}_Mat_Sight")
update_models(props, context)
update_materials(props, context)
self.report({'INFO'}, "ダミー視線セットを作成しました")
return {'FINISHED'}
class OT_detach(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_detach"
bl_label = "アドオンから切り離し (一体化)"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
col = bpy.data.collections.get(COLLECTION_MAIN)
if not col or props.is_detached: return {'CANCELLED'}
import time
timestamp = int(time.time())
mesh_objs = [obj for obj in col.objects if obj.type == 'MESH']
root = next((obj for obj in col.objects if obj.name.startswith(OBJ_NAME_ROOT)), None)
for obj in mesh_objs:
mat_world = obj.matrix_world.copy()
obj.parent = None
obj.matrix_world = mat_world
if obj.data: obj.data = obj.data.copy()
for slot in obj.material_slots:
if slot.material: slot.material = slot.material.copy()
if root: bpy.data.objects.remove(root, do_unlink=True)
if mesh_objs:
bpy.ops.object.select_all(action='DESELECT')
for obj in mesh_objs: obj.select_set(True)
context.view_layer.objects.active = mesh_objs[0]
bpy.ops.object.join()
joined_obj = context.active_object
joined_obj.name = f"Dummy_Merged_{timestamp}"
col.name = f"{COLLECTION_MAIN}_{timestamp}"
props.is_detached = True
self.report({'INFO'}, "独立化し、1つのメッシュに結合しました")
return {'FINISHED'}
class OT_reset_props(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_reset_props"
bl_label = "初期値に戻す"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
props.torus_radius = INIT_TORUS_RADIUS
props.sight_length = INIT_SIGHTLINE_LENGTH
props.dummy_thickness = INIT_DUMMY_THICKNESS
props.torus_thickness = INIT_TORUS_THICKNESS
props.sight_thickness = INIT_SIGHTLINE_THICKNESS
props.color_dummy = INIT_COLOR_DUMMY
props.color_torus = INIT_COLOR_TORUS
props.color_sight = INIT_COLOR_SIGHT
root = bpy.data.objects.get(OBJ_NAME_ROOT)
if root:
root.location = (0,0,0)
root.rotation_euler = (0,0,0)
root.scale = (1,1,1)
return {'FINISHED'}
class OT_copy_settings(bpy.types.Operator):
bl_idname = f"object.{PREFIX}_copy_settings"
bl_label = "現在の設定を初期設定としてコピー"
bl_description = "現在の数値をPythonコードの初期設定形式でクリップボードにコピーします"
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
lines = [
"# ===================================",
"# 【初期設定・定数定義】 (1行ずつ定義)",
"# ===================================",
f'ADDON_TITLE = "{ADDON_TITLE}"',
f'PREFIX = "{PREFIX}"',
f'COLLECTION_MAIN = "{COLLECTION_MAIN}"',
f'OBJ_NAME_ROOT = "{OBJ_NAME_ROOT}"',
f'OBJ_NAME_DUMMY = "{OBJ_NAME_DUMMY}"',
f'OBJ_NAME_TORUS = "{OBJ_NAME_TORUS}"',
f'OBJ_NAME_SIGHTLINE = "{OBJ_NAME_SIGHTLINE}"',
"",
"# 寸法初期値 (画像ベース)",
f"INIT_TORUS_RADIUS = {props.torus_radius:.2f}",
f"INIT_SIGHTLINE_LENGTH = {props.sight_length:.2f}",
"",
"# 太さ初期値 (画像ベース)",
f"INIT_DUMMY_THICKNESS = {props.dummy_thickness:.2f}",
f"INIT_TORUS_THICKNESS = {props.torus_thickness:.2f}",
f"INIT_SIGHTLINE_THICKNESS = {props.sight_thickness:.2f}",
"",
"# カラー初期値 (R, G, B, Alpha) - 画像の色味に近似",
f"INIT_COLOR_DUMMY = ({props.color_dummy[0]:.2f}, {props.color_dummy[1]:.2f}, {props.color_dummy[2]:.2f}, {props.color_dummy[3]:.2f})",
f"INIT_COLOR_TORUS = ({props.color_torus[0]:.2f}, {props.color_torus[1]:.2f}, {props.color_torus[2]:.2f}, {props.color_torus[3]:.2f})",
f"INIT_COLOR_SIGHT = ({props.color_sight[0]:.2f}, {props.color_sight[1]:.2f}, {props.color_sight[2]:.2f}, {props.color_sight[3]:.2f})",
f"INIT_SIGHTLINE_EMISSION = {INIT_SIGHTLINE_EMISSION:.1f}",
"",
f'LINK_NAME_1 = "{LINK_NAME_1}"',
f'LINK_URL_1 = "{LINK_URL_1}"'
]
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "現在の設定をクリップボードにコピーしました")
return {'FINISHED'}
class OT_open_url(bpy.types.Operator):
bl_idname = f"wm.{PREFIX}_open_url"
bl_label = "リンクを開く"
url: bpy.props.StringProperty()
def execute(self, context):
if self.url: webbrowser.open(self.url)
return {'FINISHED'}
class OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX}_remove_addon"
bl_label = "アドオン削除"
def execute(self, context): unregister(); return {'FINISHED'}
# ===================================
# 【UIパネル】
# ===================================
class PT_view_info(bpy.types.Panel):
bl_idname = f"{PREFIX.upper()}_PT_view_info"
bl_label = "1. ビュー情報・画面操作"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_TITLE
def draw(self, context):
layout = self.layout
props = getattr(context.scene, f"{PREFIX}_props")
info = get_view_info_data(context)
layout.operator(f"view3d.{PREFIX}_reset_view", text="選択オブジェクトを画面中央にする", icon='ZOOM_ALL')
layout.separator()
box = layout.box()
row = box.row(align=True)
row.label(text="ビュー情報:")
row.operator(f"view3d.{PREFIX}_update_info", icon='FILE_REFRESH')
if info:
col = box.column(align=True)
col.label(text=f"投影モード: {info['proj_text']}")
col.label(text=f"架空の視座位置 X: {info['cam_pos'].x:.1f}, Y: {info['cam_pos'].y:.1f}, Z: {info['cam_pos'].z:.1f}")
col.label(text=f"画面中央への視線方向 X: {info['view_dir'].x:.1f}, Y: {info['view_dir'].y:.1f}, Z: {info['view_dir'].z:.1f}")
col.label(text=f"画面中央位置 X: {info['target_pos'].x:.1f}, Y: {info['target_pos'].y:.1f}, Z: {info['target_pos'].z:.1f}")
col.label(text=f"画面水平の大きさ: {info['w']:.1f}")
col.label(text=f"画面上下の大きさ: {info['h']:.1f}")
if info['is_persp']:
col.label(text=f"視座からの距離: {info['dist']:.1f}")
layout.separator()
layout.operator(f"view3d.{PREFIX}_copy_info", text="情報をコピー (1行ずつペースト)", icon='COPYDOWN')
else:
box.label(text="3Dビューが見つかりません")
layout.separator(factor=1.5)
# Grid格子 設定 UI
box_grid = layout.box()
box_grid.label(text="Grid格子 設定", icon='GRID')
row_preset = box_grid.row(align=True)
row_preset.operator(f"view3d.{PREFIX}_set_grid_scale", text="10").scale_val = 10.0
row_preset.operator(f"view3d.{PREFIX}_set_grid_scale", text="100").scale_val = 100.0
row_custom = box_grid.row(align=True)
row_custom.prop(props, "custom_grid_scale", text="")
row_custom.operator(f"view3d.{PREFIX}_set_grid_scale", text="指定値にする").use_custom = True
class PT_generate(bpy.types.Panel):
bl_idname = f"{PREFIX.upper()}_PT_generate"
bl_label = "2. セット生成・制御"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_TITLE
def draw(self, context):
layout = self.layout
props = getattr(context.scene, f"{PREFIX}_props")
row = layout.row(align=True)
row.scale_y = 1.2
row.operator(f"object.{PREFIX}_create_set", text="セットを作成", icon='MESH_MONKEY')
row.operator(f"object.{PREFIX}_detach", text="アドオンから切り離し", icon='UNLINKED')
layout.separator()
# --- 形状・太さ・カラー設定 UI ---
col_main = layout.column(align=True)
col_main.enabled = not props.is_detached
box_size = col_main.box()
col_s = box_size.column(align=True)
col_s.prop(props, "torus_radius")
col_s.prop(props, "sight_length")
col_main.separator()
box_thick = col_main.box()
col_t = box_thick.column(align=True)
col_t.prop(props, "dummy_thickness")
col_t.prop(props, "torus_thickness")
col_t.prop(props, "sight_thickness")
col_main.separator()
col_color = col_main.column(align=True)
col_color.label(text="ダミーの色:")
col_color.prop(props, "color_dummy", text="")
col_color.label(text="トーラスの色:")
col_color.prop(props, "color_torus", text="")
col_color.label(text="視線の色:")
col_color.prop(props, "color_sight", text="")
layout.separator()
root = bpy.data.objects.get(OBJ_NAME_ROOT)
if root and not props.is_detached:
box_tf = layout.box()
box_tf.label(text="設定値 (セット全体):", icon='ORIENTATION_GLOBAL')
col_tf = box_tf.column(align=True)
col_tf.prop(root, "location", text="位置 xyz")
col_tf.prop(root, "rotation_euler", text="回転 xyz")
col_tf.prop(root, "scale", text="サイズ xyz")
if props.is_detached:
layout.label(text="※切り離し済みのため操作不可", icon='INFO')
layout.separator()
# 初期化・設定コピー ボタン
col_reset = layout.column(align=True)
col_reset.operator(f"object.{PREFIX}_copy_settings", text="現在の設定を初期設定としてコピー", icon='COPYDOWN')
col_reset.separator()
col_reset.operator(f"object.{PREFIX}_reset_props", text="初期値に戻す", icon='LOOP_BACK')
class PT_system(bpy.types.Panel):
bl_idname = f"{PREFIX.upper()}_PT_system"
bl_label = "3. システム・リンク"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_TITLE
def draw(self, context):
layout = self.layout
box = layout.box()
box.label(text="外部リンク", icon='URL')
op = box.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_1, icon='HELP')
op.url = LINK_URL_1
layout.separator()
layout.operator(f"wm.{PREFIX}_remove_addon", text="アドオン削除 (登録解除)", icon='CANCEL')
# ===================================
# 【登録・削除処理】
# ===================================
classes = [
DummySight_Props,
OT_reset_view,
OT_update_info,
OT_copy_info,
OT_set_grid_scale,
OT_create_set,
OT_detach,
OT_reset_props,
OT_copy_settings,
OT_open_url,
OT_remove_addon,
PT_view_info,
PT_generate,
PT_system
]
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, f"{PREFIX}_props", bpy.props.PointerProperty(type=DummySight_Props))
def unregister():
if hasattr(bpy.types.Scene, f"{PREFIX}_props"): delattr(bpy.types.Scene, f"{PREFIX}_props")
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except: pass
if __name__ == "__main__":
try: unregister()
except: pass
register()
'''
# ---------------------------------
# =========================================================
# 内部処理 (リスト化)
# =========================================================
RAW_CODES = [
(TITLE_1, CODE_1), (TITLE_2, CODE_2), (TITLE_3, CODE_3),
(TITLE_4, CODE_4), (TITLE_5, CODE_5), (TITLE_6, CODE_6),
(TITLE_7, CODE_7), (TITLE_8, CODE_8), (TITLE_9, CODE_9),
(TITLE_10, CODE_10)
]
STORED_CODES = []
# コードが入力されているものだけをリストに登録
for i, (title_str, code_str) in enumerate(RAW_CODES):
if code_str and len(code_str.strip()) > 0:
addon_title = title_str if title_str.strip() else f"Unnamed_Addon_Slot_{i+1}"
STORED_CODES.append({
"name": addon_title,
"code": code_str,
"slot": i + 1
})
# =========================================================
# オペレーター (テキストデータ化 & エディタ表示)
# =========================================================
class ZIONAD_OT_OutputToTextEditor(Operator):
bl_idname = f"{PREFIX_VAL}.output_to_text_editor"
bl_label = "テキストエディタで開く"
bl_description = "このコードをテキストデータとして生成し、テキストエディタで開きます"
code_index: IntProperty()
def execute(self, context):
if self.code_index < 0 or self.code_index >= len(STORED_CODES):
self.report({'ERROR'}, "指定されたコードが見つかりません。")
return {'CANCELLED'}
item = STORED_CODES[self.code_index]
# タイトルに使えない文字(空白等)をアンダースコアにしてファイル名にする
safe_name = "".join([c if c.isalnum() else "_" for c in item["name"]])
text_name = f"{safe_name}.py"
code_text = item["code"]
if text_name in bpy.data.texts:
text_data = bpy.data.texts[text_name]
else:
text_data = bpy.data.texts.new(name=text_name)
text_data.use_fake_user = True
text_data.write(code_text.strip())
editor_found = False
for win in context.window_manager.windows:
for area in win.screen.areas:
if area.type == 'TEXT_EDITOR':
area.spaces.active.text = text_data
editor_found = True
break
if editor_found:
break
if editor_found:
self.report({'INFO'}, f"'{text_data.name}' をテキストエディタに表示しました。")
else:
self.report({'INFO'}, f"'{text_data.name}' を生成しました。画面をText Editorに切り替えて確認してください。")
return {'FINISHED'}
class ZIONAD_OT_RemoveAddon(Operator):
bl_idname = f"{PREFIX_VAL}.remove_addon"
bl_label = "Unregister Addon"
bl_description = "このコンテナアドオンの登録を解除し、UIから消去します"
def execute(self, context):
bpy.app.timers.register(unregister, first_interval=0.01)
return {'FINISHED'}
# =========================================================
# UI パネル
# =========================================================
class ZIONAD_PT_ContainerMain(Panel):
bl_label = "アドオン・コード・コンテナ"
bl_idname = "ZIONAD_PT_container_main"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_TITLE
def draw(self, context):
layout = self.layout
layout.label(text="内蔵されているコード一覧:")
box = layout.box()
if not STORED_CODES:
box.label(text="※ コードが登録されていません", icon='INFO')
box.label(text="ソースコードを編集して追加してください")
else:
for i, item in enumerate(STORED_CODES):
row = box.row(align=True)
row.label(text=f"[{item['slot']}] {item['name']}", icon='FILE_SCRIPT')
op = row.operator(ZIONAD_OT_OutputToTextEditor.bl_idname, text="開く", icon='TEXT')
op.code_index = i
class ZIONAD_PT_ContainerUninstall(Panel):
bl_label = "アドオン削除"
bl_idname = "ZIONAD_PT_container_uninstall"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_TITLE
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
box = layout.box()
box.label(text="このコンテナアドオンを消去します", icon='ERROR')
box.operator(ZIONAD_OT_RemoveAddon.bl_idname, icon='CANCEL')
# =========================================================
# 登録・解除
# =========================================================
classes = (
ZIONAD_OT_OutputToTextEditor,
ZIONAD_OT_RemoveAddon,
ZIONAD_PT_ContainerMain,
ZIONAD_PT_ContainerUninstall,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
try:
bpy.utils.unregister_class(cls)
except:
pass
if __name__ == "__main__":
register()
import bpy
import re
import base64
import os
from bpy.types import Operator, Panel, PropertyGroup, UIList
from bpy.props import StringProperty, PointerProperty, CollectionProperty, IntProperty
# =========================================================
# 【タイトル・接頭辞の定義】
# =========================================================
ADDON_TITLE = "Code Matrix Container"
PREFIX = "code_matrix_container"
# =========================================================
# アドオン宣言
# =========================================================
bl_info = {
"name": ADDON_TITLE,
"author": "AI",
"version": (3, 3, 4),
"blender": (5, 0, 0),
"location": f"View3D > Sidebar > {ADDON_TITLE}",
"description": "自己複製機能を持つアドオンコード管理コンテナ",
"category": "Development",
}
# --- EMBEDDED DATA START ---
# ここに内蔵コードが自動的に保存・展開されます (Base64辞書形式)
EMBEDDED_CODES = {}
# --- EMBEDDED DATA END ---
# =========================================================
# プロパティ・データ構造
# =========================================================
class ZIONAD_CodeItem(PropertyGroup):
name: StringProperty(name="アドオン別名", default="New Addon")
text_ptr: PointerProperty(name="Text Data", type=bpy.types.Text)
class ZIONAD_ContainerProperties(PropertyGroup):
items: CollectionProperty(type=ZIONAD_CodeItem)
active_index: IntProperty(default=0)
# =========================================================
# 自己複製・出力オペレーター
# =========================================================
class ZIONAD_OT_ExportSelf(Operator):
bl_idname = f"{PREFIX}.export_self"
bl_label = "コンテナ自身のコードを出力"
bl_description = "コンテナ自身のコードを、現在の一時リストを内蔵させた状態でテキストエディタに出力します"
def execute(self, context):
source_code = ""
# 1. 自身のソースコードを取得 (Generatedファイルは絶対に除外する)
if __file__ in bpy.data.texts and not __file__.endswith("_Generated.py"):
source_code = bpy.data.texts[__file__].as_string()
elif os.path.exists(__file__):
try:
with open(__file__, 'r', encoding='utf-8') as f:
source_code = f.read()
except Exception:
pass
# テキストエディタからの取得
if not source_code:
for text in bpy.data.texts:
if "Generated" in text.name: # 壊れた残骸を無視する安全装置
continue
content = text.as_string()
if "ZIONAD_ContainerProperties" in content and "ADDON_TITLE =" in content:
source_code = content
break
if not source_code and context.space_data and context.space_data.type == 'TEXT_EDITOR':
active_text = context.space_data.text
if active_text and "Generated" not in active_text.name:
content = active_text.as_string()
if "ZIONAD_ContainerProperties" in content:
source_code = content
if not source_code:
self.report({'ERROR'}, "自身のソースコードを取得できませんでした。")
return {'CANCELLED'}
# 2. 内蔵コードをBase64化して辞書データ文字列を作成
props = context.scene.zionad_container_props
embedded_dict = {}
for item in props.items:
if item.text_ptr:
raw_text = item.text_ptr.as_string()
encoded = base64.b64encode(raw_text.encode('utf-8')).decode('utf-8')
embedded_dict[item.name] = encoded
dict_str = "{\n"
for k, v in embedded_dict.items():
dict_str += f' "{k}": "{v}",\n'
dict_str += "}"
# 3. マーカー位置を検索して安全に置換
# (chr(35)は '#' のこと。コード内で直接マーカー文字列を作らないための工夫)
start_marker = chr(35) + " --- EMBEDDED DATA START ---"
end_marker = chr(35) + " --- EMBEDDED DATA END ---"
start_idx = source_code.find(start_marker)
end_idx = source_code.find(end_marker)
if start_idx == -1 or end_idx == -1:
self.report({'ERROR'}, "マーカーが見つかりません。")
return {'CANCELLED'}
try:
start_idx += len(start_marker)
new_source = (
source_code[:start_idx]
+ "\n# ここに内蔵コードが自動的に保存・展開されます (Base64辞書形式)\nEMBEDDED_CODES = "
+ dict_str
+ "\n"
+ source_code[end_idx:]
)
except Exception as e:
self.report({'ERROR'}, f"コードの再構築に失敗しました: {e}")
return {'CANCELLED'}
# 4. 新しいテキストデータとして出力
out_name = f"{ADDON_TITLE}_Generated.py"
out_text = bpy.data.texts.get(out_name)
if not out_text:
out_text = bpy.data.texts.new(out_name)
out_text.clear()
out_text.write(new_source)
# 5. テキストエディタに表示
for win in context.window_manager.windows:
for area in win.screen.areas:
if area.type == 'TEXT_EDITOR':
area.spaces.active.text = out_text
break
self.report({'INFO'}, f"コンテナ自身を出力しました: '{out_name}'")
return {'FINISHED'}
# =========================================================
# 内蔵コード個別出力オペレーター
# =========================================================
class ZIONAD_OT_ExportEmbedded(Operator):
bl_idname = f"{PREFIX}.export_embedded"
bl_label = "内蔵コードを抽出"
bl_description = "アドオン内に静的に定義されている指定の内蔵コードをテキストエディタにデコード出力します"
code_key: StringProperty()
def execute(self, context):
if self.code_key not in EMBEDDED_CODES:
self.report({'ERROR'}, f"指定されたキーが見つかりません: {self.code_key}")
return {'CANCELLED'}
b64_text = EMBEDDED_CODES[self.code_key]
try:
decoded = base64.b64decode(b64_text).decode('utf-8')
except Exception as e:
self.report({'ERROR'}, f"デコードに失敗しました: {e}")
return {'CANCELLED'}
out_name = f"{self.code_key}.py"
out_text = bpy.data.texts.get(out_name)
if not out_text:
out_text = bpy.data.texts.new(out_name)
out_text.clear()
out_text.write(decoded)
# テキストエディタに表示
for win in context.window_manager.windows:
for area in win.screen.areas:
if area.type == 'TEXT_EDITOR':
area.spaces.active.text = out_text
break
self.report({'INFO'}, f"内蔵コード '{out_name}' を抽出しました。")
return {'FINISHED'}
# =========================================================
# 各種オペレーター
# =========================================================
class ZIONAD_OT_AddFromClipboard(Operator):
bl_idname = f"{PREFIX}.add_from_clipboard"
bl_label = "クリップボードから追加"
def execute(self, context):
text = context.window_manager.clipboard
if not text or "bl_info" not in text:
self.report({'WARNING'}, "クリップボードに有効なアドオンコードがありません。")
return {'CANCELLED'}
match = re.search(r'["\']name["\']\s*:\s*["\']([^"\']+)["\']', text)
base_name = match.group(1) if match else "新規ペーストコード"
text_name = base_name
count = 1
while f"{text_name}.py" in bpy.data.texts:
count += 1
text_name = f"{base_name} ({count})"
text_data = bpy.data.texts.new(name=f"{text_name}.py")
text_data.use_fake_user = True
text_data.write(text)
props = context.scene.zionad_container_props
item = props.items.add()
item.name = text_name
item.text_ptr = text_data
props.active_index = len(props.items) - 1
return {'FINISHED'}
class ZIONAD_OT_OutputToTextEditor(Operator):
bl_idname = f"{PREFIX}.output_to_text_editor"
bl_label = "テキストエディタで開く"
def execute(self, context):
props = context.scene.zionad_container_props
if not props.items: return {'CANCELLED'}
props.active_index = max(0, min(props.active_index, len(props.items) - 1))
item = props.items[props.active_index]
if not item.text_ptr: return {'CANCELLED'}
for win in context.window_manager.windows:
for area in win.screen.areas:
if area.type == 'TEXT_EDITOR':
area.spaces.active.text = item.text_ptr
break
return {'FINISHED'}
class ZIONAD_OT_RemoveCode(Operator):
bl_idname = f"{PREFIX}.remove_code"
bl_label = "コードを削除"
def execute(self, context):
props = context.scene.zionad_container_props
if not props.items: return {'CANCELLED'}
props.active_index = max(0, min(props.active_index, len(props.items) - 1))
item = props.items[props.active_index]
if item.text_ptr:
bpy.data.texts.remove(item.text_ptr)
props.items.remove(props.active_index)
if props.items:
props.active_index = max(0, min(props.active_index, len(props.items) - 1))
return {'FINISHED'}
class ZIONAD_OT_RemoveAddon(Operator):
bl_idname = f"{PREFIX}.remove_addon"
bl_label = "アドオンの登録解除"
def execute(self, context):
bpy.app.timers.register(unregister, first_interval=0.01)
return {'FINISHED'}
# =========================================================
# UIリスト・パネル類
# =========================================================
class ZIONAD_UL_CodeList(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
layout.label(text=item.name, icon='FILE_SCRIPT' if item.text_ptr else 'ERROR')
# 【コンテナメインパネル】
class ZIONAD_PT_ContainerMain(Panel):
bl_label = ADDON_TITLE
bl_idname = f"{PREFIX}_PT_container_main"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_TITLE
def draw(self, context):
layout = self.layout
props = context.scene.zionad_container_props
# 1. コンテナの出力
box_out = layout.box()
box_out.label(text="コンテナ出力:", icon='EXPORT')
col_out = box_out.column(align=True)
col_out.scale_y = 1.3
col_out.operator(ZIONAD_OT_ExportSelf.bl_idname, icon='TEXT')
box_out.label(text="※ 空の状態でも正常に機能する複製コードが生成されます", icon='INFO')
layout.separator()
# 2. 一時コンテナ内のコード管理リスト
box_container = layout.box()
box_container.label(text="一時コンテナ内リスト:", icon='FILE_SCRIPT')
box_container.operator(ZIONAD_OT_AddFromClipboard.bl_idname, icon='PASTEDOWN')
row_list = box_container.row()
row_list.template_list("ZIONAD_UL_CodeList", "", props, "items", props, "active_index", rows=5)
row_list.operator(ZIONAD_OT_RemoveCode.bl_idname, text="", icon='REMOVE')
if len(props.items) > 0:
safe_index = max(0, min(props.active_index, len(props.items) - 1))
item = props.items[safe_index]
box_container.prop(item, "name", text="管理名")
col_view = box_container.column(align=True)
col_view.scale_y = 1.1
col_view.operator(ZIONAD_OT_OutputToTextEditor.bl_idname, icon='TEXT')
layout.separator()
# 3. 静的に埋め込まれているアドオンコード一覧
box_embedded = layout.box()
box_embedded.label(text="静的内蔵コード一覧:", icon='BOOKMARKS')
if EMBEDDED_CODES:
for key in EMBEDDED_CODES.keys():
row = box_embedded.row(align=True)
row.label(text=key, icon='FILE_BLEND')
op = row.operator(ZIONAD_OT_ExportEmbedded.bl_idname, text="テキスト出力", icon='EXPORT')
op.code_key = key
else:
box_embedded.label(text="内蔵されているコードはありません", icon='INFO')
# 【アドオン削除パネル】
class ZIONAD_PT_Uninstall(Panel):
bl_label = "アドオン削除"
bl_idname = f"{PREFIX}_PT_uninstall"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_TITLE
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
box = layout.box()
box.label(text="アドオンの登録解除:", icon='ERROR')
box.operator(ZIONAD_OT_RemoveAddon.bl_idname, icon='CANCEL')
# =========================================================
# 内蔵コードの自動復元 (起動時展開用)
# =========================================================
def restore_embedded_codes():
if not hasattr(bpy.types.Scene, 'zionad_container_props'):
return 0.5 # Props生成待ちリトライ
props = bpy.context.scene.zionad_container_props
for title, b64_text in EMBEDDED_CODES.items():
if any(item.name == title for item in props.items):
continue
try:
decoded = base64.b64decode(b64_text).decode('utf-8')
text_data = bpy.data.texts.get(f"{title}.py")
if not text_data:
text_data = bpy.data.texts.new(name=f"{title}.py")
text_data.write(decoded)
item = props.items.add()
item.name = title
item.text_ptr = text_data
except Exception:
pass
return None
# =========================================================
# 登録・解除
# =========================================================
classes = (
ZIONAD_CodeItem,
ZIONAD_ContainerProperties,
ZIONAD_OT_ExportSelf,
ZIONAD_OT_ExportEmbedded,
ZIONAD_OT_AddFromClipboard,
ZIONAD_OT_OutputToTextEditor,
ZIONAD_OT_RemoveCode,
ZIONAD_OT_RemoveAddon,
ZIONAD_UL_CodeList,
ZIONAD_PT_ContainerMain,
ZIONAD_PT_Uninstall,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.zionad_container_props = PointerProperty(type=ZIONAD_ContainerProperties)
# 内蔵コードがある場合、起動後に自動展開する
bpy.app.timers.register(restore_embedded_codes, first_interval=0.5)
def unregister():
if hasattr(bpy.types.Scene, 'zionad_container_props'):
del bpy.types.Scene.zionad_container_props
for cls in reversed(classes):
try:
bpy.utils.unregister_class(cls)
except:
pass
if __name__ == "__main__":
register()