
簡易背景
<https://note.com/zionadmillion/n/nb7ae4d18f7ba>
bl_info = {
"name": "zionad_[ 簡易背景 ]",
"author": "Your Name",
"version": (1, 3),
"blender": (5, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "簡易背景設定、カラーピッカー(3x3)による色情報記憶・確実なコード書き出し",
"category": "3D View",
}
import bpy
import math
import mathutils
import webbrowser
import re
import os
# ===================================
# 【定義】
# ===================================
ADDON_TITLE = "[ 簡易背景 ]"
PREFIX_VAL = "DICE_TOOLS_5"
ADDON_CATEGORY = ADDON_TITLE
PREFIX = PREFIX_VAL.lower()
# ============
# 【初期設定・リンク定義】
# ============
LINK_NAME_1 = "基本アドオン設定 20260711b"
LINK_URL_1 = "<https://app.notion.com/p/20260711-39af5dacaf438027811dcd877db7c630>"
LINK_NAME_2 = "posfie"
LINK_URL_2 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_3 = "zionadchat"
LINK_URL_3 = "<https://x.com/zionadchat>"
LINK_NAME_4 = "blender コンテナ 20260713"
LINK_URL_4 = "<https://note.com/zionadmillion/n/n9a47d88fe7b4>"
LINK_NAME_5 = "簡易背景 20260714"
LINK_URL_5 = "<https://note.com/zionadmillion/n/nb7ae4d18f7ba>"
# ============
# 【コレクション・オブジェクト名の初期値】
# ============
INIT_COLL_MAIN = "MainCollection"
INIT_COLL_SUB = "SubCollection"
INIT_OBJ_NAME = "TargetObject"
# ============
# 【数値設定の初期値】
# ============
GRID_PRESET_1 = 10.0
GRID_PRESET_2 = 100.0
GRID_CUSTOM_INIT = 1.0
# ============
# 【初期設定の色 (リセット用)】
# ============
COLOR_GRID_INIT = (0.7176, 0.8, 0.8157, 1.0)
COLOR_BG_HIGH_INIT = (0.4784, 0.4431, 0.8588)
COLOR_BG_LOW_INIT = (0.5725, 0.5725, 0.5725)
# ============
# 【3セット(3x3)のカラー記憶用 初期値】
# エクスポート実行時、この部分が自動で書き換わります。
# ============
PRESET1_GRID = (0.5569, 0.6196, 0.6314, 1.0)
PRESET1_HIGH = (0.0, 0.8588, 0.6235)
PRESET1_LOW = (0.0824, 0.0824, 0.0824)
PRESET2_GRID = (0.6314, 0.0632, 0.5469, 1.0)
PRESET2_HIGH = (0.0922, 0.0355, 0.1006)
PRESET2_LOW = (0.0154, 0.0218, 0.0824)
PRESET3_GRID = (0.5569, 0.6196, 0.6314, 1.0)
PRESET3_HIGH = (0.4784, 0.4431, 0.8588)
PRESET3_LOW = (0.0824, 0.0824, 0.0824)
# ===================================
# 【ビュー情報取得 関数】
# ===================================
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, area, space = None, None, None
for a in context.window.screen.areas:
if a.type == 'VIEW_3D':
rv3d, area, space = a.spaces.active.region_3d, a, 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
is_persp = mode in {'PERSP', 'CAMERA'}
proj_text = "透視投影" if mode == 'PERSP' else "平行投影" if mode == 'ORTHO' else "カメラビュー" if mode == 'CAMERA' else "不明"
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)
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
}
# ===================================
# 【オペレーター (実行文)】
# ===================================
class DICE_TOOLS_5_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
rv3d.view_perspective = 'PERSP'
context.view_layer.update()
return {'FINISHED'}
class DICE_TOOLS_5_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 DICE_TOOLS_5_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['target_pos'].x:.1f}, Y: {info['target_pos'].y:.1f}, Z: {info['target_pos'].z:.1f}",
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"画面水平の大きさ: {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 DICE_TOOLS_5_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
return {'FINISHED'}
class DICE_TOOLS_5_OT_reset_settings(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_reset_settings"
bl_label = "初期値に戻す"
def set_theme_color(self, obj, attr, color):
try:
target_len = len(getattr(obj, attr))
setattr(obj, attr, tuple(color) + (1.0,) if target_len == 4 and len(color) == 3 else tuple(color)[:target_len])
except: pass
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
props.custom_grid_scale = GRID_CUSTOM_INIT
if context.preferences.themes:
theme_view3d = context.preferences.themes[0].view_3d
self.set_theme_color(theme_view3d, "grid", COLOR_GRID_INIT)
self.set_theme_color(theme_view3d.space.gradients, "high_gradient", COLOR_BG_HIGH_INIT)
self.set_theme_color(theme_view3d.space.gradients, "gradient", COLOR_BG_LOW_INIT)
return {'FINISHED'}
# --- 3x3 カラー記憶用 オペレーター ---
class DICE_TOOLS_5_OT_apply_preset(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_apply_preset"
bl_label = "適用"
preset_idx: bpy.props.IntProperty()
def set_theme_color(self, obj, attr, color):
try:
target_len = len(getattr(obj, attr))
setattr(obj, attr, tuple(color) + (1.0,) if target_len == 4 and len(color) == 3 else tuple(color)[:target_len])
except: pass
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
theme_view3d = context.preferences.themes[0].view_3d
self.set_theme_color(theme_view3d, "grid", getattr(props, f"preset{self.preset_idx}_grid"))
self.set_theme_color(theme_view3d.space.gradients, "high_gradient", getattr(props, f"preset{self.preset_idx}_high"))
self.set_theme_color(theme_view3d.space.gradients, "gradient", getattr(props, f"preset{self.preset_idx}_low"))
self.report({'INFO'}, f"セット {self.preset_idx} を適用しました")
return {'FINISHED'}
class DICE_TOOLS_5_OT_store_preset(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_store_preset"
bl_label = "記憶"
preset_idx: bpy.props.IntProperty()
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
theme_view3d = context.preferences.themes[0].view_3d
cg, ch, cl = theme_view3d.grid, theme_view3d.space.gradients.high_gradient, theme_view3d.space.gradients.gradient
# 色の配列長を強制補正して保存 (エラー回避)
setattr(props, f"preset{self.preset_idx}_grid", list(cg) + [1.0] if len(cg) == 3 else list(cg)[:4])
setattr(props, f"preset{self.preset_idx}_high", list(ch)[:3])
setattr(props, f"preset{self.preset_idx}_low", list(cl)[:3])
self.report({'INFO'}, f"セット {self.preset_idx} に記憶しました")
return {'FINISHED'}
class DICE_TOOLS_5_OT_export_code(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX}_export_code"
bl_label = "現在の設定・記憶色で全体コードを出力"
def get_valid_source_code(self, context):
# 確実に「3x3のピッカー変数が存在する最新のコード」を探す処理
code = ""
if context.space_data and context.space_data.type == 'TEXT_EDITOR':
code = context.space_data.text.as_string()
if code and "PRESET3_LOW" in code: return code
valid_texts = []
for text in bpy.data.texts:
s = text.as_string()
if "zionad_[ 簡易背景 ]" in s and "PRESET3_LOW" in s:
valid_texts.append(text)
if valid_texts: return valid_texts[-1].as_string() # 最新のものを返す
return ""
def execute(self, context):
props = getattr(context.scene, f"{PREFIX}_props")
grid_custom = round(props.custom_grid_scale, 4)
if not context.preferences.themes: return {'CANCELLED'}
theme_view3d = context.preferences.themes[0].view_3d
# エラー防止のため長さを固定して丸める関数
def round_c(c, size):
lst = list(c)
while len(lst) < size: lst.append(1.0)
return tuple(round(v, 4) for v in lst[:size])
code = self.get_valid_source_code(context)
if not code:
self.report({'ERROR'}, "正しいソースコードが見つかりません。")
return {'CANCELLED'}
# 数値・初期値置換
code = re.sub(r"GRID_CUSTOM_INIT\s*=\s*[\d\.]+", f"GRID_CUSTOM_INIT = {grid_custom}", code)
code = re.sub(r"COLOR_GRID_INIT\s*=\s*\(.*?\)", f"COLOR_GRID_INIT = {round_c(theme_view3d.grid, 4)}", code)
code = re.sub(r"COLOR_BG_HIGH_INIT\s*=\s*\(.*?\)", f"COLOR_BG_HIGH_INIT = {round_c(theme_view3d.space.gradients.high_gradient, 3)}", code)
code = re.sub(r"COLOR_BG_LOW_INIT\s*=\s*\(.*?\)", f"COLOR_BG_LOW_INIT = {round_c(theme_view3d.space.gradients.gradient, 3)}", code)
# 3セット記憶色の置換
for i in range(1, 4):
cg = round_c(getattr(props, f"preset{i}_grid"), 4)
ch = round_c(getattr(props, f"preset{i}_high"), 3)
cl = round_c(getattr(props, f"preset{i}_low"), 3)
code = re.sub(rf"PRESET{i}_GRID\s*=\s*\(.*?\)", f"PRESET{i}_GRID = {cg}", code)
code = re.sub(rf"PRESET{i}_HIGH\s*=\s*\(.*?\)", f"PRESET{i}_HIGH = {ch}", code)
code = re.sub(rf"PRESET{i}_LOW\s*=\s*\(.*?\)", f"PRESET{i}_LOW = {cl}", code)
new_text = bpy.data.texts.new(name="ZIONAD_Updated_Code.py")
new_text.from_string(code)
# 生成後、テキストエディタを切り替える
for area in context.screen.areas:
if area.type == 'TEXT_EDITOR':
area.spaces.active.text = new_text
break
self.report({'INFO'}, f"新しいコード '{new_text.name}' を生成しました。こちらを実行してください。")
return {'FINISHED'}
class DICE_TOOLS_5_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 DICE_TOOLS_5_OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX}_remove_addon"
bl_label = "アドオン削除実行"
def execute(self, context):
try: unregister(); self.report({'INFO'}, "アドオンを削除しました")
except: pass
return {'FINISHED'}
# ===================================
# 【プロパティ定義】
# ===================================
class DICE_TOOLS_5_Properties(bpy.types.PropertyGroup):
custom_grid_scale: bpy.props.FloatProperty(name="指定数値", default=GRID_CUSTOM_INIT, min=0.001)
# サイズ(3, 4)を厳密に固定
preset1_grid: bpy.props.FloatVectorProperty(name="G", subtype='COLOR', size=4, default=PRESET1_GRID, min=0.0, max=1.0)
preset1_high: bpy.props.FloatVectorProperty(name="H", subtype='COLOR', size=3, default=PRESET1_HIGH, min=0.0, max=1.0)
preset1_low: bpy.props.FloatVectorProperty(name="L", subtype='COLOR', size=3, default=PRESET1_LOW, min=0.0, max=1.0)
preset2_grid: bpy.props.FloatVectorProperty(name="G", subtype='COLOR', size=4, default=PRESET2_GRID, min=0.0, max=1.0)
preset2_high: bpy.props.FloatVectorProperty(name="H", subtype='COLOR', size=3, default=PRESET2_HIGH, min=0.0, max=1.0)
preset2_low: bpy.props.FloatVectorProperty(name="L", subtype='COLOR', size=3, default=PRESET2_LOW, min=0.0, max=1.0)
preset3_grid: bpy.props.FloatVectorProperty(name="G", subtype='COLOR', size=4, default=PRESET3_GRID, min=0.0, max=1.0)
preset3_high: bpy.props.FloatVectorProperty(name="H", subtype='COLOR', size=3, default=PRESET3_HIGH, min=0.0, max=1.0)
preset3_low: bpy.props.FloatVectorProperty(name="L", subtype='COLOR', size=3, default=PRESET3_LOW, min=0.0, max=1.0)
# ===================================
# 【UIパネル (アドオン構造)】
# ===================================
class DICE_TOOLS_5_PT_main_panel(bpy.types.Panel):
bl_idname = f"{PREFIX.upper()}_PT_main_panel"
bl_label = ADDON_TITLE
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
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(factor=1.5)
box_info = layout.box()
row_info = box_info.row(align=True)
row_info.label(text=f"情報: [{info['proj_text'] if info else '不明'}]", icon='INFO')
row_info.operator(f"view3d.{PREFIX}_update_info", text="", icon='FILE_REFRESH')
if info:
col_info = box_info.column(align=True)
col_info.label(text=f"画面中央位置: X: {info['target_pos'].x:.1f}, Y: {info['target_pos'].y:.1f}, Z: {info['target_pos'].z:.1f}")
col_info.label(text=f"視座位置: X: {info['cam_pos'].x:.1f}, Y: {info['cam_pos'].y:.1f}, Z: {info['cam_pos'].z:.1f}")
col_info.label(text=f"視線方向: X: {info['view_dir'].x:.1f}, Y: {info['view_dir'].y:.1f}, Z: {info['view_dir'].z:.1f}")
col_info.separator()
col_info.label(text=f"画面水平の大きさ: {info['w']:.1f}")
col_info.label(text=f"画面上下の大きさ: {info['h']:.1f}")
if info['is_persp']:
col_info.separator()
col_info.label(text=f"視座からの距離: {info['dist']:.1f}")
box_info.separator()
box_info.operator(f"view3d.{PREFIX}_copy_info", icon='COPYDOWN')
else: box_info.label(text="3Dビューが見つかりません")
layout.separator(factor=1.5)
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=f"間隔 {GRID_PRESET_1}").scale_val = GRID_PRESET_1
row_preset.operator(f"view3d.{PREFIX}_set_grid_scale", text=f"間隔 {GRID_PRESET_2}").scale_val = GRID_PRESET_2
row_custom = box_grid.row(align=True)
row_custom.prop(props, "custom_grid_scale")
row_custom.operator(f"view3d.{PREFIX}_set_grid_scale", text="適用").use_custom = True
box_grid.separator()
if context.preferences.themes:
theme_view3d = context.preferences.themes[0].view_3d
col_theme = box_grid.column(align=True)
col_theme.prop(theme_view3d, "grid", text="Grid 色変更")
col_theme.separator()
col_theme.prop(theme_view3d.space.gradients, "high_gradient", text="画面背景色 (上)")
col_theme.prop(theme_view3d.space.gradients, "gradient", text="画面背景色 (下)")
box_grid.separator()
box_grid.operator(f"view3d.{PREFIX}_reset_settings", icon='LOOP_BACK')
layout.separator(factor=1.5)
box_mem = layout.box()
box_mem.label(text="カラー記憶スロット (3セット)", icon='COLOR')
for i in range(1, 4):
row_btn = box_mem.row(align=True)
row_btn.label(text=f"セット {i}:")
row_btn.operator(f"view3d.{PREFIX}_apply_preset", text="適用", icon='CHECKMARK').preset_idx = i
row_btn.operator(f"view3d.{PREFIX}_store_preset", text="記憶", icon='EYEDROPPER').preset_idx = i
row_color = box_mem.row(align=True)
row_color.prop(props, f"preset{i}_grid", text="G")
row_color.prop(props, f"preset{i}_high", text="H")
row_color.prop(props, f"preset{i}_low", text="L")
box_mem.separator()
box_mem.operator(f"view3d.{PREFIX}_export_code", icon='TEXT')
layout.separator(factor=1.5)
box_sys = layout.box()
box_sys.label(text="システム / リンク", icon='PREFERENCES')
for name, url in [(LINK_NAME_1, LINK_URL_1), (LINK_NAME_2, LINK_URL_2), (LINK_NAME_3, LINK_URL_3), (LINK_NAME_4, LINK_URL_4), (LINK_NAME_5, LINK_URL_5)]:
if name and url:
op = box_sys.operator(f"wm.{PREFIX}_open_url", text=name, icon='URL')
op.url = url
box_sys.separator()
box_sys.operator(f"wm.{PREFIX}_remove_addon", icon='CANCEL')
# ===================================
# 【登録・削除処理】
# ===================================
classes = [
DICE_TOOLS_5_Properties,
DICE_TOOLS_5_OT_reset_view,
DICE_TOOLS_5_OT_update_info,
DICE_TOOLS_5_OT_copy_info,
DICE_TOOLS_5_OT_set_grid_scale,
DICE_TOOLS_5_OT_reset_settings,
DICE_TOOLS_5_OT_apply_preset,
DICE_TOOLS_5_OT_store_preset,
DICE_TOOLS_5_OT_export_code,
DICE_TOOLS_5_OT_open_url,
DICE_TOOLS_5_OT_remove_addon,
DICE_TOOLS_5_PT_main_panel
]
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, f"{PREFIX}_props", bpy.props.PointerProperty(type=DICE_TOOLS_5_Properties))
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()
元型: