https://posfie.com/@timekagura?sort=0&page=1
https://aistudio.google.com/prompts/1tm2fIhAtEH4AhIinhn5uEiekH3-Er0zy

import bpy
import bmesh
import math
import mathutils
import webbrowser
import os
import time
import uuid
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import StringProperty, PointerProperty, EnumProperty, FloatVectorProperty, FloatProperty, CollectionProperty, BoolProperty, IntProperty
# ======================================================================
# --- 定数管理 / CONFIG ---
# ======================================================================
class CONFIG:
PREFIX = "unit_circle_cam"
MASTER_COLLECTION = "Cam three"
CAMERA_COLLECTION = "Cam"
VP_COLLECTION = "VP_Objects"
SAVED_COLLECTION = "Saved_Objects"
SENSOR_WIDTH = 36.0
FOV_PRESETS =[1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]
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))]
ARROW_RATIO_PRESETS =[
('0.0', "0", ""),
('0.33333333', "1/3", ""),
('0.5', "1/2", ""),
('0.57735027', "1/√3 (約0.577)", ""),
('0.70710678', "1/√2 (約0.707)", ""),
('0.86602540', "√3/2 (約0.866)", ""),
('1.0', "1", ""),
('1.41421356', "√2 (約1.414)", ""),
('1.73205081', "√3 (約1.732)", ""),
('CUSTOM', "カスタム", ""),
]
NEW_DOC_LINKS =[
{"label": "時空図 光の予算配分 20260329", "url": "<https://www.notion.so/20260329-332f5dacaf438016b8f9cff480994ec1>"},
{"label": "カメラ3台 ジグザク 20260328b", "url": "<https://www.notion.so/20260328b-331f5dacaf4380b9abeed323cd5621a4>"},
{"label": "THIS_ADDON[ カメラ3台 ジグザク 20260328 ]", "url": "<https://www.notion.so/20260328-330f5dacaf43808eae2dcc7e31f14bec>"},
]
SOCIAL_LINKS =[
{"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
]
# ======================================================================
# --- アドオン情報 / Addon Info ---
# ======================================================================
bl_info = {
"name": "zionad 521[Unit Circle Cam]",
"author": "zionadchat",
"version": (41, 0, 0),
"blender": (4, 1, 0),
"location": "View3D > Sidebar > zionad Control",
"description": "【V41】矢印生成機能 (角度/成分指定)、透明球体と交差円生成",
"category": "Cam three 元型",
}
ADDON_CATEGORY_NAME = bl_info["category"]
# ======================================================================
# --- パネル管理 ---
# ======================================================================
PANEL_IDS = {
"SETUP": f"{CONFIG.PREFIX}_PT_setup", "AIMING": f"{CONFIG.PREFIX}_PT_aiming", "VIEWPORT_CAM": f"{CONFIG.PREFIX}_PT_viewport_cam",
"LENS": f"{CONFIG.PREFIX}_PT_lens", "CAMERA_DISPLAY": f"{CONFIG.PREFIX}_PT_camera_display", "WORLD_CONTROL": f"{CONFIG.PREFIX}_PT_world_control",
"GRID": f"{CONFIG.PREFIX}_PT_grid_panel", "WIRE": f"{CONFIG.PREFIX}_PT_wire_panel", "LINKS": f"{CONFIG.PREFIX}_PT_links", "REMOVE": f"{CONFIG.PREFIX}_PT_remove",
}
PANEL_ORDER = {
PANEL_IDS["SETUP"]: 0, PANEL_IDS["AIMING"]: 2, PANEL_IDS["VIEWPORT_CAM"]: 3, PANEL_IDS["LENS"]: 4,
PANEL_IDS["CAMERA_DISPLAY"]: 5, PANEL_IDS["WORLD_CONTROL"]: 6, PANEL_IDS["GRID"]: 89, PANEL_IDS["WIRE"]: 90,
PANEL_IDS["LINKS"]: 190, PANEL_IDS["REMOVE"]: 200,
}
# ======================================================================
# --- タイマー管理 & ロック機構 (安定化) ---
# ======================================================================
TIMER_REGISTRY = {}
def safe_register_timer(func, delay=0.01):
if func in TIMER_REGISTRY: return
def wrapper():
try: func()
finally: TIMER_REGISTRY.pop(func, None)
return None
TIMER_REGISTRY[func] = wrapper
bpy.app.timers.register(wrapper, first_interval=delay)
def is_updating(scene): return bool(scene.get("_sfc_updating", False)) if scene else False
def set_update_lock(scene, state: bool):
if scene: scene["_sfc_updating"] = bool(state)
def schedule_update_lock_reset():
for scene in bpy.data.scenes:
if "_sfc_updating" in scene: scene["_sfc_updating"] = False
# ======================================================================
# --- 汎用ヘルパー関数 (安全化) ---
# ======================================================================
def safe_remove_object(obj):
if not obj: return
if obj.users > 1:
for col in list(obj.users_collection): col.objects.unlink(obj)
else: bpy.data.objects.remove(obj, do_unlink=True)
def safe_link(links, out_socket, in_socket):
if in_socket.is_linked: links.remove(in_socket.links[0])
links.new(out_socket, in_socket)
def get_or_copy_material(mat, suffix):
name = f"{mat.name}_{suffix}"
existing = bpy.data.materials.get(name)
if existing: return existing
new_mat = mat.copy()
new_mat.name = name
return new_mat
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, CONFIG.MASTER_COLLECTION)
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, new_node.label = name, 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, context.scene.world = bpy.data.worlds.new("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_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))
safe_link(links, background_node.outputs['Background'], output_node.inputs['Surface'])
if mode == 'SKY': safe_link(links, sky_node.outputs['Color'], background_node.inputs['Color'])
elif mode == 'HDRI':
safe_link(links, tex_coord_node.outputs['Generated'], mapping_node.inputs['Vector'])
safe_link(links, mapping_node.outputs['Vector'], env_node.inputs['Vector'])
safe_link(links, env_node.outputs['Color'], background_node.inputs['Color'])
props = context.scene.zionad_swt_props
if 0 <= props.hdri_list_index < len(CONFIG.HDRI_PATHS): load_hdri_from_path(CONFIG.HDRI_PATHS[props.hdri_list_index], context)
wm = bpy.context.window_manager
if wm:
for window in wm.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'
# ======================================================================
# --- オブジェクト生成関数 (球体・リング・矢印) ---
# ======================================================================
def get_or_create_color_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat, mat.use_nodes = bpy.data.materials.new(name=name), True
bsdf = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None) if mat.use_nodes else None
if bsdf:
if "Base Color" in bsdf.inputs: bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs: bsdf.inputs["Alpha"].default_value = color[3]
mat.blend_method = 'BLEND'
return mat
def get_or_create_front_back_material(name, color_front, color_back):
mat = bpy.data.materials.get(name)
if not mat:
mat, mat.use_nodes = bpy.data.materials.new(name=name), True
if mat.use_nodes:
nodes, links = mat.node_tree.nodes, mat.node_tree.links
bsdf = next((n for n in nodes if n.type == 'BSDF_PRINCIPLED'), None)
if bsdf:
geom = nodes.get("Geometry") or nodes.new('ShaderNodeNewGeometry')
geom.name, geom.location = "Geometry", (bsdf.location.x - 400, bsdf.location.y + 200)
mix_rgb = nodes.get("Mix_Color") or nodes.new('ShaderNodeMix')
mix_rgb.name, mix_rgb.data_type, mix_rgb.blend_type, mix_rgb.location = "Mix_Color", 'RGBA', 'MIX', (bsdf.location.x - 200, bsdf.location.y + 200)
mix_alpha = nodes.get("Mix_Alpha") or nodes.new('ShaderNodeMix')
mix_alpha.name, mix_alpha.data_type, mix_alpha.location = "Mix_Alpha", 'FLOAT', (bsdf.location.x - 200, bsdf.location.y - 100)
safe_link(links, geom.outputs['Backfacing'], mix_rgb.inputs['Factor'])
safe_link(links, mix_rgb.outputs['Result'], bsdf.inputs['Base Color'])
safe_link(links, geom.outputs['Backfacing'], mix_alpha.inputs['Factor'])
safe_link(links, mix_alpha.outputs['Result'], bsdf.inputs['Alpha'])
mix_rgb.inputs['A'].default_value, mix_rgb.inputs['B'].default_value = color_front, color_back
mix_alpha.inputs['A'].default_value, mix_alpha.inputs['B'].default_value = color_front[3], color_back[3]
mat.blend_method = 'BLEND'
return mat
def create_sphere_object(name, collection, loc, radius, mat):
me = bpy.data.meshes.new(name); bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=radius)
bm.to_mesh(me); bm.free()
for poly in me.polygons: poly.use_smooth = True
obj = bpy.data.objects.new(name, me); obj.location = loc
if mat: obj.data.materials.append(mat)
collection.objects.link(obj)
return obj
def create_split_spheres(name_prefix, collection, loc, radius, mat1, mat2, plane):
norm, co = (mathutils.Vector((0, 0, 1)), mathutils.Vector((0, 0, -loc.z))) if plane == 'XY' else (mathutils.Vector((1, 0, 0)), mathutils.Vector((-loc.x, 0, 0))) if plane == 'YZ' else (mathutils.Vector((0, 1, 0)), mathutils.Vector((0, -loc.y, 0)))
objs, mats = [], [mat1, mat2]
for i, clear_in in enumerate([True, False]):
me = bpy.data.meshes.new(f"{name_prefix}_{i+1}"); bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=radius)
bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=norm, clear_inner=clear_in, clear_outer=not clear_in)
bm.to_mesh(me); bm.free()
if len(me.vertices) > 0:
for poly in me.polygons: poly.use_smooth = True
obj = bpy.data.objects.new(f"{name_prefix}_{i+1}", me); obj.location = loc
if mats[i]: obj.data.materials.append(mats[i])
collection.objects.link(obj); objs.append(obj)
else: bpy.data.meshes.remove(me)
return objs
def create_ring_object(name, collection, loc, rot, major_radius, minor_radius, mat):
me = bpy.data.meshes.new(name); bm = bmesh.new()
r_outer, r_inner, segments = major_radius + minor_radius, max(0.001, major_radius - minor_radius), 64
verts_outer, verts_inner = [],[]
for i in range(segments):
c, s = math.cos(2.0 * math.pi * i / segments), math.sin(2.0 * math.pi * i / segments)
verts_outer.append(bm.verts.new((r_outer * c, r_outer * s, 0.0))); verts_inner.append(bm.verts.new((r_inner * c, r_inner * s, 0.0)))
for i in range(segments): bm.faces.new((verts_outer[i], verts_outer[(i + 1) % segments], verts_inner[(i + 1) % segments], verts_inner[i]))
bm.to_mesh(me); bm.free()
for poly in me.polygons: poly.use_smooth = True
obj = bpy.data.objects.new(name, me); obj.location, obj.rotation_euler = loc, rot
if mat: obj.data.materials.append(mat)
mod = obj.modifiers.new(name="Solidify", type='SOLIDIFY'); mod.thickness, mod.offset = minor_radius * 2, 0.0
collection.objects.link(obj)
return obj
def create_arrow_object(name, collection, loc, direction, length, thickness, mat):
me = bpy.data.meshes.new(name); bm = bmesh.new()
r_shaft, r_head, r_head_len = thickness, thickness * 2.0, min(thickness * 4.0, length * 0.5)
shaft_len = length - r_head_len
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=32, radius1=r_shaft, radius2=r_shaft, depth=shaft_len)
bmesh.ops.translate(bm, verts=bm.verts[:], vec=(0, 0, shaft_len / 2.0))
geom_head = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=32, radius1=r_head, radius2=0.0, depth=r_head_len)
verts_head =[v for v in geom_head['verts']]
bmesh.ops.translate(bm, verts=verts_head, vec=(0, 0, shaft_len + r_head_len / 2.0))
bm.to_mesh(me); bm.free()
for poly in me.polygons: poly.use_smooth = True
obj = bpy.data.objects.new(name, me); obj.location = loc
if direction.length > 0.0001: obj.rotation_euler = mathutils.Vector((0, 0, 1)).rotation_difference(direction.normalized()).to_euler('XYZ')
if mat: obj.data.materials.append(mat)
collection.objects.link(obj)
return obj
def get_arrow_direction(props):
if props.arrow_mode == 'ANGLE':
theta, phi = props.arrow_angle_xy, props.arrow_angle_z
return mathutils.Vector((math.cos(phi)*math.cos(theta), math.cos(phi)*math.sin(theta), math.sin(phi)))
else:
x_val = props.arrow_ratio_x_custom if props.arrow_ratio_x_preset == 'CUSTOM' else float(props.arrow_ratio_x_preset)
y_val = props.arrow_ratio_y_custom if props.arrow_ratio_y_preset == 'CUSTOM' else float(props.arrow_ratio_y_preset)
x_val *= -1 if props.arrow_x_invert else 1
y_val *= -1 if props.arrow_y_invert else 1
sq_sum = x_val**2 + y_val**2
if sq_sum > 1.0:
scale = 1.0 / math.sqrt(sq_sum)
return mathutils.Vector((x_val * scale, y_val * scale, 0.0))
else:
z_val = math.sqrt(1.0 - sq_sum) * (-1 if props.arrow_z_invert else 1)
return mathutils.Vector((x_val, y_val, z_val))
def update_group_visibility(prefix, hide):
col = bpy.data.collections.get(CONFIG.VP_COLLECTION)
if col:
for obj in col.objects:
if obj.name.startswith(prefix): obj.hide_viewport = obj.hide_render = hide
def update_group_visibility_exact(name, hide):
col = bpy.data.collections.get(CONFIG.VP_COLLECTION)
if col and (obj := col.objects.get(name)): obj.hide_viewport = obj.hide_render = hide
def update_vis_vp_sphere_1(self, context): update_group_visibility_exact("VP_Sphere_1", not self.vis_vp_sphere_1)
def update_vis_vp_sphere_2(self, context): update_group_visibility_exact("VP_Sphere_2", not self.vis_vp_sphere_2)
def update_vis_vp_circles(self, context): update_group_visibility("VP_Circle", not self.vis_vp_circles)
def update_vis_vp_arrow(self, context): update_group_visibility("VP_Arrow", not self.vis_vp_arrow)
# ======================================================================
# --- カメラ コアロジック・プロパティ ---
# ======================================================================
def update_cam_color(self, context):
if self.camera_obj: context.preferences.themes[0].view_3d.camera = self.camera_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=lambda self, context: setattr(context.preferences.themes[0].view_3d, 'grid', self.grid_color))
grid_preset: EnumProperty(name="Grid Preset", items=[(p[0], p[1], p[2]) for p in CONFIG.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=lambda self, context:[setattr(context.preferences.themes[0].view_3d, 'wire', self.wire_color), setattr(context.preferences.themes[0].view_3d, 'object_active', self.wire_color)])
wire_preset: EnumProperty(name="Wire Preset", items=[(p[0], p[1], p[2]) for p in CONFIG.WIRE_PRESETS], update=lambda self, context: SFC_OT_WireApplyColor.update_preset(self, context))
class TargetProperty(PropertyGroup): name: StringProperty()
def _do_update_viewport_cam():
wm = bpy.context.window_manager
if not wm: return
for window in wm.windows:
scene = window.scene
if not scene: continue
props = scene.surface_camera_properties
vp_loc, vp_tgt = mathutils.Vector(props.viewport_location), 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 area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D' and space.region_3d:
set_update_lock(scene, True)
try:
if space.region_3d.view_perspective == 'CAMERA': space.region_3d.view_perspective = 'PERSP'
space.region_3d.view_location = vp_tgt
space.region_3d.view_rotation = rot_quat
space.region_3d.view_distance = direction.length
finally: safe_register_timer(schedule_update_lock_reset, 0.01)
def safe_update_viewport_cam(self, context):
if not is_updating(context.scene): safe_register_timer(_do_update_viewport_cam, 0.01)
def _do_update_surface_camera():
wm = bpy.context.window_manager
if not wm: return
for window in wm.windows:
scene = window.scene
if not scene: continue
props, camera_obj = scene.surface_camera_properties, scene.surface_camera_properties.camera_obj
set_update_lock(scene, True)
try:
if props.is_updating_settings or not camera_obj:
update_info_panel_text(props); continue
if camera_obj.data:
camera_obj.data.sensor_fit = 'HORIZONTAL'
camera_obj.data.lens_unit = 'MILLIMETERS'
camera_obj.data.lens, camera_obj.data.clip_start, camera_obj.data.clip_end = props.lens_focal_length, props.clip_start, props.clip_end
update_object_transform(camera_obj, props)
update_info_panel_text(props)
finally: safe_register_timer(schedule_update_lock_reset, 0.01)
def safe_update_surface_camera(self, context):
if not is_updating(context.scene): safe_register_timer(_do_update_surface_camera, 0.01)
def update_sphere_colors(self, context):
if getattr(self, "sync_sphere_colors", False):
c = self.sphere1_color_front
self.sphere1_color_back = c
self.sphere2_color_front = c
self.sphere2_color_back = c
def update_sync_checkbox(self, context):
if self.sync_sphere_colors: update_sphere_colors(self, context)
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, 1.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, 20.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)
vis_vp_sphere_1: BoolProperty(name="球体1", default=True, update=update_vis_vp_sphere_1)
vis_vp_sphere_2: BoolProperty(name="球体2", default=True, update=update_vis_vp_sphere_2)
vis_vp_circles: BoolProperty(name="交差円", default=True, update=update_vis_vp_circles)
vis_vp_arrow: BoolProperty(name="矢印", default=True, update=update_vis_vp_arrow)
intersect_plane: EnumProperty(name="交差平面", items=[('XY', "XY平面 (Z=0)", ""), ('YZ', "YZ平面 (X=0)", ""), ('ZX', "ZX平面 (Y=0)", "")], default='ZX')
sphere_mode: EnumProperty(name="サイズ指定モード", items=[('RADIUS', "球の半径を指定", ""), ('CIRCLE', "交差円の半径を指定", "")], default='RADIUS')
sphere_radius: FloatProperty(name="球の半径", default=10.0, min=0.001)
intersect_circle_radius: FloatProperty(name="交差円の半径", default=10.0, min=0.001)
sync_sphere_colors: BoolProperty(name="球体1の表色で全て統一", default=False, update=update_sync_checkbox)
sphere1_color_front: FloatVectorProperty(name="球体1 表色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.1, 0.5, 0.8, 0.2), update=update_sphere_colors)
sphere1_color_back: FloatVectorProperty(name="球体1 裏色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.2, 0.6, 0.9, 0.2))
sphere2_color_front: FloatVectorProperty(name="球体2 表色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.8, 0.5, 0.1, 0.2))
sphere2_color_back: FloatVectorProperty(name="球体2 裏色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.9, 0.6, 0.2, 0.2))
circle_color: FloatVectorProperty(name="交差円 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.8, 0.2, 0.1, 0.8))
circle_thickness: FloatProperty(name="交差円 太さ", default=0.05, min=0.001)
arrow_mode: EnumProperty(name="矢印の向き指定", items=[('ANGLE', "角度指定", ""), ('RATIO', "成分割合指定", "")], default='RATIO')
arrow_angle_xy: FloatProperty(name="XY平面角度", subtype='ANGLE', default=0.0)
arrow_angle_z: FloatProperty(name="Z仰角", subtype='ANGLE', default=0.0)
arrow_ratio_x_preset: EnumProperty(name="X割合", items=CONFIG.ARROW_RATIO_PRESETS, default='0.57735027')
arrow_ratio_y_preset: EnumProperty(name="Y割合", items=CONFIG.ARROW_RATIO_PRESETS, default='0.57735027')
arrow_ratio_x_custom: FloatProperty(name="X割合 (カスタム)", default=0.57735027)
arrow_ratio_y_custom: FloatProperty(name="Y割合 (カスタム)", default=0.57735027)
arrow_x_invert: BoolProperty(name="X 負", default=False)
arrow_y_invert: BoolProperty(name="Y 負", default=False)
arrow_z_invert: BoolProperty(name="Z 負", default=False)
arrow_color: FloatVectorProperty(name="矢印 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(1.0, 0.8, 0.1, 1.0))
arrow_thickness: FloatProperty(name="矢印の太さ", default=0.2, min=0.01)
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=update_cam_color)
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=CONFIG.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=CONFIG.SENSOR_WIDTH):
try: return sensor_width / (2 * math.tan(math.radians(fov_degrees) / 2))
except: return 50.0
def update_object_transform(obj, props):
direction = mathutils.Vector(props.target_location) - obj.location
if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
obj.rotation_euler = (direction.to_track_quat('-Z', 'Y') @ mathutils.Euler((props.offset_pitch, props.offset_yaw, props.offset_roll), 'XYZ').to_quaternion()).to_euler('XYZ')
def update_info_panel_text(props):
if props and props.camera_obj: props.info_horizontal_fov = f"{calculate_horizontal_fov(props.lens_focal_length):.1f} °"
def sync_ui_from_manual_transform(props, obj, scene):
if is_updating(scene): return
set_update_lock(scene, True)
try:
direction = mathutils.Vector(props.target_location) - obj.location
if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
offset_euler = (direction.to_track_quat('-Z', 'Y').inverted() @ obj.matrix_world.to_quaternion()).to_euler('XYZ')
props.offset_pitch, props.offset_yaw, props.offset_roll = offset_euler.x, offset_euler.y, offset_euler.z
finally: safe_register_timer(schedule_update_lock_reset, 0.01)
update_info_panel_text(props)
@bpy.app.handlers.persistent
def on_depsgraph_update(scene, depsgraph):
if is_updating(scene): return
sfc_props = scene.surface_camera_properties
if not sfc_props.camera_obj: return
if any(update.is_updated_transform and update.id.original == sfc_props.camera_obj for update in depsgraph.updates):
sync_ui_from_manual_transform(sfc_props, sfc_props.camera_obj, scene)
# ======================================================================
# --- オペレーター ---
# ======================================================================
def set_initial_camera_transform(obj, loc, tgt):
direction = mathutils.Vector(tgt) - mathutils.Vector(loc)
if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
obj.location, obj.rotation_euler = mathutils.Vector(loc), direction.to_track_quat('-Z', 'Y').to_euler('XYZ')
class SFC_OT_CreateThreeCameras(Operator):
bl_idname = f"{CONFIG.PREFIX}.create_three_cameras"; bl_label = "3つのカメラを生成・初期化"
def execute(self, context):
col, props = get_or_create_collection(context, CONFIG.CAMERA_COLLECTION, get_master_collection(context)), context.scene.surface_camera_properties
for idx, loc, tgt in[(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)]:
name = f"Fixed_Cam_{idx}"; cam_obj = bpy.data.objects.get(name)
if cam_obj and cam_obj.type != 'CAMERA': cam_obj.name += "_old"; cam_obj = None
if not cam_obj: cam_obj = bpy.data.objects.new(name, bpy.data.cameras.new(name=name))
if cam_obj.name not in col.objects: col.objects.link(cam_obj)
for c in list(cam_obj.users_collection):
if c != col: c.objects.unlink(cam_obj)
set_initial_camera_transform(cam_obj, loc, tgt)
getattr(getattr(bpy.ops, CONFIG.PREFIX), "switch_camera")(cam_index="1")
self.report({'INFO'}, "3つのカメラを生成しました"); return {'FINISHED'}
class SFC_OT_GetCameraInitInfo(Operator):
bl_idname = f"{CONFIG.PREFIX}.get_camera_init_info"; bl_label = "カメラの現在位置・注視点を取得"; bl_options = {'REGISTER', 'UNDO'}; cam_index: StringProperty()
def execute(self, context):
props, cam_obj = context.scene.surface_camera_properties, bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
if not cam_obj: return {'CANCELLED'}
forward_vec = mathutils.Vector((0.0, 0.0, -100.0)); forward_vec.rotate(cam_obj.rotation_euler)
setattr(props, f"cam{self.cam_index}_init_loc", cam_obj.location.copy())
setattr(props, f"cam{self.cam_index}_init_tgt", cam_obj.location.copy() + forward_vec)
return {'FINISHED'}
class SFC_OT_ResetCameraInit(Operator):
bl_idname = f"{CONFIG.PREFIX}.reset_camera_init"; bl_label = "カメラを初期値にリセット"; bl_options = {'REGISTER', 'UNDO'}; cam_index: StringProperty()
def execute(self, context):
props = context.scene.surface_camera_properties
loc, tgt = getattr(props, f"cam{self.cam_index}_init_loc"), getattr(props, f"cam{self.cam_index}_init_tgt")
cam_obj = bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
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, props.offset_yaw, props.offset_pitch, props.offset_roll = tgt, 0.0, 0.0, 0.0
props.is_updating_settings = False
return {'FINISHED'}
class SFC_OT_CopyCameraInitInfo(Operator):
bl_idname = f"{CONFIG.PREFIX}.copy_camera_init_info"; bl_label = "初期値情報をコピー"; cam_index: StringProperty()
def execute(self, context):
loc, tgt = getattr(context.scene.surface_camera_properties, f"cam{self.cam_index}_init_loc"), getattr(context.scene.surface_camera_properties, f"cam{self.cam_index}_init_tgt")
context.window_manager.clipboard = f"Cam {self.cam_index}: 位置 ({loc.x:.2f}, {loc.y:.2f}, {loc.z:.2f}) / 注視 ({tgt.x:.2f}, {tgt.y:.2f}, {tgt.z:.2f})"
return {'FINISHED'}
class SFC_OT_SetViewportToCamera(Operator):
bl_idname = f"{CONFIG.PREFIX}.set_viewport_to_camera"; bl_label = "指定カメラの視座を透視投影に適用"; bl_options = {'REGISTER', 'UNDO'}; cam_index: StringProperty()
def execute(self, context):
props, cam_obj = context.scene.surface_camera_properties, bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
if not cam_obj: return {'CANCELLED'}
forward_vec = mathutils.Vector((0.0, 0.0, -100.0)); forward_vec.rotate(cam_obj.rotation_euler)
props.viewport_location, props.viewport_target = cam_obj.location.copy(), cam_obj.location + forward_vec
return {'FINISHED'}
class SFC_OT_ResetViewportLocation(Operator):
bl_idname = f"{CONFIG.PREFIX}.reset_viewport_location"; bl_label = "視座位置をリセット"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context): context.scene.surface_camera_properties.viewport_location = (0.0, -10.0, 5.0); return {'FINISHED'}
class SFC_OT_ResetViewportTarget(Operator):
bl_idname = f"{CONFIG.PREFIX}.reset_viewport_target"; bl_label = "注視点をリセット"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context): context.scene.surface_camera_properties.viewport_target = (0.0, 0.0, 0.0); return {'FINISHED'}
class SFC_OT_CopyViewportInfo(Operator):
bl_idname = f"{CONFIG.PREFIX}.copy_viewport_info"; bl_label = "視座・注視点情報をコピー"
def execute(self, context):
loc, tgt = context.scene.surface_camera_properties.viewport_location, context.scene.surface_camera_properties.viewport_target
context.window_manager.clipboard = f"視座位置: ({loc.x:.2f}, {loc.y:.2f}, {loc.z:.2f})\n注視点: ({tgt.x:.2f}, {tgt.y:.2f}, {tgt.z:.2f})"
return {'FINISHED'}
class SFC_OT_GetViewportInfo(Operator):
bl_idname = f"{CONFIG.PREFIX}.get_viewport_info"; bl_label = "現在の視座・注視点を取得"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.surface_camera_properties
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D' and space.region_3d:
set_update_lock(context.scene, True)
try: props.viewport_location, props.viewport_target = space.region_3d.view_matrix.inverted().translation, space.region_3d.view_location
finally: safe_register_timer(schedule_update_lock_reset, 0.01)
return {'FINISHED'}
return {'CANCELLED'}
class SFC_OT_CopySphereInfo(Operator):
bl_idname = f"{CONFIG.PREFIX}.copy_sphere_info"; bl_label = "球体・円情報をコピー"
def execute(self, context):
props = context.scene.surface_camera_properties; vp_loc, plane = props.viewport_location, props.intersect_plane
d, plane_str = (abs(vp_loc.z), "XY平面 (Z=0)") if plane == 'XY' else (abs(vp_loc.x), "YZ平面 (X=0)") if plane == 'YZ' else (abs(vp_loc.y), "ZX平面 (Y=0)")
R, r_circ = (props.sphere_radius, math.sqrt(max(0, props.sphere_radius**2 - d**2))) if props.sphere_mode == 'RADIUS' else (math.sqrt(d**2 + props.intersect_circle_radius**2), props.intersect_circle_radius)
context.window_manager.clipboard = f"視座位置: ({vp_loc.x:.3f}, {vp_loc.y:.3f}, {vp_loc.z:.3f})\n交差平面: {plane_str}\n平面までの距離: {d:.3f}\n球の半径: {R:.3f}\n交差円の半径: {r_circ:.3f}"
return {'FINISHED'}
class SFC_OT_GenerateViewportSphere(Operator):
bl_idname = f"{CONFIG.PREFIX}.generate_viewport_sphere"; bl_label = "透明球体と交差円を生成"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.surface_camera_properties
col = get_or_create_collection(context, CONFIG.VP_COLLECTION, get_master_collection(context))
for obj in[o for o in col.objects if o.name.startswith("VP_Sphere") or o.name.startswith("VP_Circle") or o.name.startswith("VP_Arrow")]: safe_remove_object(obj)
vp_loc, plane = mathutils.Vector(props.viewport_location), props.intersect_plane
d, circle_loc, circle_rot = (abs(vp_loc.z), mathutils.Vector((vp_loc.x, vp_loc.y, 0.0)), mathutils.Euler((0.0, 0.0, 0.0), 'XYZ')) if plane == 'XY' else (abs(vp_loc.x), mathutils.Vector((0.0, vp_loc.y, vp_loc.z)), mathutils.Euler((0.0, math.pi/2, 0.0), 'XYZ')) if plane == 'YZ' else (abs(vp_loc.y), mathutils.Vector((vp_loc.x, 0.0, vp_loc.z)), mathutils.Euler((math.pi/2, 0.0, 0.0), 'XYZ'))
R, r_circ = (props.sphere_radius, math.sqrt(max(0, props.sphere_radius**2 - d**2))) if props.sphere_mode == 'RADIUS' else (math.sqrt(d**2 + props.intersect_circle_radius**2), props.intersect_circle_radius)
mat1 = get_or_create_front_back_material("Mat_VP_Sphere_1", props.sphere1_color_front, props.sphere1_color_back)
mat2 = get_or_create_front_back_material("Mat_VP_Sphere_2", props.sphere2_color_front, props.sphere2_color_back)
mat_c = get_or_create_color_material("Mat_VP_Circle", props.circle_color)
mat_arrow = get_or_create_color_material("Mat_VP_Arrow", props.arrow_color)
if r_circ > 0.001:
create_split_spheres("VP_Sphere", col, vp_loc, R, mat1, mat2, plane)
create_ring_object("VP_Circle", col, circle_loc, circle_rot, r_circ, props.circle_thickness, mat_c)
else:
create_sphere_object("VP_Sphere_1", col, vp_loc, R, mat1)
# 矢印生成
direction = get_arrow_direction(props)
create_arrow_object("VP_Arrow", col, vp_loc, direction, R, props.arrow_thickness, mat_arrow)
update_group_visibility_exact("VP_Sphere_1", not props.vis_vp_sphere_1)
update_group_visibility_exact("VP_Sphere_2", not props.vis_vp_sphere_2)
update_group_visibility("VP_Circle", not props.vis_vp_circles)
update_group_visibility("VP_Arrow", not props.vis_vp_arrow)
return {'FINISHED'}
class SFC_OT_DetachSpheres(Operator):
bl_idname = f"{CONFIG.PREFIX}.detach_spheres"; bl_label = "アドオンから切り離して残す"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col = bpy.data.collections.get(CONFIG.VP_COLLECTION)
if not col: return {'CANCELLED'}
saved_col = get_or_create_collection(context, CONFIG.SAVED_COLLECTION, get_master_collection(context))
objs_to_detach =[obj for obj in col.objects if obj.name.startswith("VP_Sphere") or obj.name.startswith("VP_Circle") or obj.name.startswith("VP_Arrow")]
if not objs_to_detach: return {'CANCELLED'}
suffix = str(uuid.uuid4())[:6]
for obj in objs_to_detach:
obj.name = obj.name.replace("VP_", f"Saved_{suffix}_")
if obj.data.materials:
new_mats =[get_or_copy_material(mat, suffix) for mat in obj.data.materials if mat]
obj.data.materials.clear()
for nm in new_mats: obj.data.materials.append(nm)
saved_col.objects.link(obj)
col.objects.unlink(obj)
self.report({'INFO'}, f"{len(objs_to_detach)} 個のオブジェクトを保存しました")
return {'FINISHED'}
class SFC_OT_SwitchCamera(Operator):
bl_idname = f"{CONFIG.PREFIX}.switch_camera"; bl_label = "カメラを切り替え"; cam_index: StringProperty()
def execute(self, context):
props, cam_obj = context.scene.surface_camera_properties, bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
if not cam_obj or cam_obj.type != 'CAMERA': return {'CANCELLED'}
props.is_updating_settings, props.camera_obj, context.scene.camera = True, cam_obj, 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
props.lens_focal_length, props.clip_start, props.clip_end = cam_obj.data.lens, cam_obj.data.clip_start, cam_obj.data.clip_end
forward_vec = mathutils.Vector((0.0, 0.0, -100.0)); forward_vec.rotate(cam_obj.rotation_euler)
props.target_location, props.offset_yaw, props.offset_pitch, props.offset_roll = cam_obj.location + forward_vec, 0.0, 0.0, 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"{CONFIG.PREFIX}.apply_grid_color"; bl_label = "Apply Grid Color"
def execute(self, context): bpy.context.preferences.themes[0].view_3d.grid = context.scene.theme_grid_properties.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 CONFIG.GRID_PRESETS if p[0] == props.grid_preset), props.grid_color)
getattr(bpy.ops, f"{CONFIG.PREFIX}.apply_grid_color")()
class SFC_OT_GridCopyColor(Operator):
bl_idname = f"{CONFIG.PREFIX}.copy_grid_color"; bl_label = "Copy Grid Color"
def execute(self, context): context.window_manager.clipboard = f'("CUSTOM", "Custom", "Custom grid color", {tuple(round(c, 3) for c in bpy.context.preferences.themes[0].view_3d.grid)}),'; return {'FINISHED'}
class SFC_OT_ResetProperty(Operator):
bl_idname = f"{CONFIG.PREFIX}.reset_property"; bl_label = "プロパティリセット"; targets: CollectionProperty(type=TargetProperty); prop_group_name: StringProperty()
def execute(self, context):
props = context.scene.surface_camera_properties
groups = {"ypr":["offset_yaw", "offset_pitch", "offset_roll"], "aim":["target_location"], "clip":["clip_start", "clip_end", "lens_focal_length"]}
to_reset = set(p for t in self.targets for p in (groups.get(t.name,[]) if t.name != "all" else sum(groups.values(),[])))
props.is_updating_settings = True
for p in 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"{CONFIG.PREFIX}.set_fov"; bl_label = "FOV設定"; fov: FloatProperty(default=0.0)
def execute(self, context): context.scene.surface_camera_properties.lens_focal_length = calculate_focal_length(self.fov); return {'FINISHED'}
class SFC_OT_OpenURL(Operator):
bl_idname = f"{CONFIG.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"{CONFIG.PREFIX}.remove_addon"; bl_label = "アドオン解除"
def execute(self, context): bpy.ops.preferences.addon_disable(module=__name__.split('.')[0]); unregister(); return {'FINISHED'}
class SFC_OT_WireApplyColor(Operator):
bl_idname = f"{CONFIG.PREFIX}.apply_wire_color"; bl_label = "Apply Wire Color"
def execute(self, context): t, c = bpy.context.preferences.themes[0].view_3d, context.scene.theme_wire_properties.wire_color; t.wire = t.object_active = c; return {'FINISHED'}
@staticmethod
def update_preset(self, context):
props = context.scene.theme_wire_properties
props.wire_color = next((p[3] for p in CONFIG.WIRE_PRESETS if p[0] == props.wire_preset), props.wire_color)
getattr(bpy.ops, f"{CONFIG.PREFIX}.apply_wire_color")()
class SFC_OT_WireCopyColor(Operator):
bl_idname = f"{CONFIG.PREFIX}.copy_wire_color"; bl_label = "Copy Wire Color"
def execute(self, context): context.window_manager.clipboard = f'("CUSTOM", "Custom", "Custom wire color", {tuple(round(c, 2) for c in bpy.context.preferences.themes[0].view_3d.wire)}),'; return {'FINISHED'}
class ZIONAD_SWT_OT_LoadHdriFromList(Operator):
bl_idname = f"{CONFIG.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(CONFIG.HDRI_PATHS):
props.hdri_list_index, props.background_mode = self.hdri_index, 'HDRI'
load_hdri_from_path(CONFIG.HDRI_PATHS[self.hdri_index], context); update_background_mode(props, context)
return {'FINISHED'}
class ZIONAD_SWT_OT_ResetTransform(Operator):
bl_idname = f"{CONFIG.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 nodes and (mn := find_node(nodes, 'ShaderNodeMapping', 'Mapping')):
mn.inputs[self.property_to_reset].default_value = (1, 1, 1) if self.property_to_reset == 'Scale' else (0, 0, 0)
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, props = self.layout, 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:
for idx in ["1", "2", "3"]:
b = box_init.box(); b.label(text=f"Cam {idx} 初期値"); col = b.column(align=True); col.prop(props, f"cam{idx}_init_loc", text="位置"); col.prop(props, f"cam{idx}_init_tgt", text="注視")
row_ops = b.row(align=True); row_ops.operator(SFC_OT_GetCameraInitInfo.bl_idname, text="取得", icon='RESTRICT_VIEW_OFF').cam_index = idx; row_ops.operator(SFC_OT_ResetCameraInit.bl_idname, text="リセット", icon='LOOP_BACK').cam_index = idx; row_ops.operator(SFC_OT_CopyCameraInitInfo.bl_idname, text="コピー", icon='COPYDOWN').cam_index = idx
layout.separator(); box = layout.box(); box.label(text="操作するカメラを選択:", icon='VIEW_CAMERA'); row = box.row(align=True)
for i in["1", "2", "3"]: row.operator(SFC_OT_SwitchCamera.bl_idname, text=f"Cam {i}", depress=(props.camera_obj and props.camera_obj.name==f"Fixed_Cam_{i}")).cam_index = i
box.label(text=f"操作・描画中: {props.camera_obj.name}" if props.camera_obj else "操作カメラ未選択", icon='CAMERA_DATA' if props.camera_obj else 'ERROR')
box.separator(); box.box().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, props = self.layout, context.scene.surface_camera_properties
box = layout.box(); box.label(text="回転・注視点のコントロール", icon='MOUSE_LMB')
if props.camera_obj: box.label(text=f"現在の位置: {tuple(round(v, 2) for v in props.camera_obj.location)} (固定)")
col1 = box.column(align=True); r1 = col1.row(align=True); r1.label(text="注視点"); r1.operator(f"{CONFIG.PREFIX}.reset_property", text="", icon='LOOP_BACK').targets.add().name = "aim"; col1.prop(props, "target_location", text="")
box.separator(); col2 = box.column(align=True); r2 = col2.row(align=True); r2.label(text="視線オフセット (YPR)"); r2.operator(f"{CONFIG.PREFIX}.reset_property", text="", icon='LOOP_BACK').targets.add().name = "ypr"
for p in["offset_yaw", "offset_pitch", "offset_roll"]: col2.prop(props, p)
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, props = self.layout, context.scene.surface_camera_properties
box = layout.box(); box.label(text="透視投影ビューの操作", icon='VIEW3D'); box.operator(SFC_OT_GetViewportInfo.bl_idname, icon='RESTRICT_VIEW_OFF', text="現在の視座・注視点を取得")
row = box.row(align=True)
for i in ["1", "2", "3"]: row.operator(SFC_OT_SetViewportToCamera.bl_idname, text=f"Cam{i} 視座へ", icon='CAMERA_DATA').cam_index = i
col = box.column(align=True); col.prop(props, "viewport_location"); col.prop(props, "viewport_target")
box.separator(); row_vp = box.row(align=True); row_vp.operator(SFC_OT_CopyViewportInfo.bl_idname, icon='COPYDOWN', text="コピー"); row_vp.operator(SFC_OT_ResetViewportLocation.bl_idname, icon='LOOP_BACK', text="位置リセット"); row_vp.operator(SFC_OT_ResetViewportTarget.bl_idname, icon='LOOP_BACK', text="注視リセット")
layout.separator(); box_vis = layout.box(); box_vis.label(text="生成オブジェクト 表示 / 非表示", icon='RESTRICT_VIEW_OFF'); row_vis = box_vis.row(align=True)
for p, t in[("vis_vp_sphere_1", "球体1"), ("vis_vp_sphere_2", "球体2"), ("vis_vp_circles", "交差円"), ("vis_vp_arrow", "矢印")]: row_vis.prop(props, p, text=t, toggle=True)
layout.separator(); box_sp = layout.box(); box_sp.label(text="透明球体 & 交差平面の円", icon='SPHERE'); col_sp = box_sp.column(align=True)
col_sp.prop(props, "intersect_plane"); col_sp.prop(props, "sphere_mode"); col_sp.prop(props, "sphere_radius" if props.sphere_mode == 'RADIUS' else "intersect_circle_radius")
col_sp.separator()
col_sp.prop(props, "sync_sphere_colors")
col_sp.prop(props, "sphere1_color_front", text="球1 表色")
col_b1 = col_sp.column(align=True); col_b1.enabled = not props.sync_sphere_colors; col_b1.prop(props, "sphere1_color_back", text="球1 裏色")
col_f2 = col_sp.column(align=True); col_f2.enabled = not props.sync_sphere_colors; col_f2.prop(props, "sphere2_color_front", text="球2 表色")
col_b2 = col_sp.column(align=True); col_b2.enabled = not props.sync_sphere_colors; col_b2.prop(props, "sphere2_color_back", text="球2 裏色")
col_sp.separator()
r_circ = col_sp.row(align=True); r_circ.prop(props, "circle_thickness"); r_circ.prop(props, "circle_color", text="")
# --- 矢印設定 ---
layout.separator()
box_arrow = layout.box()
box_arrow.label(text="矢印設定 (中心 → 表面)", icon='EMPTY_SINGLE_ARROW')
box_arrow.prop(props, "arrow_mode", expand=True)
if props.arrow_mode == 'ANGLE':
col_a = box_arrow.column(align=True)
col_a.prop(props, "arrow_angle_xy", text="XY平面 回転角度")
col_a.prop(props, "arrow_angle_z", text="Z軸 仰角")
else:
col_r = box_arrow.column(align=True)
r_x = col_r.row(align=True)
r_x.prop(props, "arrow_ratio_x_preset", text="X割合")
if props.arrow_ratio_x_preset == 'CUSTOM': r_x.prop(props, "arrow_ratio_x_custom", text="")
r_x.prop(props, "arrow_x_invert", text="負", toggle=True)
r_y = col_r.row(align=True)
r_y.prop(props, "arrow_ratio_y_preset", text="Y割合")
if props.arrow_ratio_y_preset == 'CUSTOM': r_y.prop(props, "arrow_ratio_y_custom", text="")
r_y.prop(props, "arrow_y_invert", text="負", toggle=True)
r_z = col_r.row(align=True)
x_val = props.arrow_ratio_x_custom if props.arrow_ratio_x_preset == 'CUSTOM' else float(props.arrow_ratio_x_preset)
y_val = props.arrow_ratio_y_custom if props.arrow_ratio_y_preset == 'CUSTOM' else float(props.arrow_ratio_y_preset)
sq_sum = x_val**2 + y_val**2
if sq_sum > 1.0: z_str = "0.000 (X,Yを正規化)"
else: z_str = f"{math.sqrt(1.0 - sq_sum):.3f} (自動計算)"
r_z.label(text=f"Z割合: {z_str}")
r_z.prop(props, "arrow_z_invert", text="負", toggle=True)
r_ac = box_arrow.row(align=True)
r_ac.prop(props, "arrow_thickness")
r_ac.prop(props, "arrow_color", text="")
vp_loc, plane = props.viewport_location, props.intersect_plane
d = abs(vp_loc.z) if plane == 'XY' else abs(vp_loc.x) if plane == 'YZ' else abs(vp_loc.y)
R, r_circ = (props.sphere_radius, math.sqrt(max(0, props.sphere_radius**2 - d**2))) if props.sphere_mode == 'RADIUS' else (math.sqrt(d**2 + props.intersect_circle_radius**2), props.intersect_circle_radius)
col_i = box_arrow.column(align=True); col_i.label(text=f"平面までの距離: {d:.2f}"); col_i.label(text=f"球の半径: {R:.2f}"); col_i.label(text=f"交差円 半径: {r_circ:.2f}")
box_arrow.separator(); col_g = box_arrow.column(align=True); col_g.operator(SFC_OT_GenerateViewportSphere.bl_idname, icon='MESH_UVSPHERE'); col_g.operator(SFC_OT_CopySphereInfo.bl_idname, icon='COPYDOWN'); col_g.operator(SFC_OT_DetachSpheres.bl_idname, icon='UNLINKED')
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, props = self.layout, context.scene.surface_camera_properties
if props.camera_obj and props.camera_obj.data: layout.box().prop(props.camera_obj.data, "type", text="投影タイプ")
box = layout.box(); col = box.column(align=True); r = col.row(align=True); r.label(text="レンズとクリップ"); r.operator(f"{CONFIG.PREFIX}.reset_property", text="", icon='LOOP_BACK').targets.add().name = "clip"
col.prop(props, "lens_focal_length"); r2 = col.row(align=True); r2.label(text="水平視野角:"); r2.label(text=props.info_horizontal_fov); col.label(text="FOVプリセット:")
r3 = col.row(align=True); c1, c2 = r3.column(align=True), r3.column(align=True)
for i, fov in enumerate(CONFIG.FOV_PRESETS): (c1 if i % 2 == 0 else c2).operator(f"{CONFIG.PREFIX}.set_fov", text=f"{fov}°").fov = fov
col.separator(); r4 = col.row(align=True); r4.prop(props, "clip_start"); r4.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
layout.box().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, overlay = cam.data, getattr(context.space_data, 'overlay', None)
layout.label(text="Active Camera: " + cam.name, icon='CAMERA_DATA'); bp = layout.box(); bp.label(text="Passepartout", icon='MOD_MASK'); cp = bp.column(align=True); cp.prop(cam_data, "show_passepartout"); rp = cp.row(); rp.enabled = cam_data.show_passepartout; rp.prop(cam_data, "passepartout_alpha")
if not overlay: return
layout.separator(); bd = layout.box(); bd.label(text="Viewport Display", icon='OVERLAY'); bd.prop(overlay, "show_overlays"); co = bd.column(); co.enabled = overlay.show_overlays; co.prop(overlay, "show_extras"); cd = co.column(); cd.enabled = overlay.show_extras; cd.prop(overlay, "show_text"); cd.prop(cam_data, "show_name"); cd.prop(cam_data, "show_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, props = self.layout, 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
bm = layout.box(); bm.label(text="Background Mode", icon='WORLD'); bm.prop(props, "background_mode", expand=True); layout.separator()
if props.background_mode == 'HDRI':
be = layout.box(); be.label(text="Environment Texture", icon='IMAGE_DATA'); cl = be.column(align=True)
for i, path in enumerate(CONFIG.HDRI_PATHS): cl.operator(f"{CONFIG.PREFIX}.load_hdri_from_list", text=os.path.basename(path), depress=(props.hdri_list_index == i)).hdri_index = i
be.separator(); en = find_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
if en: be.template_ID(en, "image", open="image.open", text="Select HDRI")
elif props.background_mode == 'SKY':
bs = layout.box(); sn = find_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture')
if sn: bs.prop(sn, "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"{CONFIG.PREFIX}.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"{CONFIG.PREFIX}.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; b1 = layout.box(); b1.label(text="ドキュメント", icon='HELP')
for link in CONFIG.NEW_DOC_LINKS: b1.operator(f"{CONFIG.PREFIX}.open_url", text=link["label"], icon='URL').url = link["url"]
b2 = layout.box(); b2.label(text="ソーシャル", icon='WORLD_DATA')
for link in CONFIG.SOCIAL_LINKS: b2.operator(f"{CONFIG.PREFIX}.open_url", text=link["label"], icon='URL').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"{CONFIG.PREFIX}.remove_addon", icon='CANCEL')
# ======================================================================
# --- 登録/解除 ---
# ======================================================================
def initial_setup():
wm = bpy.context.window_manager
if not wm: return 0.1
for window in wm.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 bpy.context.scene.world and bpy.context.scene.world.use_nodes:
props = bpy.context.scene.zionad_swt_props
nodes = bpy.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
props.background_mode = 'SKY' if source_node.type == 'TEX_SKY' else 'HDRI'
update_background_mode(props, bpy.context)
return None
classes = (
ThemeGridProperties, ThemeWireProperties, TargetProperty, SurfaceCameraProperties, ZIONAD_SWT_Properties,
SFC_OT_GridApplyColor, SFC_OT_GridCopyColor, SFC_OT_CreateThreeCameras, SFC_OT_GetCameraInitInfo, SFC_OT_ResetCameraInit, SFC_OT_CopyCameraInitInfo,
SFC_OT_SetViewportToCamera, SFC_OT_ResetViewportLocation, SFC_OT_ResetViewportTarget, SFC_OT_SwitchCamera, SFC_OT_ResetProperty, SFC_OT_SetFOV,
SFC_OT_CopyViewportInfo, SFC_OT_GetViewportInfo, SFC_OT_CopySphereInfo, SFC_OT_GenerateViewportSphere, SFC_OT_DetachSpheres,
SFC_OT_OpenURL, SFC_OT_RemoveAddon, SFC_OT_WireApplyColor, SFC_OT_WireCopyColor, 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():
_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)
safe_register_timer(initial_setup, 0.1)
def unregister():
if on_depsgraph_update in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update)
for wrapper in list(TIMER_REGISTRY.values()):
if bpy.app.timers.is_registered(wrapper): bpy.app.timers.unregister(wrapper)
TIMER_REGISTRY.clear()
for prop_name in['surface_camera_properties', 'theme_grid_properties', 'theme_wire_properties', 'zionad_swt_props']:
if hasattr(bpy.types.Scene, prop_name): delattr(bpy.types.Scene, prop_name)
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()