https://posfie.com/@timekagura?sort=0&page=1
# ======================================================================
# --- リンク設定 / Links ---
# ======================================================================
NEW_DOC_LINKS = [
{"label": "THIS_ADDON [ ]", "url": ""},
]
SOCIAL_LINKS = [
{"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
]
# ======================================================================
# ======================================================================
# --- リンク設定 / Links ---
# ======================================================================
NEW_DOC_LINKS = [
{"label": "THIS_ADDON [ ]", "url": ""},
]
SOCIAL_LINKS = [
{"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
]
# ======================================================================
import bpy
import bmesh
import math
import mathutils
import webbrowser
import os
from bpy.types import Operator, Panel, Scene, PropertyGroup
from bpy.props import StringProperty, PointerProperty, EnumProperty, FloatVectorProperty, FloatProperty, CollectionProperty, BoolProperty, IntProperty
# ======================================================================
# --- アドオン情報 / Addon Info ---
# ======================================================================
PREFIX = "unit_circle_cam"
bl_info = {
"name": "zionad 521 [Unit Circle Cam]",
"author": "zionadchat",
"version": (37, 0, 7),
"blender": (4, 1, 0),
"location": "View3D > Sidebar > zionad Control",
"description": "3つの専用カメラ初期値変更と一括リセット、ビューポートカメラの一括リセット機能(完全安定版)",
"category": "Cam three", # UI崩れ防止のため余分なスペースを削除
}
# ======================================================================
# --- ユーザー設定 / Parameters to Customize ---
# ======================================================================
ADDON_CATEGORY_NAME = bl_info["category"]
# ※ハードコードパスですが、内部で os.path.exists により存在チェックを行うため
# 他環境で実行してもクラッシュはしません(ファイルが見つからないだけになります)
HDRI_PATHS = [
r"C:\a111\HDRi_pic\qwantani_afternoon_puresky_4k.exr",
r"C:\a111\HDRi_pic\rogland_moonlit_night_4k.hdr",
r"C:\a111\HDRi_pic\rogland_clear_night_4k.hdr",
r"C:\a111\HDRi_pic\golden_bay_4k.hdr",
]
WIRE_PRESETS = [("CUSTOM_GREENISH", "Custom Greenish", "Custom greenish wire color", (0.51, 1.0, 0.75)), ("WHITE", "White", "White wire", (1.0, 1.0, 1.0)), ("RED", "Red", "Red wire", (1.0, 0.0, 0.0)), ("GREEN", "Green", "Green wire", (0.0, 1.0, 0.0)),]
GRID_PRESETS = [("CUSTOM_REDDISH", "Custom Reddish", "Custom reddish color", (0.545, 0.322, 0.322, 1.0)), ("DEEP_GREEN", "Deep Green", "A deep green color", (0.098, 0.314, 0.271, 1.0)), ("MINT_GREEN", "Mint Green", "A mint green color", (0.165, 0.557, 0.475, 1.0)),]
# ★ 親コレクションとサブコレクション名
MASTER_COLLECTION_NAME = "Cam three"
CAMERA_COLLECTION_NAME = "Cam"
SENSOR_WIDTH = 36.0
FOV_PRESETS = [1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]
# ======================================================================
# --- リンク設定 / Links ---
# ======================================================================
NEW_DOC_LINKS = [
{"label": "THIS_ADDON [ カメラ3台 原型 20260328 ]", "url": "<https://www.notion.so/3-20260328-330f5dacaf4380a4b9b5eef6e98a276f>"},
]
SOCIAL_LINKS = [
{"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
]
# ======================================================================
# --- パネル管理 ---
# ======================================================================
PANEL_IDS = {
"SETUP": f"{PREFIX}_PT_setup",
"AIMING": f"{PREFIX}_PT_aiming", "VIEWPORT_CAM": f"{PREFIX}_PT_viewport_cam",
"LENS": f"{PREFIX}_PT_lens", "CAMERA_DISPLAY": f"{PREFIX}_PT_camera_display", "WORLD_CONTROL": f"{PREFIX}_PT_world_control",
"GRID": f"{PREFIX}_PT_grid_panel", "WIRE": f"{PREFIX}_PT_wire_panel", "LINKS": f"{PREFIX}_PT_links", "REMOVE": f"{PREFIX}_PT_remove",
}
PANEL_ORDER = {
PANEL_IDS["SETUP"]: 0, PANEL_IDS["AIMING"]: 1, PANEL_IDS["VIEWPORT_CAM"]: 2, PANEL_IDS["LENS"]: 3,
PANEL_IDS["CAMERA_DISPLAY"]: 4, PANEL_IDS["WORLD_CONTROL"]: 5,
PANEL_IDS["GRID"]: 89, PANEL_IDS["WIRE"]: 90, PANEL_IDS["LINKS"]: 190, PANEL_IDS["REMOVE"]: 200,
}
# ======================================================================
# --- ロック機構 & タイマー管理 (プロフェッショナル設計) ---
# ======================================================================
def set_update_lock(scene, state: bool):
""" 更新フラグをSceneに持たせ、競合を防ぐ """
if scene:
scene["_sfc_updating"] = state
def is_updating(scene):
if scene:
return scene.get("_sfc_updating", False)
return False
def schedule_update_lock_reset():
# タイマーから呼ばれるためcontextから安全に取得
if bpy.context and hasattr(bpy.context, 'scene'):
bpy.context.scene["_sfc_updating"] = False
return None
def trigger_delayed_unlock():
""" 提言: 多重登録リスクを完全に回避する堅牢なタイマー登録 """
if bpy.app.timers.is_registered(schedule_update_lock_reset):
bpy.app.timers.unregister(schedule_update_lock_reset)
bpy.app.timers.register(schedule_update_lock_reset, first_interval=0.01)
# ======================================================================
# --- 汎用ヘルパー関数 ---
# ======================================================================
def get_or_create_collection(context, name, parent_col=None):
col = bpy.data.collections.get(name)
if not col:
col = bpy.data.collections.new(name)
if parent_col:
if col.name not in parent_col.children:
parent_col.children.link(col)
else:
if col.name not in context.scene.collection.children:
context.scene.collection.children.link(col)
return col
def get_master_collection(context):
return get_or_create_collection(context, MASTER_COLLECTION_NAME)
def find_node(nodes, node_type, name):
if node_type == 'OUTPUT_WORLD': return next((n for n in nodes if n.type == 'OUTPUT_WORLD'), None)
return nodes.get(name)
def find_or_create_node(nodes, node_type, name, location_offset=(0, 0)):
node = find_node(nodes, node_type, name)
if node: return node
new_node = nodes.new(type=node_type)
new_node.name = name
new_node.label = name.replace("_", " ")
output_node = find_node(nodes, 'OUTPUT_WORLD', '')
if output_node:
new_node.location = output_node.location + mathutils.Vector(location_offset)
return new_node
def get_world_nodes(context, create=True):
world = context.scene.world
if not world and create:
world = bpy.data.worlds.new("World")
context.scene.world = world
if not world: return None, None, None
if create: world.use_nodes = True
if not world.use_nodes: return world, None, None
return world, world.node_tree.nodes, world.node_tree.links
def load_hdri_from_path(filepath, context):
_, nodes, _ = get_world_nodes(context)
if not nodes: return False
env_node = find_or_create_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
if os.path.exists(filepath):
try:
env_node.image = bpy.data.images.load(filepath, check_existing=True)
return True
except Exception as e:
print(f"[HDRI Load Error] {filepath} -> {e}")
return False
return False
def update_viewport(context):
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.type = 'MATERIAL'
return
def update_background_mode(self, context):
mode = context.scene.zionad_swt_props.background_mode
world, nodes, links = get_world_nodes(context)
if not nodes: return
output_node = find_or_create_node(nodes, 'OUTPUT_WORLD', 'World_Output')
background_node = find_or_create_node(nodes, 'ShaderNodeBackground', 'Background', (-250, 0))
sky_node = find_or_create_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture', (-550, 0))
env_node = find_or_create_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture', (-550, 0))
mapping_node = find_or_create_node(nodes, 'ShaderNodeMapping', 'Mapping', (-800, 0))
tex_coord_node = find_or_create_node(nodes, 'ShaderNodeTexCoord', 'Texture_Coordinate', (-1050, 0))
if background_node.inputs['Color'].is_linked: links.remove(background_node.inputs['Color'].links[0])
if output_node.inputs['Surface'].is_linked: links.remove(output_node.inputs['Surface'].links[0])
links.new(background_node.outputs['Background'], output_node.inputs['Surface'])
if mode == 'SKY':
links.new(sky_node.outputs['Color'], background_node.inputs['Color'])
elif mode == 'HDRI':
if not mapping_node.inputs['Vector'].is_linked: links.new(tex_coord_node.outputs['Generated'], mapping_node.inputs['Vector'])
if not env_node.inputs['Vector'].is_linked: links.new(mapping_node.outputs['Vector'], env_node.inputs['Vector'])
links.new(env_node.outputs['Color'], background_node.inputs['Color'])
props = context.scene.zionad_swt_props
if 0 <= props.hdri_list_index < len(HDRI_PATHS):
load_hdri_from_path(HDRI_PATHS[props.hdri_list_index], context)
update_viewport(context)
# ======================================================================
# --- カメラ コアロジック・プロパティ ---
# ======================================================================
def update_cam_color(self, context):
if self.camera_obj:
context.preferences.themes[0].view_3d.camera = self.camera_color
def update_grid_color_cb(self, context):
context.preferences.themes[0].view_3d.grid = self.grid_color
def update_wire_color_cb(self, context):
context.preferences.themes[0].view_3d.wire = self.wire_color
context.preferences.themes[0].view_3d.object_active = self.wire_color
class ThemeGridProperties(PropertyGroup):
grid_color: FloatVectorProperty(name="Grid Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.545, 0.322, 0.322, 1.0), update=update_grid_color_cb)
grid_preset: EnumProperty(name="Grid Preset", items=[(p[0], p[1], p[2]) for p in GRID_PRESETS], update=lambda self, context: SFC_OT_GridApplyColor.update_preset(self, context))
class ThemeWireProperties(PropertyGroup):
wire_color: FloatVectorProperty(name="Wire Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=(0.51, 1.0, 0.75), update=update_wire_color_cb)
wire_preset: EnumProperty(name="Wire Preset", items=[(p[0], p[1], p[2]) for p in WIRE_PRESETS], update=lambda self, context: SFC_OT_WireApplyColor.update_preset(self, context))
class TargetProperty(PropertyGroup): name: StringProperty()
# ----------------------------------------------------------------------
# Property update の過密呼び出しを防ぐ Debounce(遅延)処理
# ----------------------------------------------------------------------
def _do_update_viewport_cam():
context = bpy.context
if not context or not hasattr(context, 'scene'): return None
scene = context.scene
props = scene.surface_camera_properties
vp_loc = mathutils.Vector(props.viewport_location)
vp_tgt = mathutils.Vector(props.viewport_target)
direction = vp_tgt - vp_loc
if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
rot_quat = direction.to_track_quat('-Z', 'Y')
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
rv3d = space.region_3d
if rv3d:
set_update_lock(scene, True)
try:
if rv3d.view_perspective == 'CAMERA':
rv3d.view_perspective = 'PERSP'
rv3d.view_location = vp_tgt
rv3d.view_rotation = rot_quat
rv3d.view_distance = direction.length
finally:
trigger_delayed_unlock()
break
return None
def safe_update_viewport_cam(self, context):
if is_updating(context.scene): return
if bpy.app.timers.is_registered(_do_update_viewport_cam):
bpy.app.timers.unregister(_do_update_viewport_cam)
bpy.app.timers.register(_do_update_viewport_cam, first_interval=0.01)
def _do_update_surface_camera():
context = bpy.context
if not context or not hasattr(context, 'scene'): return None
scene = context.scene
props = scene.surface_camera_properties
camera_obj = props.camera_obj
set_update_lock(scene, True)
try:
if props.is_updating_settings or not camera_obj:
update_info_panel_text(props, scene)
return None
cam_data = camera_obj.data
if cam_data:
cam_data.sensor_fit = 'HORIZONTAL'
cam_data.lens_unit = 'MILLIMETERS'
cam_data.lens = props.lens_focal_length
cam_data.clip_start = props.clip_start
cam_data.clip_end = props.clip_end
update_object_transform(camera_obj, props)
update_info_panel_text(props, scene)
finally:
trigger_delayed_unlock()
return None
def safe_update_surface_camera(self, context):
if is_updating(context.scene): return
if bpy.app.timers.is_registered(_do_update_surface_camera):
bpy.app.timers.unregister(_do_update_surface_camera)
bpy.app.timers.register(_do_update_surface_camera, first_interval=0.01)
# ----------------------------------------------------------------------
class SurfaceCameraProperties(PropertyGroup):
camera_obj: PointerProperty(name="操作カメラ", type=bpy.types.Object, poll=lambda self, obj: obj.type == 'CAMERA', update=safe_update_surface_camera)
show_init_settings: BoolProperty(name="初期値設定を表示", default=False)
cam1_init_loc: FloatVectorProperty(name="位置", default=(0.0, 0.0, 0.0), subtype='XYZ')
cam1_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 100.0, 0.0), subtype='XYZ')
cam2_init_loc: FloatVectorProperty(name="位置", default=(0.0, -10.0, 0.0), subtype='XYZ')
cam2_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 0.0, 0.0), subtype='XYZ')
cam3_init_loc: FloatVectorProperty(name="位置", default=(0.0, 0.0, 100.0), subtype='XYZ')
cam3_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 0.0, 0.0), subtype='XYZ')
target_location: FloatVectorProperty(name="固定注視点", default=(0.0, 100.0, 0.0), subtype='XYZ', update=safe_update_surface_camera)
offset_yaw: FloatProperty(name="Yaw", subtype='ANGLE', default=0, update=safe_update_surface_camera)
offset_pitch: FloatProperty(name="Pitch", subtype='ANGLE', default=0, update=safe_update_surface_camera)
offset_roll: FloatProperty(name="Roll", subtype='ANGLE', default=0, update=safe_update_surface_camera)
viewport_location: FloatVectorProperty(name="視座位置", default=(0.0, -10.0, 5.0), subtype='XYZ', update=safe_update_viewport_cam)
viewport_target: FloatVectorProperty(name="注視点", default=(0.0, 0.0, 0.0), subtype='XYZ', update=safe_update_viewport_cam)
is_updating_settings: BoolProperty(default=False, options={'HIDDEN'})
lens_focal_length: FloatProperty(name="焦点距離 (mm)", default=50.0, min=1.0, max=1000.0, unit='LENGTH', update=safe_update_surface_camera)
clip_start: FloatProperty(name="クリップ開始", default=0.1, min=0.001, update=safe_update_surface_camera)
clip_end: FloatProperty(name="クリップ終了", default=1000.0, min=1.0, update=safe_update_surface_camera)
info_horizontal_fov: StringProperty(name="水平視野角")
camera_color: FloatVectorProperty(
name="カメラ枠線 色",
subtype='COLOR', size=3, min=0.0, max=1.0,
default=(0.0, 1.0, 1.0),
update=lambda self, context: update_cam_color(self, context)
)
class ZIONAD_SWT_Properties(PropertyGroup):
background_mode: EnumProperty(name="Background Mode", items=[('HDRI', "HDRI", ""), ('SKY', "Sky", "")], default='HDRI', update=update_background_mode)
hdri_list_index: IntProperty(name="Active HDRI Index", default=0, update=update_background_mode)
def calculate_horizontal_fov(focal_length, sensor_width=SENSOR_WIDTH):
try: return 2 * math.atan(sensor_width / (2 * focal_length)) * (180 / math.pi)
except: return 0.0
def calculate_focal_length(fov_degrees, sensor_width=SENSOR_WIDTH):
try: return sensor_width / (2 * math.tan(math.radians(fov_degrees) / 2))
except: return 50.0
def get_target_location(props):
return mathutils.Vector(props.target_location)
def update_object_transform(obj, props):
location = obj.location
target_location = get_target_location(props)
direction = target_location - location
if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
base_track_quat = direction.to_track_quat('-Z', 'Y')
offset_euler = mathutils.Euler((props.offset_pitch, props.offset_yaw, props.offset_roll), 'XYZ')
final_quat = base_track_quat @ offset_euler.to_quaternion()
obj.rotation_euler = final_quat.to_euler('XYZ')
def update_info_panel_text(props, scene):
if not props: return
camera_obj = props.camera_obj
if not camera_obj: return
current_fov = calculate_horizontal_fov(props.lens_focal_length)
props.info_horizontal_fov = f"{current_fov:.1f} °"
def sync_ui_from_manual_transform(props, obj, scene):
if is_updating(scene): return
set_update_lock(scene, True)
try:
target_location = get_target_location(props)
direction = target_location - obj.location
if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
base_track_quat = direction.to_track_quat('-Z', 'Y')
final_quat = obj.matrix_world.to_quaternion()
offset_quat = base_track_quat.inverted() @ final_quat
offset_euler = offset_quat.to_euler('XYZ')
props.offset_pitch = offset_euler.x
props.offset_yaw = offset_euler.y
props.offset_roll = offset_euler.z
finally:
trigger_delayed_unlock()
update_info_panel_text(props, scene)
@bpy.app.handlers.persistent
def on_depsgraph_update(scene, depsgraph):
if is_updating(scene): return
sfc_props = scene.surface_camera_properties
cam_obj = sfc_props.camera_obj
if not cam_obj: return
for update in depsgraph.updates:
if not update.is_updated_transform: continue
# 提言: 監視対象を特定のものだけに完全に限定し、無駄な同期をなくす
if update.id.original == cam_obj:
sync_ui_from_manual_transform(sfc_props, cam_obj, scene)
return
# ======================================================================
# --- オペレーター ---
# ======================================================================
def set_initial_camera_transform(obj, loc, tgt):
loc_vec = mathutils.Vector(loc)
tgt_vec = mathutils.Vector(tgt)
direction = tgt_vec - loc_vec
if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
rot_quat = direction.to_track_quat('-Z', 'Y')
obj.location = loc_vec
obj.rotation_euler = rot_quat.to_euler('XYZ')
class SFC_OT_CreateThreeCameras(Operator):
bl_idname = f"{PREFIX}.create_three_cameras"
bl_label = "3つのカメラを生成・初期化"
def execute(self, context):
master_col = get_master_collection(context)
col = get_or_create_collection(context, CAMERA_COLLECTION_NAME, master_col)
props = context.scene.surface_camera_properties
configs = [
(1, props.cam1_init_loc, props.cam1_init_tgt),
(2, props.cam2_init_loc, props.cam2_init_tgt),
(3, props.cam3_init_loc, props.cam3_init_tgt),
]
for idx, loc, tgt in configs:
name = f"Fixed_Cam_{idx}"
# 提言1: O(1)アクセスによるカメラの高速・安全取得
cam_obj = bpy.data.objects.get(name)
if cam_obj and cam_obj.type != 'CAMERA':
cam_obj = None
if not cam_obj:
cam_data = bpy.data.cameras.new(name=name)
cam_obj = bpy.data.objects.new(name, cam_data)
col.objects.link(cam_obj)
if cam_obj.name in context.scene.collection.objects.keys():
context.scene.collection.objects.unlink(cam_obj)
set_initial_camera_transform(cam_obj, loc, tgt)
op_func = getattr(getattr(bpy.ops, PREFIX), "switch_camera")
op_func(cam_index="1")
self.report({'INFO'}, "3つのカメラを生成しました")
return {'FINISHED'}
class SFC_OT_ResetThreeCameras(Operator):
bl_idname = f"{PREFIX}.reset_three_cameras"
bl_label = "カメラを初期値に一括リセット"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.surface_camera_properties
configs = [
(1, props.cam1_init_loc, props.cam1_init_tgt),
(2, props.cam2_init_loc, props.cam2_init_tgt),
(3, props.cam3_init_loc, props.cam3_init_tgt),
]
for idx, loc, tgt in configs:
cam_obj = bpy.data.objects.get(f"Fixed_Cam_{idx}")
if cam_obj and cam_obj.type == 'CAMERA':
set_initial_camera_transform(cam_obj, loc, tgt)
if props.camera_obj == cam_obj:
props.is_updating_settings = True
props.target_location = tgt
props.offset_yaw = 0.0
props.offset_pitch = 0.0
props.offset_roll = 0.0
props.is_updating_settings = False
self.report({'INFO'}, "カメラを初期値にリセットしました")
return {'FINISHED'}
class SFC_OT_ResetViewportCam(Operator):
bl_idname = f"{PREFIX}.reset_viewport_cam"
bl_label = "架空カメラを一括リセット"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.surface_camera_properties
props.viewport_location = (0.0, -10.0, 5.0)
props.viewport_target = (0.0, 0.0, 0.0)
self.report({'INFO'}, "架空カメラをリセットしました")
return {'FINISHED'}
class SFC_OT_CopyViewportInfo(Operator):
bl_idname = f"{PREFIX}.copy_viewport_info"
bl_label = "視座・注視点情報をコピー"
def execute(self, context):
props = context.scene.surface_camera_properties
loc = props.viewport_location
tgt = props.viewport_target
fmt = ".2f"
loc_str = f"({loc.x:{fmt}}, {loc.y:{fmt}}, {loc.z:{fmt}})"
tgt_str = f"({tgt.x:{fmt}}, {tgt.y:{fmt}}, {tgt.z:{fmt}})"
text_to_copy = f"視座位置: {loc_str}\n注視点: {tgt_str}"
context.window_manager.clipboard = text_to_copy
self.report({'INFO'}, "ビューポートの視座位置・注視点をコピーしました")
return {'FINISHED'}
class SFC_OT_SwitchCamera(Operator):
bl_idname = f"{PREFIX}.switch_camera"
bl_label = "カメラを切り替え"
cam_index: StringProperty()
def execute(self, context):
props = context.scene.surface_camera_properties
name = f"Fixed_Cam_{self.cam_index}"
# 提言1: O(1)アクセスによるカメラの高速・安全取得
cam_obj = bpy.data.objects.get(name)
if cam_obj and cam_obj.type != 'CAMERA':
cam_obj = None
if not cam_obj:
self.report({'WARNING'}, f"{name} が見つかりません。先に「生成」ボタンを押してください。")
return {'CANCELLED'}
props.is_updating_settings = True
props.camera_obj = cam_obj
context.scene.camera = cam_obj
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.region_3d.view_perspective = 'CAMERA'
context.preferences.themes[0].view_3d.camera = props.camera_color
cam_data = cam_obj.data
props.lens_focal_length = cam_data.lens
props.clip_start = cam_data.clip_start
props.clip_end = cam_data.clip_end
forward_vec = mathutils.Vector((0.0, 0.0, -100.0))
forward_vec.rotate(cam_obj.rotation_euler)
props.target_location = cam_obj.location + forward_vec
props.offset_yaw = 0.0
props.offset_pitch = 0.0
props.offset_roll = 0.0
props.is_updating_settings = False
sync_ui_from_manual_transform(props, cam_obj, context.scene)
return {'FINISHED'}
class SFC_OT_GridApplyColor(Operator):
bl_idname = f"{PREFIX}.apply_grid_color"; bl_label = "Apply Grid Color"
def execute(self, context): props = context.scene.theme_grid_properties; theme = bpy.context.preferences.themes[0]; theme.view_3d.grid = props.grid_color; return {'FINISHED'}
@staticmethod
def update_preset(self, context):
props = context.scene.theme_grid_properties
props.grid_color = next((p[3] for p in GRID_PRESETS if p[0] == props.grid_preset), props.grid_color)
getattr(bpy.ops, f"{PREFIX}.apply_grid_color")()
class SFC_OT_GridCopyColor(Operator):
bl_idname = f"{PREFIX}.copy_grid_color"; bl_label = "Copy Grid Color"
def execute(self, context): theme = bpy.context.preferences.themes[0]; color_tuple = tuple(round(c, 3) for c in theme.view_3d.grid); context.window_manager.clipboard = f'("CUSTOM", "Custom", "Custom grid color", {color_tuple}),'; self.report({'INFO'}, "コピーしました"); return {'FINISHED'}
class SFC_OT_ResetProperty(Operator):
bl_idname = f"{PREFIX}.reset_property"; bl_label = "プロパティリセット"; targets: CollectionProperty(type=TargetProperty); prop_group_name: StringProperty()
def execute(self, context):
props = context.scene.surface_camera_properties
prop_groups = {"ypr": ["offset_yaw", "offset_pitch", "offset_roll"],"aim": ["target_location"],"clip": ["clip_start", "clip_end", "lens_focal_length"],}
target_names, props_to_reset = {t.name for t in self.targets}, set()
if "all" in target_names:
for g in prop_groups.values(): props_to_reset.update(g)
else:
for name in target_names: props_to_reset.update(prop_groups.get(name, []))
props.is_updating_settings = True
for p in props_to_reset:
if hasattr(props, p): props.property_unset(p)
props.is_updating_settings = False
safe_update_surface_camera(props, context)
return {'FINISHED'}
class SFC_OT_SetFOV(Operator):
bl_idname = f"{PREFIX}.set_fov"; bl_label = "FOV設定"; fov: FloatProperty(default=0.0)
def execute(self, context): props = context.scene.surface_camera_properties; props.lens_focal_length = calculate_focal_length(self.fov); return {'FINISHED'}
class SFC_OT_OpenURL(Operator):
bl_idname = f"{PREFIX}.open_url"; bl_label = "URLを開く"; url: StringProperty(default="")
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class SFC_OT_RemoveAddon(Operator):
bl_idname = f"{PREFIX}.remove_addon"; bl_label = "アドオン解除"
def execute(self, context): module_name = __name__.split('.')[0]; bpy.ops.preferences.addon_disable(module=module_name); unregister(); return {'FINISHED'}
class SFC_OT_WireApplyColor(Operator):
bl_idname = f"{PREFIX}.apply_wire_color"; bl_label = "Apply Wire Color"
def execute(self, context): props=context.scene.theme_wire_properties; theme=bpy.context.preferences.themes[0]; theme.view_3d.wire=props.wire_color; theme.view_3d.object_active=props.wire_color; return {'FINISHED'}
@staticmethod
def update_preset(self, context):
props = context.scene.theme_wire_properties
props.wire_color = next((p[3] for p in WIRE_PRESETS if p[0] == props.wire_preset), props.wire_color)
getattr(bpy.ops, f"{PREFIX}.apply_wire_color")()
class SFC_OT_WireCopyColor(Operator):
bl_idname = f"{PREFIX}.copy_wire_color"; bl_label = "Copy Wire Color"
def execute(self, context): theme=bpy.context.preferences.themes[0]; color_tuple=tuple(round(c, 2) for c in theme.view_3d.wire); context.window_manager.clipboard=f'("CUSTOM", "Custom", "Custom wire color", {color_tuple}),'; return {'FINISHED'}
class ZIONAD_SWT_OT_LoadHdriFromList(Operator):
bl_idname = f"{PREFIX}.load_hdri_from_list"; bl_label = "Load HDRI from List"; bl_options = {'REGISTER', 'UNDO'}; hdri_index: IntProperty()
def execute(self, context):
props = context.scene.zionad_swt_props
if 0 <= self.hdri_index < len(HDRI_PATHS):
props.hdri_list_index = self.hdri_index; props.background_mode = 'HDRI'; load_hdri_from_path(HDRI_PATHS[self.hdri_index], context); update_background_mode(props, context)
return {'FINISHED'}
class ZIONAD_SWT_OT_ResetTransform(Operator):
bl_idname = f"{PREFIX}.reset_transform"; bl_label = "Reset Transform Value"; bl_options = {'REGISTER', 'UNDO'}; property_to_reset: StringProperty()
def execute(self, context):
_, nodes, _ = get_world_nodes(context)
if not nodes: return {'CANCELLED'}
mapping_node = find_node(nodes, 'ShaderNodeMapping', 'Mapping')
if not mapping_node: return {'CANCELLED'}
if self.property_to_reset == 'Location': mapping_node.inputs['Location'].default_value = (0, 0, 0)
elif self.property_to_reset == 'Rotation': mapping_node.inputs['Rotation'].default_value = (0, 0, 0)
elif self.property_to_reset == 'Scale': mapping_node.inputs['Scale'].default_value = (1, 1, 1)
return {'FINISHED'}
# ======================================================================
# --- UIパネル ---
# ======================================================================
class SFC_PT_CameraSetupPanel(Panel):
bl_label = "1. カメラ作成・切り替え"
bl_idname = PANEL_IDS["SETUP"]
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY_NAME
bl_order = PANEL_ORDER[PANEL_IDS["SETUP"]]
def draw(self, context):
layout = self.layout
props = context.scene.surface_camera_properties
layout.operator(SFC_OT_CreateThreeCameras.bl_idname, icon='OUTLINER_OB_CAMERA', text="3つのカメラを生成・初期化")
box_init = layout.box()
box_init.prop(props, "show_init_settings", icon="TRIA_DOWN" if props.show_init_settings else "TRIA_RIGHT")
if props.show_init_settings:
col_init = box_init.column(align=True)
col_init.prop(props, "cam1_init_loc", text="1: 位置"); col_init.prop(props, "cam1_init_tgt", text=" 注視")
col_init.separator()
col_init.prop(props, "cam2_init_loc", text="2: 位置"); col_init.prop(props, "cam2_init_tgt", text=" 注視")
col_init.separator()
col_init.prop(props, "cam3_init_loc", text="3: 位置"); col_init.prop(props, "cam3_init_tgt", text=" 注視")
box_init.separator()
box_init.operator(SFC_OT_ResetThreeCameras.bl_idname, icon='LOOP_BACK', text="初期値にリセット")
layout.separator()
box = layout.box()
box.label(text="操作するカメラを選択:", icon='VIEW_CAMERA')
row = box.row(align=True)
row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 1", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_1")).cam_index = "1"
row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 2", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_2")).cam_index = "2"
row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 3", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_3")).cam_index = "3"
if props.camera_obj:
box.label(text=f"操作・描画中: {props.camera_obj.name}", icon='CAMERA_DATA')
else:
box.label(text="操作カメラ未選択", icon='ERROR')
box.separator()
box_color = box.box()
box_color.prop(props, "camera_color")
class SFC_PT_CameraAimingPanel(Panel):
bl_label = "2. 専用カメラ視線制御 (位置固定)"
bl_idname = PANEL_IDS["AIMING"]
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY_NAME
bl_order = PANEL_ORDER[PANEL_IDS["AIMING"]]
def draw(self, context):
layout = self.layout
props = context.scene.surface_camera_properties
box_manual = layout.box()
box_manual.label(text="回転・注視点のコントロール", icon='MOUSE_LMB')
if props.camera_obj:
box_manual.label(text=f"現在の位置: {tuple(round(v, 2) for v in props.camera_obj.location)} (固定)")
col_aim = box_manual.column(align=True)
row_aim = col_aim.row(align=True)
row_aim.label(text="注視点")
op_aim = row_aim.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
op_aim.targets.add().name = "aim"
op_aim.prop_group_name = "camera"
col_aim.prop(props, "target_location", text="")
box_manual.separator()
col_offset = box_manual.column(align=True)
row_offset = col_offset.row(align=True)
row_offset.label(text="視線オフセット (YPR)")
op_offset = row_offset.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
op_offset.targets.add().name = "ypr"
op_offset.prop_group_name = "camera"
col_offset.prop(props, "offset_yaw")
col_offset.prop(props, "offset_pitch")
col_offset.prop(props, "offset_roll")
class SFC_PT_ViewportCamPanel(Panel):
bl_label = "3. ビューポート視座コントロール (架空カメラ)"
bl_idname = PANEL_IDS["VIEWPORT_CAM"]
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY_NAME
bl_order = PANEL_ORDER[PANEL_IDS["VIEWPORT_CAM"]]
def draw(self, context):
layout = self.layout
props = context.scene.surface_camera_properties
box = layout.box()
box.label(text="透視投影ビューの操作", icon='VIEW3D')
col = box.column(align=True)
col.prop(props, "viewport_location")
col.prop(props, "viewport_target")
box.separator()
box.operator(SFC_OT_CopyViewportInfo.bl_idname, icon='COPYDOWN', text="視座位置・注視点をコピー")
box.operator(SFC_OT_ResetViewportCam.bl_idname, icon='LOOP_BACK', text="視座・注視点を一括リセット")
class SFC_PT_LensPanel(Panel):
bl_label = "4. レンズ設定"; bl_idname = PANEL_IDS["LENS"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["LENS"]]
def draw(self, context):
layout = self.layout
props = context.scene.surface_camera_properties
if props.camera_obj and props.camera_obj.data:
cam_data = props.camera_obj.data
box_type = layout.box()
box_type.prop(cam_data, "type", text="投影タイプ (透視/平行)")
box = layout.box()
col = box.column(align=True)
row = col.row(align=True)
row.label(text="レンズとクリップ")
op = row.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
op.targets.add().name = "clip"
op.prop_group_name = "camera"
col.prop(props, "lens_focal_length")
row = col.row(align=True)
row.label(text="水平視野角:"); row.label(text=props.info_horizontal_fov)
col.label(text="FOVプリセット:")
row = col.row(align=True)
col1, col2 = row.column(align=True), row.column(align=True)
for i, fov in enumerate(FOV_PRESETS):
op = (col1 if i % 2 == 0 else col2).operator(f"{PREFIX}.set_fov", text=f"{fov}°")
op.fov = fov
col.separator()
row = col.row(align=True)
row.prop(props, "clip_start")
row.prop(props, "clip_end")
class SFC_PT_CameraDisplayPanel(Panel):
bl_label = "Camera Display & Render"; bl_idname = PANEL_IDS["CAMERA_DISPLAY"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["CAMERA_DISPLAY"]]
def draw(self, context):
layout, scene, cam = self.layout, context.scene, context.scene.camera
box_render = layout.box(); box_render.label(text="Render Engine", icon='SCENE'); box_render.prop(scene.render, "engine", expand=True); layout.separator()
if not cam or not isinstance(cam.data, bpy.types.Camera): layout.box().label(text="シーンにアクティブなカメラがありません", icon='ERROR'); return
cam_data = cam.data; overlay = context.space_data.overlay if context.space_data and hasattr(context.space_data, 'overlay') else None
layout.label(text="Active Camera: " + cam.name, icon='CAMERA_DATA')
box_passepartout = layout.box(); box_passepartout.label(text="Passepartout", icon='MOD_MASK'); col_passepartout = box_passepartout.column(align=True); col_passepartout.prop(cam_data, "show_passepartout", text="Enable"); row_passepartout = col_passepartout.row(); row_passepartout.enabled = cam_data.show_passepartout; row_passepartout.prop(cam_data, "passepartout_alpha", text="Opacity")
layout.separator(); box_display = layout.box(); box_display.label(text="Viewport Display", icon='OVERLAY')
if not overlay: return
box_display.prop(overlay, "show_overlays", text="Viewport Overlays"); col_overlay_options = box_display.column(); col_overlay_options.enabled = overlay.show_overlays; col_overlay_options.prop(overlay, "show_extras", text="Extras")
col_details = col_overlay_options.column(); col_details.enabled = overlay.show_extras; col_details.prop(overlay, "show_text", text="Text Info"); col_details.prop(cam_data, "show_name", text="Name"); col_details.prop(cam_data, "show_limits", text="Limits")
class ZIONAD_SWT_PT_WorldControlPanel(Panel):
bl_label = "World Control"; bl_idname = PANEL_IDS["WORLD_CONTROL"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["WORLD_CONTROL"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout, scene, props = self.layout, context.scene, context.scene.zionad_swt_props; world, nodes, _ = get_world_nodes(context, create=False)
if not world or not world.use_nodes or not nodes: return
box_mode = layout.box(); box_mode.label(text="Background Mode", icon='WORLD'); box_mode.prop(props, "background_mode", expand=True); layout.separator()
if props.background_mode == 'HDRI':
box_env = layout.box(); box_env.label(text="Environment Texture (HDRI)", icon='IMAGE_DATA'); col_list = box_env.column(align=True)
for i, path in enumerate(HDRI_PATHS): op = col_list.operator(f"{PREFIX}.load_hdri_from_list", text=os.path.basename(path), depress=(props.hdri_list_index == i)); op.hdri_index = i
box_env.separator(); env_node = find_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
if env_node: box_env.template_ID(env_node, "image", open="image.open", text="Select HDRI")
elif props.background_mode == 'SKY':
box_sky = layout.box(); sky_node = find_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture')
if sky_node: box_sky.prop(sky_node, "sky_type", text="Sky Type")
class SFC_PT_GridPanel(Panel):
bl_label = "Grid Color"; bl_idname = PANEL_IDS["GRID"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["GRID"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): layout, props = self.layout, context.scene.theme_grid_properties; layout.prop(props, "grid_preset"); layout.prop(props, "grid_color"); layout.operator(f"{PREFIX}.apply_grid_color", text="Apply Grid Color")
class SFC_PT_WirePanel(Panel):
bl_label = "Wire Color"; bl_idname = PANEL_IDS["WIRE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["WIRE"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): layout, props = self.layout, context.scene.theme_wire_properties; layout.prop(props, "wire_preset"); layout.prop(props, "wire_color"); layout.operator(f"{PREFIX}.apply_wire_color", text="Apply Wire Color")
class SFC_PT_LinksPanel(Panel):
bl_label = "リンク"; bl_idname = PANEL_IDS["LINKS"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["LINKS"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
box1 = layout.box()
box1.label(text="ドキュメント", icon='HELP')
for link in NEW_DOC_LINKS:
op = box1.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
op.url = link["url"]
box2 = layout.box()
box2.label(text="ソーシャル", icon='WORLD_DATA')
for link in SOCIAL_LINKS:
op = box2.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
op.url = link["url"]
class SFC_PT_RemovePanel(Panel):
bl_label = "アドオン削除"; bl_idname = PANEL_IDS["REMOVE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["REMOVE"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): self.layout.operator(f"{PREFIX}.remove_addon", text="このアドオンを解除", icon='CANCEL')
# ======================================================================
# --- World Tools 初期化 ---
# ======================================================================
def initial_setup():
context = bpy.context
if not context.window_manager:
return 0.1
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
area.show_region_ui = True
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.type = 'MATERIAL'
if context.scene.world and context.scene.world.use_nodes:
props = context.scene.zionad_swt_props
nodes = context.scene.world.node_tree.nodes
background_node = find_node(nodes, 'ShaderNodeBackground', 'Background')
if background_node and background_node.inputs['Color'].is_linked:
source_node = background_node.inputs['Color'].links[0].from_node
if source_node.type == 'TEX_SKY': props.background_mode = 'SKY'
else: props.background_mode = 'HDRI'
update_background_mode(props, context)
return None
# ======================================================================
# --- 登録/解除 ---
# ======================================================================
classes = (
ThemeGridProperties, ThemeWireProperties, TargetProperty, SurfaceCameraProperties, ZIONAD_SWT_Properties,
SFC_OT_GridApplyColor, SFC_OT_GridCopyColor,
SFC_OT_CreateThreeCameras, SFC_OT_ResetThreeCameras, SFC_OT_ResetViewportCam, SFC_OT_SwitchCamera, SFC_OT_ResetProperty, SFC_OT_SetFOV,
SFC_OT_CopyViewportInfo, SFC_OT_OpenURL, SFC_OT_RemoveAddon,
ZIONAD_SWT_OT_LoadHdriFromList, ZIONAD_SWT_OT_ResetTransform,
SFC_PT_CameraSetupPanel, SFC_PT_CameraAimingPanel, SFC_PT_ViewportCamPanel, SFC_PT_LensPanel, SFC_PT_CameraDisplayPanel,
ZIONAD_SWT_PT_WorldControlPanel, SFC_PT_GridPanel, SFC_PT_WirePanel, SFC_PT_LinksPanel,
SFC_PT_RemovePanel,
)
_registered_classes = []
def register():
global _registered_classes
_registered_classes.clear()
for cls in classes:
try:
bpy.utils.register_class(cls)
_registered_classes.append(cls)
except Exception as e:
print(f"[REGISTER ERROR] {cls.__name__}: {e}")
bpy.types.Scene.surface_camera_properties = PointerProperty(type=SurfaceCameraProperties)
bpy.types.Scene.theme_grid_properties = PointerProperty(type=ThemeGridProperties)
bpy.types.Scene.theme_wire_properties = PointerProperty(type=ThemeWireProperties)
bpy.types.Scene.zionad_swt_props = PointerProperty(type=ZIONAD_SWT_Properties)
if on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update)
if not bpy.app.timers.is_registered(initial_setup):
bpy.app.timers.register(initial_setup, first_interval=0.1)
def unregister():
global _registered_classes
if on_depsgraph_update in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update)
if bpy.app.timers.is_registered(schedule_update_lock_reset):
bpy.app.timers.unregister(schedule_update_lock_reset)
if bpy.app.timers.is_registered(_do_update_surface_camera):
bpy.app.timers.unregister(_do_update_surface_camera)
if bpy.app.timers.is_registered(_do_update_viewport_cam):
bpy.app.timers.unregister(_do_update_viewport_cam)
if bpy.app.timers.is_registered(initial_setup):
bpy.app.timers.unregister(initial_setup)
for prop_name in ['surface_camera_properties', 'theme_grid_properties', 'theme_wire_properties', 'zionad_swt_props']:
if prop_name in bpy.types.Scene.__dict__:
try: delattr(bpy.types.Scene, prop_name)
except Exception as e: print(f"[UNREGISTER ERROR] delattr {prop_name}: {e}")
for cls in reversed(_registered_classes):
try:
bpy.utils.unregister_class(cls)
except Exception as e:
print(f"[UNREGISTER ERROR] {cls.__name__}: {e}")
_registered_classes.clear()
if __name__ == "__main__":
try: unregister()
except: pass
register()