https://posfie.com/@timekagura?sort=0&page=1
アドオン追加 エディター コピーアドオン 原型 20260326
https://note.com/zionadmillion/n/n5d271c94fa74
bl_info = {
"name": "zionad 520[ Sq-Torus ] SquareTorus20260324",
"author": "zionadchat",
"version": (7, 0, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": "Topology-Perfect Square Torus Generator & Script Loader",
"category": "3D View",
}
import bpy
import bmesh
import webbrowser
import math
import mathutils
import time
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime
# ==============================================================================
# 【 基本設定エリア 】
# ==============================================================================
PREFIX = "SquareTorus20260324"
ADDON_NAME = "zionad 520[ Sq-Torus ]"
TAB_NAME = "[ addon text editor ] "
PANEL_TITLE = "Square Torus Generator"
AUTHOR = "zionadchat"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SQUARE_TORUS_2026_03_24_V7_FINAL ###"
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
ADDON_LINKS = (
{"label": "Prefix トーラス正方形 20260324", "url": "<https://www.notion.so/Prefix-20260324-32df5dacaf4380528980db6a989d6306>"},
)
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"show_preview": True,
"show_square_guide": True,
"torus_color": (0.0391, 0.8000, 0.1647, 0.8000),
"torus_loc": (0.0000, 0.0000, 0.0000),
"torus_rot": (0.0000, 30.0000, 0.0000),
"square_size": 10.0000,
"corner_radius": 0.0000,
"minor_radius": 0.5000,
"corner_segments": 8,
"minor_segments": 16,
"torus_plane": "XY",
}
# <END_DICT>
# ==============================================================================
# 【 内包する追加スクリプトの文字列定義 】
# ※以下の指定された場所に、対象のスクリプトをペーストしてください。
# ==============================================================================
# ① 図形ジェネレーター (b5200_zukkei...) のコードを以下に貼り付け
ZUKKEI_SCRIPT_CONTENT = r'''
# ▼▼▼ ここに「図形&配列ジェネレーター」の全コードを貼り付けてください ▼▼▼
#20260319 合体版
import bpy
import bmesh
import math
import random
import datetime
import webbrowser
from mathutils import Vector, Euler, Matrix
from bpy.props import FloatProperty, FloatVectorProperty, EnumProperty, IntProperty, BoolProperty, StringProperty, PointerProperty
from bpy.types import Operator, Panel, PropertyGroup
# ==============================================================================
# 設定エリア & ID管理
# ==============================================================================
PREFIX = "B5200_Zukkei_Array_View_20260319_v2"
TAB_NAME = " b5200[ 図形作成 ] "
OP_PREFIX = "b200_zukkei"
PROPS_NAME = f"{OP_PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: B5200_ZUKKEI_ARRAY_6_16_0 ###"
# 透視投影 視座関連の定数パラメーター
VIEW_RESET_BTN_TEXT = "Reset View (0, -10, 10)"
VIEW_POS_INIT = (0.0, -10.0, 10.0)
bl_info = {
"name": f"zionad b5200 Zukkei & Array & View {PREFIX}",
"author": "zionadchat",
"version": (6, 17, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar",
"description": "視座コントロール & リアルタイムプレビュー対応 実体化切り離し機能付き図形・配列ジェネレーター",
"category": "3D View",
}
# ==============================================================================
# リンク集
# ==============================================================================
THIS_LINKS =[
{"label": "b5200 図形作成 20260317版", "url": "<https://www.notion.so/b5200-20260317-326f5dacaf4380b4ad6afb3fe0f9e619>"},
{"label": "b200 図形作成 20250721", "url": "<https://memo2017.hatenablog.com/entry/2025/07/21/115312>"},
{"label": "カメラ 固定 Git 管理 20250711", "url": "<https://memo2017.hatenablog.com/entry/2025/07/11/131157>"},
]
NEW_DOC_LINKS =[
{"label": "blender アドオン 公開", "url": "<https://ivory-handsaw-95b.notion.site/blender-230b3deba7a280d7b610e0e3cdc178da>"},
{"label": "完成品 目次", "url": "<https://mokuji000zionad.hatenablog.com/entry/2025/05/30/135936>"},
]
DOC_LINKS =[
{"label": "812 地球儀 経度 緯度でのコントロール 20250302", "url": "<https://sortphotos2025.hatenablog.jp/entry/2025/03/02/211757>"},
{"label": "アドオン目次 from 20250227", "url": "<https://sortphotos2025.hatenablog.jp/entry/2025/02/27/201251>"},
]
SOCIAL_LINKS =[
{"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
{"label": "Posfie zionad2022", "url": "<https://posfie.com/t/zionad2022>"},
{"label": "X (Twitter) zionadchat", "url": "<https://x.com/zionadchat>"},
]
# ==============================================================================
# デフォルト値設定 (コピー機能で書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"slider_limit": 300.0000,
"view_pos": (0.0000, -10.0000, 10.0000),
"show_preview": True,
"collection_name": "MyCollection",
"sub_collection_name": "Generated_Shapes",
"object_name_prefix": "Shape",
"primitive_type": "CUBE",
"use_solidify": False,
"solidify_thickness": 0.1000,
"cube_size": 2.0000,
"cuboid_dimensions": (2.0000, 2.0000, 2.0000),
"plane_size_x": 2.0000,
"plane_size_y": 2.0000,
"circle_radius": 1.0000,
"circle_vertices": 32,
"ellipse_radius_x": 1.0000,
"ellipse_radius_y": 0.5000,
"ellipse_vertices": 64,
"uv_sphere_radius": 1.0000,
"uv_sphere_segments": 32,
"uv_sphere_rings": 16,
"ico_sphere_radius": 1.0000,
"ico_sphere_subdivisions": 2,
"cylinder_radius": 1.0000,
"cylinder_mode": "CENTER",
"cylinder_depth": 2.0000,
"cylinder_point_top": (0.0000, 0.0000, 2.0000),
"cylinder_point_bottom": (0.0000, 0.0000, 0.0000),
"cylinder_vertices": 32,
"cylinder_cap_top": False,
"cylinder_cap_bottom": False,
"frustum_mode": "CENTER",
"frustum_radius_top": 0.0000,
"frustum_radius_bottom": 1.0000,
"frustum_height": 2.0000,
"frustum_vertices": 32,
"frustum_cap_top": False,
"frustum_cap_bottom": False,
"frustum_point_top": (0.0000, 0.0000, 2.0000),
"frustum_point_bottom": (0.0000, 0.0000, 0.0000),
"image_cylinder_radius": 30.0000,
"image_cylinder_depth": 60.0000,
"image_cylinder_vertices": 64,
"image_cylinder_uv_offset": (0.0000, 0.0000),
"image_cylinder_uv_scale": (1.0000, 1.0000),
"image_cylinder_alpha_outer": 0.0000,
"image_cylinder_alpha_inner": 1.0000,
"cube_frame_size": 2.0000,
"cube_frame_radius": 0.0500,
"cube_frame_vertices": 16,
"cuboid_frame_dimensions": (2.0000, 2.0000, 2.0000),
"cuboid_frame_radius": 0.0500,
"cuboid_frame_vertices": 16,
"torus_major_radius": 1.0000,
"torus_minor_radius": 0.2500,
"torus_major_segments": 48,
"torus_minor_segments": 12,
"monkey_size": 1.0000,
"sphere_spacing": 0.5000,
"sphere_elem_radius": 0.1000,
"sphere_size": 4.0000,
"sphere_radius_val": 2.0000,
"sphere_segments": 12,
"sphere_rings": 6,
"torus_count": 5,
"torus_spacing": 1.0000,
"torus_minor_radius_arr": 0.1000,
"grid_count_x": 5,
"grid_count_y": 5,
"grid_count_z": 5,
"grid_spacing_x": 1.0000,
"grid_spacing_y": 1.0000,
"grid_spacing_z": 1.0000,
"grid_radius": 0.0500,
"grid_vertices": 16,
"location": (0.0000, 0.0000, 0.0000),
"scale_uniform": True,
"scale_factor": 1.0000,
"scale_vector": (1.0000, 1.0000, 1.0000),
"additional_rotation_x": 0.0000,
"additional_rotation_y": 0.0000,
"additional_rotation_z": 0.0000,
"color_mode": "PRESET",
"preset_set": "A",
"preset_color": "1",
"custom_color": (0.8000, 0.8000, 0.8000, 1.0000),
"face_alpha": 1.0000,
"preset_color_1": (0.9000, 0.2000, 0.2000, 1.0000),
"preset_color_2": (1.0000, 0.5000, 0.1000, 1.0000),
"preset_color_3": (1.0000, 0.8000, 0.0000, 1.0000),
"preset_color_4": (0.4000, 0.8000, 0.2000, 1.0000),
"preset_color_5": (0.1000, 0.7000, 0.8000, 1.0000),
"preset_color_6": (0.2000, 0.4000, 0.9000, 1.0000),
"preset_color_7": (0.6000, 0.3000, 0.8000, 1.0000),
"preset_color_8": (1.0000, 0.9000, 0.6000, 1.0000),
"preset_color_9": (0.1000, 0.5000, 0.4000, 1.0000),
"preset_color_10": (0.7000, 0.4000, 0.2000, 1.0000),
"preset_color_b_1": (0.9000, 0.1000, 0.1000, 1.0000),
"preset_color_b_2": (1.0000, 0.5000, 0.2000, 1.0000),
"preset_color_b_3": (0.9000, 0.9000, 0.1000, 1.0000),
"preset_color_b_4": (0.4000, 0.9000, 0.1000, 1.0000),
"preset_color_b_5": (0.1000, 0.9000, 0.9000, 1.0000),
"preset_color_b_6": (0.1000, 0.1000, 0.9000, 1.0000),
"preset_color_b_7": (0.6000, 0.1000, 0.9000, 1.0000),
"preset_color_b_8": (0.9500, 0.9500, 0.8000, 1.0000),
"preset_color_b_9": (0.1000, 0.6000, 0.6000, 1.0000),
"preset_color_b_10": (0.2500, 0.5000, 0.1500, 1.0000),
"preset_color_c_1": (0.6000, 1.0000, 0.6000, 1.0000),
"preset_color_c_2": (0.3000, 0.9000, 0.4000, 1.0000),
"preset_color_c_3": (0.1000, 0.7000, 0.2000, 1.0000),
"preset_color_c_4": (0.0000, 0.5000, 0.1000, 1.0000),
"preset_color_c_5": (0.5000, 0.8000, 0.2000, 1.0000),
"preset_color_c_6": (0.2000, 0.6000, 0.5000, 1.0000),
"preset_color_c_7": (0.7000, 0.9000, 0.3000, 1.0000),
"preset_color_c_8": (0.4000, 0.7000, 0.7000, 1.0000),
"preset_color_c_9": (0.0000, 0.8000, 0.6000, 1.0000),
"preset_color_c_10": (0.1000, 0.3000, 0.1000, 1.0000),
"show_main_docs": True,
"show_new_docs": True,
"show_old_docs": False,
"show_social": False,
}
# <END_DICT>
# ==============================================================================
# 透視投影 ビュー同期ロジック
# ==============================================================================
_is_updating_view = False
def update_view_position(self, context):
global _is_updating_view
if _is_updating_view: return
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
limit = props.slider_limit
v = list(props.view_pos)
clamped = False
for i in range(3):
if v[i] > limit: v[i] = limit; clamped = True
elif v[i] < -limit: v[i] = -limit; clamped = True
if clamped:
_is_updating_view = True
props.view_pos = v
_is_updating_view = False
_is_updating_view = True
try:
cam_pos = Vector(props.view_pos)
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':
r3d = space.region_3d
r3d.view_perspective = 'PERSP'
target_pos = Vector(r3d.view_location)
rel_pos = cam_pos - target_pos
dist = rel_pos.length
if dist > 0.001:
r3d.view_distance = dist
r3d.view_rotation = rel_pos.to_track_quat('Z', 'Y')
finally:
_is_updating_view = False
def view_sync_timer():
global _is_updating_view
if _is_updating_view: return 0.05
context = bpy.context
if getattr(context, "scene", None) is None: return 0.05
props = getattr(context.scene, PROPS_NAME, None)
if not props: return 0.05
r3d = None
target_area = None
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':
r3d = space.region_3d
target_area = area
break
if r3d: break
if r3d: break
if r3d and target_area:
target_pos = Vector(r3d.view_location)
actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
current_pos = Vector(props.view_pos)
if (current_pos - actual_cam_pos).length > 0.001:
_is_updating_view = True
max_val = max(abs(actual_cam_pos.x), abs(actual_cam_pos.y), abs(actual_cam_pos.z))
if max_val > props.slider_limit:
props.slider_limit = max_val + 50.0
props.view_pos = actual_cam_pos
_is_updating_view = False
target_area.tag_redraw()
return 0.05
# ==============================================================================
# コアロジック (図形メッシュ生成 & プレビュー管理)
# ==============================================================================
PREVIEW_COL_NAME = f"{OP_PREFIX}_Preview_Zone"
PREVIEW_TAG = f"{OP_PREFIX}_preview_tag"
_timer = None
def delayed_update():
global _timer
_timer = None
if bpy.context and bpy.context.scene:
update_preview_geometry(bpy.context)
return None
def on_update(self, context):
global _timer
if _timer:
try: bpy.app.timers.unregister(_timer)
except: pass
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
def post_creation_cap_handling(obj, cap_top, cap_bottom, direction=None):
if cap_top and cap_bottom: return
if not obj or obj.type != 'MESH': return
if not cap_top and not cap_bottom: return
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='DESELECT')
bm = bmesh.from_edit_mesh(obj.data)
bm.faces.ensure_lookup_table()
top_face, bottom_face = None, None
max_proj, min_proj = -float('inf'), float('inf')
world_matrix = obj.matrix_world
if direction is None:
direction = Vector((0, 0, 1))
else:
direction = direction.normalized()
for face in bm.faces:
face_center_world = world_matrix @ face.calc_center_median()
proj = face_center_world.dot(direction)
if proj > max_proj:
max_proj = proj
top_face = face
if proj < min_proj:
min_proj = proj
bottom_face = face
faces_to_delete =[]
if not cap_top and top_face: faces_to_delete.append(top_face)
if not cap_bottom and bottom_face: faces_to_delete.append(bottom_face)
if faces_to_delete:
bmesh.ops.delete(bm, geom=faces_to_delete, context='FACES')
bmesh.update_edit_mesh(obj.data)
bpy.ops.object.mode_set(mode='OBJECT')
def setup_sphere_instances_node_tree(obj, props):
"""ポイント上に球体を爆速でインスタンス化するジオメトリノードを構築"""
modifier_name = "Array_GeoNodes"
mod = obj.modifiers.get(modifier_name)
if not mod:
mod = obj.modifiers.new(name=modifier_name, type='NODES')
node_tree = bpy.data.node_groups.new(name="SphereArray_Nodes", type='GeometryNodeTree')
mod.node_group = node_tree
# 5.0対応 Group Input/Output
node_in = node_tree.nodes.new('NodeGroupInput')
node_tree.interface.new_socket(name="Geometry", in_out='INPUT', socket_type='NodeSocketGeometry')
node_out = node_tree.nodes.new('NodeGroupOutput')
node_tree.interface.new_socket(name="Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry')
node_inst = node_tree.nodes.new('GeometryNodeInstanceOnPoints')
node_sphere = node_tree.nodes.new('GeometryNodeMeshUVSphere')
node_sphere.inputs['Radius'].default_value = props.sphere_elem_radius
node_sphere.inputs['Segments'].default_value = props.sphere_segments
node_sphere.inputs['Rings'].default_value = props.sphere_rings
node_realize = node_tree.nodes.new('GeometryNodeRealizeInstances')
node_set_mat = node_tree.nodes.new('GeometryNodeSetMaterial')
# リンク接続
node_tree.links.new(node_in.outputs['Geometry'], node_inst.inputs['Points'])
node_tree.links.new(node_sphere.outputs['Mesh'], node_inst.inputs['Instance'])
node_tree.links.new(node_inst.outputs['Instances'], node_realize.inputs['Geometry'])
node_tree.links.new(node_realize.outputs['Geometry'], node_set_mat.inputs['Geometry'])
node_tree.links.new(node_set_mat.outputs['Geometry'], node_out.inputs['Geometry'])
def create_primitive_object(context, props, name):
old_objs = set(bpy.data.objects)
prim_type = props.primitive_type
use_panel_transform = True
win = context.window_manager.windows[0]
area = next((a for a in win.screen.areas if a.type == 'VIEW_3D'), None)
region = next((r for r in area.regions if r.type == 'WINDOW'), None) if area else None
try:
with context.temp_override(window=win, area=area, region=region):
if prim_type == 'CUBE':
bpy.ops.mesh.primitive_cube_add(size=props.cube_size, align='WORLD', location=(0, 0, 0))
elif prim_type == 'CUBOID':
bpy.ops.mesh.primitive_cube_add(size=1.0, align='WORLD', location=(0, 0, 0))
context.active_object.dimensions = props.cuboid_dimensions
elif prim_type == 'PLANE':
bpy.ops.mesh.primitive_plane_add(size=1.0, align='WORLD', location=(0, 0, 0))
context.active_object.dimensions = (props.plane_size_x, props.plane_size_y, 0)
elif prim_type == 'CIRCLE':
bpy.ops.mesh.primitive_circle_add(vertices=props.circle_vertices, radius=props.circle_radius, fill_type='NGON', align='WORLD', location=(0, 0, 0))
elif prim_type == 'ELLIPSE':
bpy.ops.mesh.primitive_circle_add(vertices=props.ellipse_vertices, radius=1.0, fill_type='NGON', align='WORLD', location=(0, 0, 0))
context.active_object.scale.x = props.ellipse_radius_x
context.active_object.scale.y = props.ellipse_radius_y
elif prim_type == 'UV_SPHERE':
bpy.ops.mesh.primitive_uv_sphere_add(radius=props.uv_sphere_radius, segments=props.uv_sphere_segments, ring_count=props.uv_sphere_rings, align='WORLD', location=(0, 0, 0))
elif prim_type == 'ICO_SPHERE':
bpy.ops.mesh.primitive_ico_sphere_add(radius=props.ico_sphere_radius, subdivisions=props.ico_sphere_subdivisions, align='WORLD', location=(0, 0, 0))
elif prim_type == 'CYLINDER':
cap_ends = props.cylinder_cap_top or props.cylinder_cap_bottom
direction = Vector((0, 0, 1))
if props.cylinder_mode == 'CENTER':
bpy.ops.mesh.primitive_cylinder_add(vertices=props.cylinder_vertices, radius=props.cylinder_radius, depth=props.cylinder_depth, end_fill_type='NGON' if cap_ends else 'NOTHING', align='WORLD', location=(0, 0, 0))
elif props.cylinder_mode == 'POINTS_NORMAL':
use_panel_transform = False
p_top, p_bottom = Vector(props.cylinder_point_top), Vector(props.cylinder_point_bottom)
direction = p_top - p_bottom
height = direction.length
if height < 1e-4: return None
bpy.ops.mesh.primitive_cylinder_add(vertices=props.cylinder_vertices, radius=props.cylinder_radius, depth=height, end_fill_type='NGON' if cap_ends else 'NOTHING', align='WORLD', location=(0, 0, 0))
obj = context.active_object
obj.location = (p_top + p_bottom) / 2
obj.rotation_euler = direction.to_track_quat('Z', 'Y').to_euler('XYZ')
elif props.cylinder_mode == 'POINTS_HORIZONTAL':
use_panel_transform = False
p_top, p_bottom = Vector(props.cylinder_point_top), Vector(props.cylinder_point_bottom)
direction = p_top - p_bottom
dz = direction.z
if abs(dz) < 1e-4: return None
height = abs(dz)
bpy.ops.mesh.primitive_cylinder_add(vertices=props.cylinder_vertices, radius=props.cylinder_radius, depth=height, end_fill_type='NGON' if cap_ends else 'NOTHING', align='WORLD', location=(0, 0, 0))
obj = context.active_object
dx, dy = direction.x, direction.y
sign_z = 1 if dz > 0 else -1
bpy.ops.object.mode_set(mode='EDIT')
bm = bmesh.from_edit_mesh(obj.data)
for v in bm.verts:
factor = v.co.z / height
v.co.x += factor * dx * sign_z
v.co.y += factor * dy * sign_z
bmesh.update_edit_mesh(obj.data)
bpy.ops.object.mode_set(mode='OBJECT')
obj.location = (p_top + p_bottom) / 2
post_creation_cap_handling(context.active_object, props.cylinder_cap_top, props.cylinder_cap_bottom, direction)
elif prim_type == 'FRUSTUM':
cap_ends = props.frustum_cap_top or props.frustum_cap_bottom
direction = Vector((0, 0, 1))
radius_bottom, radius_top = props.frustum_radius_bottom, props.frustum_radius_top
if props.frustum_mode == 'CENTER':
bpy.ops.mesh.primitive_cone_add(vertices=props.frustum_vertices, radius1=radius_bottom, radius2=radius_top, depth=props.frustum_height, end_fill_type='NGON' if cap_ends else 'NOTHING', align='WORLD', location=(0, 0, 0))
elif props.frustum_mode == 'POINTS_NORMAL':
use_panel_transform = False
p_top, p_bottom = Vector(props.frustum_point_top), Vector(props.frustum_point_bottom)
direction = p_top - p_bottom
height = direction.length
if height < 1e-4: return None
bpy.ops.mesh.primitive_cone_add(vertices=props.frustum_vertices, radius1=radius_bottom, radius2=radius_top, depth=height, end_fill_type='NGON' if cap_ends else 'NOTHING', align='WORLD', location=(0, 0, 0))
obj = context.active_object
obj.location = (p_top + p_bottom) / 2
obj.rotation_euler = direction.to_track_quat('Z', 'Y').to_euler('XYZ')
elif props.frustum_mode == 'POINTS_HORIZONTAL':
use_panel_transform = False
p_top, p_bottom = Vector(props.frustum_point_top), Vector(props.frustum_point_bottom)
direction = p_top - p_bottom
dz = direction.z
if abs(dz) < 1e-4: return None
height = abs(dz)
if dz > 0:
rad_bottom, rad_top = radius_bottom, radius_top
else:
rad_bottom, rad_top = radius_top, radius_bottom
bpy.ops.mesh.primitive_cone_add(vertices=props.frustum_vertices, radius1=rad_bottom, radius2=rad_top, depth=height, end_fill_type='NGON' if cap_ends else 'NOTHING', align='WORLD', location=(0, 0, 0))
obj = context.active_object
dx, dy = direction.x, direction.y
sign_z = 1 if dz > 0 else -1
bpy.ops.object.mode_set(mode='EDIT')
bm = bmesh.from_edit_mesh(obj.data)
for v in bm.verts:
factor = v.co.z / height
v.co.x += factor * dx * sign_z
v.co.y += factor * dy * sign_z
bmesh.update_edit_mesh(obj.data)
bpy.ops.object.mode_set(mode='OBJECT')
obj.location = (p_top + p_bottom) / 2
post_creation_cap_handling(context.active_object, props.frustum_cap_top, props.frustum_cap_bottom, direction)
elif prim_type == 'IMAGE_CYLINDER':
bpy.ops.mesh.primitive_cylinder_add(
vertices=props.image_cylinder_vertices,
radius=props.image_cylinder_radius,
depth=props.image_cylinder_depth,
end_fill_type='NOTHING',
align='WORLD',
location=(0, 0, 0)
)
elif prim_type in ('CUBE_FRAME', 'CUBOID_FRAME'):
if prim_type == 'CUBE_FRAME':
s_x = s_y = s_z = props.cube_frame_size / 2.0
r = props.cube_frame_radius
v = props.cube_frame_vertices
else:
s_x = props.cuboid_frame_dimensions[0] / 2.0
s_y = props.cuboid_frame_dimensions[1] / 2.0
s_z = props.cuboid_frame_dimensions[2] / 2.0
r = props.cuboid_frame_radius
v = props.cuboid_frame_vertices
pts =[
Vector((-s_x, -s_y, -s_z)), Vector(( s_x, -s_y, -s_z)), Vector(( s_x, s_y, -s_z)), Vector((-s_x, s_y, -s_z)),
Vector((-s_x, -s_y, s_z)), Vector(( s_x, -s_y, s_z)), Vector(( s_x, s_y, s_z)), Vector((-s_x, s_y, s_z))
]
edges =[
(0,1), (1,2), (2,3), (3,0),
(4,5), (5,6), (6,7), (7,4),
(0,4), (1,5), (2,6), (3,7)
]
mesh = bpy.data.meshes.new("TempBase")
base_obj = bpy.data.objects.new("TempBase", mesh)
context.collection.objects.link(base_obj)
base_obj.location = (0, 0, 0)
parts = [base_obj]
for idx1, idx2 in edges:
p1, p2 = pts[idx1], pts[idx2]
direction = p2 - p1
length = direction.length
bpy.ops.mesh.primitive_cylinder_add(vertices=v, radius=r, depth=length, end_fill_type='NGON', align='WORLD', location=(p1+p2)/2)
obj_part = context.active_object
obj_part.rotation_euler = direction.to_track_quat('Z', 'Y').to_euler('XYZ')
parts.append(obj_part)
if len(parts) > 1:
bpy.ops.object.select_all(action='DESELECT')
for obj_part in parts:
obj_part.select_set(True)
context.view_layer.objects.active = base_obj
bpy.ops.object.join()
elif prim_type == 'TORUS':
bpy.ops.mesh.primitive_torus_add(major_segments=props.torus_major_segments, minor_segments=props.torus_minor_segments, major_radius=props.torus_major_radius, minor_radius=props.torus_minor_radius, align='WORLD', location=(0, 0, 0))
elif prim_type == 'MONKEY':
bpy.ops.mesh.primitive_monkey_add(size=props.monkey_size, align='WORLD', location=(0, 0, 0))
# --- 配列図形 ---
elif prim_type.startswith('SPHERE_'):
locations =[]
spacing = props.sphere_spacing
size = props.sphere_size
radius = props.sphere_radius_val
if prim_type == 'SPHERE_SQUARE_EDGE':
count = max(1, round(size / spacing))
actual_spacing = size / count
half = size / 2.0
for i in range(count):
f = i * actual_spacing
locations.append(Vector((-half + f, -half, 0)))
locations.append(Vector((half, -half + f, 0)))
locations.append(Vector((half - f, half, 0)))
locations.append(Vector((-half, half - f, 0)))
elif prim_type == 'SPHERE_SQUARE_AREA':
count = max(1, round(size / spacing))
actual_spacing = size / count
half = size / 2.0
for i in range(count + 1):
x = -half + i * actual_spacing
for j in range(count + 1):
y = -half + j * actual_spacing
locations.append(Vector((x, y, 0)))
elif prim_type == 'SPHERE_CUBE_EDGE':
count = max(1, round(size / spacing))
actual_spacing = size / count
half = size / 2.0
for i in range(count):
f = i * actual_spacing
locations.extend([
Vector((-half + f, -half, -half)), Vector((half, -half + f, -half)),
Vector((half - f, half, -half)), Vector((-half, half - f, -half)),
Vector((-half + f, -half, half)), Vector((half, -half + f, half)),
Vector((half - f, half, half)), Vector((-half, half - f, half)),
Vector((-half, -half, -half + f)), Vector((half, -half, -half + f)),
Vector((half, half, -half + f)), Vector((-half, half, -half + f))
])
elif prim_type in ('SPHERE_CUBE_SURFACE', 'SPHERE_CUBE_VOLUME'):
count = max(1, round(size / spacing))
actual_spacing = size / count
half = size / 2.0
for i in range(count + 1):
x = -half + i * actual_spacing
for j in range(count + 1):
y = -half + j * actual_spacing
for k in range(count + 1):
z = -half + k * actual_spacing
if prim_type == 'SPHERE_CUBE_SURFACE':
if not (i == 0 or i == count or j == 0 or j == count or k == 0 or k == count):
continue
locations.append(Vector((x, y, z)))
elif prim_type == 'SPHERE_CIRCLE_EDGE':
circ = 2.0 * math.pi * radius
count = max(3, round(circ / spacing))
d_theta = 2.0 * math.pi / count
for i in range(count):
theta = i * d_theta
locations.append(Vector((radius * math.cos(theta), radius * math.sin(theta), 0)))
elif prim_type == 'SPHERE_CIRCLE_AREA': # グリッド状の円内
count = math.ceil(radius / spacing)
for i in range(-count, count + 1):
x = i * spacing
for j in range(-count, count + 1):
y = j * spacing
if x*x + y*y <= radius * radius + 1e-5:
locations.append(Vector((x, y, 0)))
elif prim_type == 'SPHERE_CIRCLE_SPIRAL': # 螺旋渦の円内(ひまわりの種)
area = math.pi * radius**2
count = max(1, int(area / (spacing**2)))
phi = math.pi * (3.0 - math.sqrt(5.0)) # 黄金角
for i in range(count):
r = radius * math.sqrt((i + 0.5) / count)
theta = i * phi
x = r * math.cos(theta)
y = r * math.sin(theta)
locations.append(Vector((x, y, 0.0)))
elif prim_type == 'SPHERE_SPHERE_SURFACE': # フィボナッチ球(表面)
area = 4 * math.pi * radius * radius
N = max(4, int(round(area / (spacing * spacing))))
phi = math.pi * (3.0 - math.sqrt(5.0))
for i in range(N):
z = 1.0 - (i / float(N - 1)) * 2.0
r_xy = math.sqrt(max(0.0, 1.0 - z * z))
theta = phi * i
x = math.cos(theta) * r_xy
y = math.sin(theta) * r_xy
locations.append(Vector((x * radius, y * radius, z * radius)))
elif prim_type == 'SPHERE_SPHERE_VOLUME': # グリッド状の球内
count = math.ceil(radius / spacing)
for i in range(-count, count + 1):
x = i * spacing
for j in range(-count, count + 1):
y = j * spacing
for k in range(-count, count + 1):
z = k * spacing
if x*x + y*y + z*z <= radius * radius + 1e-5:
locations.append(Vector((x, y, z)))
elif prim_type == 'SPHERE_SPHERE_SPIRAL': # 螺旋渦の球内(タマネギ状のフィボナッチ球)
r_count = max(1, int(radius / spacing))
phi = math.pi * (3.0 - math.sqrt(5.0))
locations.append(Vector((0,0,0))) # 中心点
for r_idx in range(1, r_count + 1):
r_current = r_idx * spacing
if r_current > radius: continue
layer_count = max(1, int((4 * math.pi * r_current**2) / (spacing**2)))
for i in range(layer_count):
z = 1.0 - (i / float(layer_count - 1 if layer_count > 1 else 1)) * 2.0
r_xy = math.sqrt(max(0.0, 1.0 - z * z))
theta = i * phi
x = r_xy * math.cos(theta)
y = r_xy * math.sin(theta)
locations.append(Vector((x * r_current, y * r_current, z * r_current)))
# 安全装置 (ノードなので10万個まで許容)
MAX_SPHERES = 100000
if len(locations) > MAX_SPHERES:
print(f"[{PREFIX}] Warning: Too many spheres ({len(locations)}). Limited to {MAX_SPHERES}.")
locations = locations[:MAX_SPHERES]
if locations:
mesh = bpy.data.meshes.new("TempSphereArray")
mesh.from_pydata(locations, [],[])
mesh.update()
obj = bpy.data.objects.new("TempSphereArray", mesh)
context.collection.objects.link(obj)
context.view_layer.objects.active = obj
obj.select_set(True)
setup_sphere_instances_node_tree(obj, props)
elif prim_type == 'TORUS_CONCENTRIC':
mesh = bpy.data.meshes.new("TempConcentricTorus")
base_obj = bpy.data.objects.new("TempConcentricTorus", mesh)
context.collection.objects.link(base_obj)
base_obj.location = (0, 0, 0)
parts =[base_obj]
for i in range(1, props.torus_count + 1):
maj_r = i * props.torus_spacing
bpy.ops.mesh.primitive_torus_add(
major_segments=props.torus_major_segments,
minor_segments=props.torus_minor_segments,
major_radius=maj_r,
minor_radius=props.torus_minor_radius_arr,
align='WORLD', location=(0, 0, 0)
)
parts.append(context.active_object)
if len(parts) > 1:
bpy.ops.object.select_all(action='DESELECT')
for p in parts:
p.select_set(True)
context.view_layer.objects.active = base_obj
bpy.ops.object.join()
elif prim_type == 'CYLINDER_GRID':
bm = bmesh.new()
cx = props.grid_count_x
cy = props.grid_count_y
cz = props.grid_count_z
sx = props.grid_spacing_x
sy = props.grid_spacing_y
sz = props.grid_spacing_z
r = props.grid_radius
v = props.grid_vertices
wx = (cx - 1) * sx
wy = (cy - 1) * sy
wz = (cz - 1) * sz
# X軸に平行な線
if cx > 1:
for j in range(cy):
for k in range(cz):
y = -wy/2.0 + j * sy
z = -wz/2.0 + k * sz
mat = Matrix.Translation((0, y, z)) @ Euler((0, math.radians(90), 0), 'XYZ').to_matrix().to_4x4()
bmesh.ops.create_cone(bm, cap_ends=True, segments=v, radius1=r, radius2=r, depth=wx, matrix=mat)
# Y軸に平行な線
if cy > 1:
for i in range(cx):
for k in range(cz):
x = -wx/2.0 + i * sx
z = -wz/2.0 + k * sz
mat = Matrix.Translation((x, 0, z)) @ Euler((math.radians(90), 0, 0), 'XYZ').to_matrix().to_4x4()
bmesh.ops.create_cone(bm, cap_ends=True, segments=v, radius1=r, radius2=r, depth=wy, matrix=mat)
# Z軸に平行な線
if cz > 1:
for i in range(cx):
for j in range(cy):
x = -wx/2.0 + i * sx
y = -wy/2.0 + j * sy
mat = Matrix.Translation((x, y, 0))
bmesh.ops.create_cone(bm, cap_ends=True, segments=v, radius1=r, radius2=r, depth=wz, matrix=mat)
mesh = bpy.data.meshes.new("TempGrid")
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new("TempGrid", mesh)
context.collection.objects.link(obj)
context.view_layer.objects.active = obj
obj.select_set(True)
except Exception as e:
print(f"Error creating primitive: {e}")
return None
new_objs = set(bpy.data.objects) - old_objs
if not new_objs: return None
obj = list(new_objs)[0]
obj.name = name
if use_panel_transform:
if props.scale_uniform:
scale_vec = Vector((props.scale_factor, props.scale_factor, props.scale_factor))
else:
scale_vec = Vector(props.scale_vector)
obj.scale.x *= scale_vec.x
obj.scale.y *= scale_vec.y
obj.scale.z *= scale_vec.z
obj.location = props.location
obj.rotation_euler.rotate(Euler(map(math.radians, (props.additional_rotation_x, props.additional_rotation_y, props.additional_rotation_z)), 'XYZ'))
if props.use_solidify and props.primitive_type != 'IMAGE_CYLINDER':
mod = obj.modifiers.new(name="Solidify", type='SOLIDIFY')
mod.thickness = props.solidify_thickness
return obj
def apply_primitive_material(obj, props, is_preview=False):
if not obj.data: return
timestamp = datetime.datetime.now().strftime('%M%S%f')[:5]
mat_prefix = "Mat_Prev" if is_preview else "Mat_Entity"
mat = bpy.data.materials.new(name=f"{mat_prefix}_{obj.name}_{timestamp}")
mat.use_nodes = True
obj.data.materials.append(mat)
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if not bsdf: return
if props.primitive_type == 'IMAGE_CYLINDER':
if hasattr(mat, "blend_method"): mat.blend_method = 'BLEND'
if hasattr(mat, "shadow_method"): mat.shadow_method = 'NONE'
tex_node = mat.node_tree.nodes.new('ShaderNodeTexImage')
tex_node.location = (-400, 200)
tex_coord = mat.node_tree.nodes.new('ShaderNodeTexCoord')
tex_coord.location = (-800, 200)
mapping = mat.node_tree.nodes.new('ShaderNodeMapping')
mapping.location = (-600, 200)
mapping.inputs['Location'].default_value[0] = props.image_cylinder_uv_offset[0]
mapping.inputs['Location'].default_value[1] = props.image_cylinder_uv_offset[1]
mapping.inputs['Scale'].default_value[0] = -1.0 * props.image_cylinder_uv_scale[0]
mapping.inputs['Scale'].default_value[1] = props.image_cylinder_uv_scale[1]
mat.node_tree.links.new(tex_coord.outputs['UV'], mapping.inputs['Vector'])
mat.node_tree.links.new(mapping.outputs['Vector'], tex_node.inputs['Vector'])
if props.image_cylinder_image:
tex_node.image = props.image_cylinder_image
mat.node_tree.links.new(tex_node.outputs['Color'], bsdf.inputs['Base Color'])
if 'Emission Color' in bsdf.inputs: mat.node_tree.links.new(tex_node.outputs['Color'], bsdf.inputs['Emission Color'])
elif 'Emission' in bsdf.inputs: mat.node_tree.links.new(tex_node.outputs['Color'], bsdf.inputs['Emission'])
if 'Emission Strength' in bsdf.inputs: bsdf.inputs['Emission Strength'].default_value = 1.0
else:
bsdf.inputs['Base Color'].default_value = (0.2, 0.2, 0.2, 1.0)
geo_node = mat.node_tree.nodes.new('ShaderNodeNewGeometry')
geo_node.location = (-400, -100)
map_node = mat.node_tree.nodes.new('ShaderNodeMapRange')
map_node.location = (-200, -100)
map_node.inputs['To Min'].default_value = props.image_cylinder_alpha_outer
map_node.inputs['To Max'].default_value = props.image_cylinder_alpha_inner
mat.node_tree.links.new(geo_node.outputs['Backfacing'], map_node.inputs['Value'])
mat.node_tree.links.new(map_node.outputs['Result'], bsdf.inputs['Alpha'])
else:
color_to_set = (0.8, 0.8, 0.8, 1.0)
if props.color_mode == 'RANDOM_ALL':
color_to_set = (random.random(), random.random(), random.random(), 1.0)
elif props.color_mode == 'PICKER':
color_to_set = props.custom_color
elif props.color_mode == 'PRESET':
preset_key = f"preset_color_{props.preset_set.lower()}_{props.preset_color}" if props.preset_set != 'A' else f"preset_color_{props.preset_color}"
color_to_set = getattr(props, preset_key)
bsdf.inputs['Base Color'].default_value = color_to_set
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = props.face_alpha
if hasattr(mat, "blend_method"):
mat.blend_method = 'BLEND' if props.face_alpha < 1.0 else 'OPAQUE'
# GeoNodes経由でマテリアルを確実に反映
if props.primitive_type.startswith('SPHERE_'):
mod = obj.modifiers.get("Array_GeoNodes")
if mod and mod.node_group:
for node in mod.node_group.nodes:
if node.type == 'SET_MATERIAL':
node.inputs['Material'].default_value = mat
def update_preview_geometry(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_NAME)
context.scene.collection.children.link(col)
for o in list(col.objects):
if o.get(PREVIEW_TAG):
bpy.data.objects.remove(o, do_unlink=True)
for m in bpy.data.meshes:
if m.users == 0: bpy.data.meshes.remove(m)
for mat in bpy.data.materials:
if mat.users == 0 and mat.name.startswith("Mat_Prev"): bpy.data.materials.remove(mat)
# 配列図形用ノードグループのお掃除
for ng in bpy.data.node_groups:
if ng.users == 0 and ng.name.startswith("SphereArray_Nodes"):
bpy.data.node_groups.remove(ng)
if not props.show_preview: return
name = f"[Preview] {props.primitive_type}"
obj = create_primitive_object(context, props, name)
if not obj: return
for c in obj.users_collection: c.objects.unlink(obj)
col.objects.link(obj)
obj[PREVIEW_TAG] = True
apply_primitive_material(obj, props, is_preview=True)
obj.select_set(False)
def place_object_in_hierarchical_collection(obj, collection_path_names):
if not obj or not collection_path_names: return
target_parent_collection = bpy.context.scene.collection
for name in collection_path_names:
found_child = target_parent_collection.children.get(name)
if not found_child:
found_child = bpy.data.collections.new(name)
target_parent_collection.children.link(found_child)
target_parent_collection = found_child
for coll in obj.users_collection:
coll.objects.unlink(obj)
try:
if obj.name not in target_parent_collection.objects:
target_parent_collection.objects.link(obj)
except Exception as e:
print(f"Error linking object {obj.name}: {e}")
# ==============================================================================
# PROPERTIES
# ==============================================================================
def make_color_update(set_val, idx):
def update_cb(self, context):
if self.color_mode == 'PRESET':
self.preset_set = set_val
self.preset_color = str(idx)
on_update(self, context)
return update_cb
class PG_B200Props(PropertyGroup):
# --- View Control Props ---
slider_limit: FloatProperty(name="Range Limit", default=CURRENT_DEFAULTS.get('slider_limit', 300.0), min=10.0, max=10000.0)
view_pos: FloatVectorProperty(name="View Position", size=3, soft_min=-10000.0, soft_max=10000.0,
default=CURRENT_DEFAULTS.get('view_pos', VIEW_POS_INIT),
update=update_view_position)
# --- Zukkei & Array Props ---
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
collection_name: StringProperty(name="Collection", default=CURRENT_DEFAULTS['collection_name'])
sub_collection_name: StringProperty(name="Sub-Collection", default=CURRENT_DEFAULTS['sub_collection_name'])
object_name_prefix: StringProperty(name="Object Name", default=CURRENT_DEFAULTS['object_name_prefix'])
primitive_type: EnumProperty(
name="Primitive Type",
items=[
('CUBE', "Cube", ""), ('CUBOID', "Cuboid", ""), ('PLANE', "Plane", ""),
('CIRCLE', "Circle", ""), ('ELLIPSE', "Ellipse", ""), ('UV_SPHERE', "UV Sphere", ""),
('ICO_SPHERE', "Ico Sphere", ""), ('CYLINDER', "Cylinder", ""),
('FRUSTUM', "Frustum (Cone)", ""), ('IMAGE_CYLINDER', "Image Cylinder (パノラマ筒)", ""),
('CUBE_FRAME', "Cube Frame (辺を円柱で)", ""),
('CUBOID_FRAME', "Cuboid Frame (辺を円柱で)", ""),
('TORUS', "Torus", ""), ('MONKEY', "Monkey", ""),
# 配列図形の追加分
('SPHERE_SQUARE_EDGE', "Sphere: Square Perimeter (正方形の辺)", ""),
('SPHERE_SQUARE_AREA', "Sphere: Square Area (正方形の面)", ""),
('SPHERE_CUBE_EDGE', "Sphere: Cube Edges (立方体の辺)", ""),
('SPHERE_CUBE_SURFACE', "Sphere: Cube Surface (立方体の表面)", ""),
('SPHERE_CUBE_VOLUME', "Sphere: Cube Volume (立方体の中身)", ""),
('SPHERE_CIRCLE_EDGE', "Sphere: Circle Perimeter (円の円周)", ""),
('SPHERE_CIRCLE_AREA', "Sphere: Circle Area (円の面内グリッド)", ""),
('SPHERE_CIRCLE_SPIRAL', "Sphere: Circle Spiral (円内を螺旋渦で)", ""),
('SPHERE_SPHERE_SURFACE', "Sphere: Sphere Surface (球の表面)", ""),
('SPHERE_SPHERE_VOLUME', "Sphere: Sphere Volume (球体内グリッド)", ""),
('SPHERE_SPHERE_SPIRAL', "Sphere: Sphere Spiral (球内を螺旋渦で)", ""),
('TORUS_CONCENTRIC', "Torus: Concentric (同心円トーラス)", ""),
('CYLINDER_GRID', "Cylinder: Grid 3D (円柱の3D格子)", "")
],
default=CURRENT_DEFAULTS['primitive_type'], update=on_update
)
use_solidify: BoolProperty(name="面に厚みを加える", default=CURRENT_DEFAULTS['use_solidify'], update=on_update)
solidify_thickness: FloatProperty(name="厚み", default=CURRENT_DEFAULTS['solidify_thickness'], update=on_update)
cube_size: FloatProperty(name="Size", default=CURRENT_DEFAULTS['cube_size'], min=0.001, update=on_update)
cuboid_dimensions: FloatVectorProperty(name="Dimensions", default=CURRENT_DEFAULTS['cuboid_dimensions'], min=0.001, subtype='XYZ', size=3, update=on_update)
plane_size_x: FloatProperty(name="Size X", default=CURRENT_DEFAULTS['plane_size_x'], min=0.001, update=on_update)
plane_size_y: FloatProperty(name="Size Y", default=CURRENT_DEFAULTS['plane_size_y'], min=0.001, update=on_update)
circle_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['circle_radius'], min=0.001, update=on_update)
circle_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['circle_vertices'], min=3, update=on_update)
ellipse_radius_x: FloatProperty(name="Radius X", default=CURRENT_DEFAULTS['ellipse_radius_x'], min=0.001, update=on_update)
ellipse_radius_y: FloatProperty(name="Radius Y", default=CURRENT_DEFAULTS['ellipse_radius_y'], min=0.001, update=on_update)
ellipse_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['ellipse_vertices'], min=3, update=on_update)
uv_sphere_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['uv_sphere_radius'], min=0.001, update=on_update)
uv_sphere_segments: IntProperty(name="Segments", default=CURRENT_DEFAULTS['uv_sphere_segments'], min=3, update=on_update)
uv_sphere_rings: IntProperty(name="Rings", default=CURRENT_DEFAULTS['uv_sphere_rings'], min=2, update=on_update)
ico_sphere_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['ico_sphere_radius'], min=0.001, update=on_update)
ico_sphere_subdivisions: IntProperty(name="Subdivisions", default=CURRENT_DEFAULTS['ico_sphere_subdivisions'], min=1, max=10, update=on_update)
CYL_FRUSTUM_MODES =[
('CENTER', "Center, Height", "中心座標と高さを指定して作成"),
('POINTS_NORMAL', "Points (Axis Normal)", "両端指定: 上下面が中心軸に対して直角"),
('POINTS_HORIZONTAL', "Points (Horizontal Caps)", "両端指定: 上下面がXY平面に平行(シアー変形)")
]
cylinder_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['cylinder_radius'], min=0.001, update=on_update)
cylinder_mode: EnumProperty(name="Creation Mode", items=CYL_FRUSTUM_MODES, default=CURRENT_DEFAULTS['cylinder_mode'], update=on_update)
cylinder_depth: FloatProperty(name="Depth", default=CURRENT_DEFAULTS['cylinder_depth'], min=0.001, update=on_update)
cylinder_point_top: FloatVectorProperty(name="Top Point", subtype='TRANSLATION', default=CURRENT_DEFAULTS['cylinder_point_top'], size=3, update=on_update)
cylinder_point_bottom: FloatVectorProperty(name="Bottom Point", subtype='TRANSLATION', default=CURRENT_DEFAULTS['cylinder_point_bottom'], size=3, update=on_update)
cylinder_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['cylinder_vertices'], min=3, update=on_update)
cylinder_cap_top: BoolProperty(name="Fill Top Cap", default=CURRENT_DEFAULTS['cylinder_cap_top'], update=on_update)
cylinder_cap_bottom: BoolProperty(name="Fill Bottom Cap", default=CURRENT_DEFAULTS['cylinder_cap_bottom'], update=on_update)
frustum_mode: EnumProperty(name="Creation Mode", items=CYL_FRUSTUM_MODES, default=CURRENT_DEFAULTS['frustum_mode'], update=on_update)
frustum_radius_top: FloatProperty(name="Top Radius (+Z)", default=CURRENT_DEFAULTS['frustum_radius_top'], min=0.0, update=on_update)
frustum_radius_bottom: FloatProperty(name="Bottom Radius (-Z)", default=CURRENT_DEFAULTS['frustum_radius_bottom'], min=0.001, update=on_update)
frustum_height: FloatProperty(name="Height", default=CURRENT_DEFAULTS['frustum_height'], min=0.001, update=on_update)
frustum_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['frustum_vertices'], min=3, update=on_update)
frustum_cap_top: BoolProperty(name="Fill Top Cap", default=CURRENT_DEFAULTS['frustum_cap_top'], update=on_update)
frustum_cap_bottom: BoolProperty(name="Fill Bottom Cap", default=CURRENT_DEFAULTS['frustum_cap_bottom'], update=on_update)
frustum_point_top: FloatVectorProperty(name="Top Point", subtype='TRANSLATION', default=CURRENT_DEFAULTS['frustum_point_top'], size=3, update=on_update)
frustum_point_bottom: FloatVectorProperty(name="Bottom Point", subtype='TRANSLATION', default=CURRENT_DEFAULTS['frustum_point_bottom'], size=3, update=on_update)
image_cylinder_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['image_cylinder_radius'], min=0.001, update=on_update)
image_cylinder_depth: FloatProperty(name="Height", default=CURRENT_DEFAULTS['image_cylinder_depth'], min=0.001, update=on_update)
image_cylinder_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['image_cylinder_vertices'], min=3, update=on_update)
image_cylinder_image: PointerProperty(type=bpy.types.Image, name="Image", update=on_update)
image_cylinder_uv_offset: FloatVectorProperty(name="UV Offset", size=2, default=CURRENT_DEFAULTS['image_cylinder_uv_offset'], update=on_update)
image_cylinder_uv_scale: FloatVectorProperty(name="UV Scale", size=2, default=CURRENT_DEFAULTS['image_cylinder_uv_scale'], update=on_update)
image_cylinder_alpha_outer: FloatProperty(name="Outer Alpha (外側の透明度)", default=CURRENT_DEFAULTS['image_cylinder_alpha_outer'], min=0.0, max=1.0, update=on_update)
image_cylinder_alpha_inner: FloatProperty(name="Inner Alpha (内側の透明度)", default=CURRENT_DEFAULTS['image_cylinder_alpha_inner'], min=0.0, max=1.0, update=on_update)
cube_frame_size: FloatProperty(name="Size", default=CURRENT_DEFAULTS['cube_frame_size'], min=0.001, update=on_update)
cube_frame_radius: FloatProperty(name="Radius (Thickness)", default=CURRENT_DEFAULTS['cube_frame_radius'], min=0.001, update=on_update)
cube_frame_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['cube_frame_vertices'], min=3, update=on_update)
cuboid_frame_dimensions: FloatVectorProperty(name="Dimensions", default=CURRENT_DEFAULTS['cuboid_frame_dimensions'], min=0.001, subtype='XYZ', size=3, update=on_update)
cuboid_frame_radius: FloatProperty(name="Radius (Thickness)", default=CURRENT_DEFAULTS['cuboid_frame_radius'], min=0.001, update=on_update)
cuboid_frame_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['cuboid_frame_vertices'], min=3, update=on_update)
torus_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['torus_major_radius'], min=0.001, update=on_update)
torus_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['torus_minor_radius'], min=0.001, update=on_update)
torus_major_segments: IntProperty(name="Major Segments", default=CURRENT_DEFAULTS['torus_major_segments'], min=3, update=on_update)
torus_minor_segments: IntProperty(name="Minor Segments", default=CURRENT_DEFAULTS['torus_minor_segments'], min=3, update=on_update)
monkey_size: FloatProperty(name="Size", default=CURRENT_DEFAULTS['monkey_size'], min=0.001, update=on_update)
# 配列図形用プロパティ
sphere_spacing: FloatProperty(name="間隔", default=CURRENT_DEFAULTS.get('sphere_spacing', 0.5), min=0.01, update=on_update)
sphere_elem_radius: FloatProperty(name="球の半径", default=CURRENT_DEFAULTS.get('sphere_elem_radius', 0.1), min=0.001, update=on_update)
sphere_size: FloatProperty(name="全体サイズ", default=CURRENT_DEFAULTS.get('sphere_size', 4.0), min=0.001, update=on_update)
sphere_radius_val: FloatProperty(name="全体半径", default=CURRENT_DEFAULTS.get('sphere_radius_val', 2.0), min=0.001, update=on_update)
sphere_segments: IntProperty(name="Segments", default=CURRENT_DEFAULTS.get('sphere_segments', 12), min=3, update=on_update)
sphere_rings: IntProperty(name="Rings", default=CURRENT_DEFAULTS.get('sphere_rings', 6), min=2, update=on_update)
torus_count: IntProperty(name="同心円の数", default=CURRENT_DEFAULTS.get('torus_count', 5), min=1, update=on_update)
torus_spacing: FloatProperty(name="間隔", default=CURRENT_DEFAULTS.get('torus_spacing', 1.0), min=0.01, update=on_update)
torus_minor_radius_arr: FloatProperty(name="トーラスの太さ", default=CURRENT_DEFAULTS.get('torus_minor_radius_arr', 0.1), min=0.001, update=on_update)
grid_count_x: IntProperty(name="Count X", default=CURRENT_DEFAULTS.get('grid_count_x', 5), min=1, update=on_update)
grid_count_y: IntProperty(name="Count Y", default=CURRENT_DEFAULTS.get('grid_count_y', 5), min=1, update=on_update)
grid_count_z: IntProperty(name="Count Z", default=CURRENT_DEFAULTS.get('grid_count_z', 5), min=1, update=on_update)
grid_spacing_x: FloatProperty(name="Spacing X", default=CURRENT_DEFAULTS.get('grid_spacing_x', 1.0), min=0.01, update=on_update)
grid_spacing_y: FloatProperty(name="Spacing Y", default=CURRENT_DEFAULTS.get('grid_spacing_y', 1.0), min=0.01, update=on_update)
grid_spacing_z: FloatProperty(name="Spacing Z", default=CURRENT_DEFAULTS.get('grid_spacing_z', 1.0), min=0.01, update=on_update)
grid_radius: FloatProperty(name="円柱の太さ", default=CURRENT_DEFAULTS.get('grid_radius', 0.05), min=0.001, update=on_update)
grid_vertices: IntProperty(name="円柱の頂点数", default=CURRENT_DEFAULTS.get('grid_vertices', 16), min=3, update=on_update)
location: FloatVectorProperty(name="Location", subtype='TRANSLATION', default=CURRENT_DEFAULTS['location'], size=3, update=on_update)
scale_uniform: BoolProperty(name="Uniform Scale", default=CURRENT_DEFAULTS['scale_uniform'], update=on_update)
scale_factor: FloatProperty(name="Scale", default=CURRENT_DEFAULTS['scale_factor'], min=0.001, update=on_update)
scale_vector: FloatVectorProperty(name="Scale", default=CURRENT_DEFAULTS['scale_vector'], min=0.001, size=3, update=on_update)
additional_rotation_x: FloatProperty(name="Rotation X", default=CURRENT_DEFAULTS['additional_rotation_x'], update=on_update)
additional_rotation_y: FloatProperty(name="Rotation Y", default=CURRENT_DEFAULTS['additional_rotation_y'], update=on_update)
additional_rotation_z: FloatProperty(name="Rotation Z", default=CURRENT_DEFAULTS['additional_rotation_z'], update=on_update)
color_mode: EnumProperty(name="Color Mode", items=[('RANDOM_ALL', "Random All", ""), ('PICKER', "Color Picker", ""), ('PRESET', "Preset", "")], default=CURRENT_DEFAULTS['color_mode'], update=on_update)
preset_set: EnumProperty(name="Preset Set", items=[('A', "Preset A", ""), ('B', "Preset B", ""), ('C', "Preset C (Green)", "")], default=CURRENT_DEFAULTS['preset_set'], update=on_update)
preset_color: EnumProperty(name="Preset Color", items=[(str(i), str(i), "") for i in range(1, 11)], default=CURRENT_DEFAULTS['preset_color'], update=on_update)
custom_color: FloatVectorProperty(name="Custom Color", subtype='COLOR', default=CURRENT_DEFAULTS['custom_color'], min=0.0, max=1.0, size=4, update=on_update)
face_alpha: FloatProperty(name="Alpha", default=CURRENT_DEFAULTS['face_alpha'], min=0.0, max=1.0, update=on_update)
show_main_docs: BoolProperty(default=CURRENT_DEFAULTS['show_main_docs'])
show_new_docs: BoolProperty(default=CURRENT_DEFAULTS['show_new_docs'])
show_old_docs: BoolProperty(default=CURRENT_DEFAULTS['show_old_docs'])
show_social: BoolProperty(default=CURRENT_DEFAULTS['show_social'])
for s_key, set_val in[('preset_color', 'A'), ('preset_color_b', 'B'), ('preset_color_c', 'C')]:
for i in range(1, 11):
key = f"{s_key}_{i}"
PG_B200Props.__annotations__[key] = FloatVectorProperty(subtype='COLOR', default=CURRENT_DEFAULTS[key], min=0.0, max=1.0, size=4, update=make_color_update(set_val, i))
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_ViewCenterFront(Operator):
bl_idname = f"{OP_PREFIX}.view_center_front"
bl_label = "Center 0,0,0 (Front View)"
bl_description = "原点(0,0,0)を画面中央に配置し、Yマイナス方向からの視点(正面)にします"
def execute(self, context):
for area in context.screen.areas:
if area.type == 'VIEW_3D':
rv3d = area.spaces.active.region_3d
if rv3d:
rv3d.view_location = (0.0, 0.0, 0.0)
rv3d.view_rotation = Euler((math.radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
if rv3d.view_distance < 10.0:
rv3d.view_distance = 60.0
return {'FINISHED'}
class OT_CreatePrimitive(Operator):
bl_idname = f"{OP_PREFIX}.create_primitive"
bl_label = "実体メッシュを生成 (切り離し)"
bl_description = "現在のパラメータで実体を生成し、アドオン管理から切り離して指定コレクションに配置します"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME)
scene = context.scene
prefix = props.object_name_prefix.strip() or "Object"
final_name = f"{prefix}{scene.b200_object_counter:03d}"
obj = create_primitive_object(context, props, final_name)
if not obj:
self.report({'ERROR'}, "形状の生成に失敗しました。")
return {'CANCELLED'}
scene.b200_object_counter += 1
for c in obj.users_collection:
c.objects.unlink(obj)
collection_path =[n for n in[props.collection_name.strip(), props.sub_collection_name.strip()] if n]
if collection_path:
place_object_in_hierarchical_collection(obj, collection_path)
else:
context.scene.collection.objects.link(obj)
apply_primitive_material(obj, props, is_preview=False)
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
context.view_layer.objects.active = obj
self.report({'INFO'}, f"{props.primitive_type} を生成し、アドオンから切り離しました: {obj.name}")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = next((t for t in bpy.data.texts if SOURCE_ID_TAG in t.as_string()), None)
if not target_text:
self.report({'ERROR'}, "スクリプトのソースが見つかりません。")
return {'CANCELLED'}
def format_val(v):
if isinstance(v, str): return repr(v)
if isinstance(v, bool): return str(v)
if isinstance(v, (int, float)): return f"{v:.4f}" if isinstance(v, float) else str(v)
try:
return "(" + ", ".join(f"{float(x):.4f}" for x in v) + ")"
except: pass
return str(v)
new_dict = "CURRENT_DEFAULTS = {\n"
for k in CURRENT_DEFAULTS.keys():
val = getattr(props, k)
new_dict += f' "{k}": {format_val(val)},\n'
new_dict += "}\n"
code = target_text.as_string()
try:
start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
pre, post = code.split(start)[0], code.split(end)[1]
final = f"# Copied: {datetime.datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
context.window_manager.clipboard = final
self.report({'INFO'}, "現在のパラメータでコードをコピーしました!")
except Exception as e:
self.report({'ERROR'}, f"コピーに失敗: {e}")
return {'CANCELLED'}
return {'FINISHED'}
class OT_ResetProperty(Operator):
bl_idname = f"{OP_PREFIX}.reset_property"
bl_label = "Reset Property"
bl_options = {'REGISTER', 'UNDO'}
prop_name: StringProperty()
def execute(self, context):
props = getattr(context.scene, PROPS_NAME)
if self.prop_name in CURRENT_DEFAULTS:
setattr(props, self.prop_name, CURRENT_DEFAULTS[self.prop_name])
return {'FINISHED'}
class OT_ResetAllSettings(Operator):
bl_idname = f"{OP_PREFIX}.reset_all_settings"
bl_label = "Reset All Settings"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME)
for k, v in CURRENT_DEFAULTS.items():
setattr(props, k, v)
context.scene.b200_object_counter = 1
self.report({'INFO'}, "All settings and counter have been reset.")
return {'FINISHED'}
class OT_ResetCounter(Operator):
bl_idname = f"{OP_PREFIX}.reset_counter"
bl_label = "Reset Counter"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
context.scene.b200_object_counter = 1
return {'FINISHED'}
class OT_OpenUrl(Operator):
bl_idname = f"{OP_PREFIX}.open_url"
bl_label = "Open URL"
url: StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class OT_RemoveAddon(Operator):
bl_idname = f"{OP_PREFIX}.remove_addon"
bl_label = "Remove Addon"
def execute(self, context):
def delayed_unregister(): unregister(); return None
bpy.app.timers.register(delayed_unregister, first_interval=0.1)
return {'FINISHED'}
# --- View Control Operators ---
class OT_View_GetCurrent(Operator):
bl_idname = f"{OP_PREFIX}.view_get_current"
bl_label = "Get Current View & Update"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return {'CANCELLED'}
r3d = context.space_data.region_3d if context.space_data else None
if not r3d: return {'CANCELLED'}
target_pos = Vector(r3d.view_location)
actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
max_val = max(abs(actual_cam_pos.x), abs(actual_cam_pos.y), abs(actual_cam_pos.z))
if max_val > props.slider_limit: props.slider_limit = max_val + 50.0
props.view_pos = actual_cam_pos
return {'FINISHED'}
class OT_View_Reset(Operator):
bl_idname = f"{OP_PREFIX}.view_reset"
bl_label = VIEW_RESET_BTN_TEXT
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props:
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.region_3d.view_location = (0.0, 0.0, 0.0)
props.view_pos = VIEW_POS_INIT
return {'FINISHED'}
class OT_View_CenterSelected(Operator):
bl_idname = f"{OP_PREFIX}.view_center_selected"
bl_label = "Center Selected Object"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.view3d.view_selected()
r3d = context.space_data.region_3d if context.space_data else None
if r3d:
props = getattr(context.scene, PROPS_NAME, None)
if props:
target_pos = Vector(r3d.view_location)
props.view_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
return {'FINISHED'}
class OT_View_CopyActualPos(Operator):
bl_idname = f"{OP_PREFIX}.view_copy_actual_pos"
bl_label = "Copy Position Only"
def execute(self, context):
r3d = context.space_data.region_3d if context.space_data else None
if not r3d: return {'CANCELLED'}
target_pos = Vector(r3d.view_location)
p = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
context.window_manager.clipboard = f"Actual View Pos: ({p.x:.4f}, {p.y:.4f}, {p.z:.4f})"
self.report({'INFO'}, "視座位置をコピーしました")
return {'FINISHED'}
class OT_View_CopyAngles(Operator):
bl_idname = f"{OP_PREFIX}.view_copy_angles"
bl_label = "Copy Full Info (Pos & Angles)"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
r3d = context.space_data.region_3d if context.space_data else None
if not r3d or not props: return {'CANCELLED'}
target_pos = Vector(r3d.view_location)
actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
vec = target_pos - actual_cam_pos
length = vec.length
if length < 0.0001: return {'CANCELLED'}
ang_x = math.degrees(math.acos(vec.x / length))
ang_y = math.degrees(math.acos(vec.y / length))
ang_z = math.degrees(math.acos(vec.z / length))
pl_x = math.degrees(math.asin(vec.x / length))
pl_y = math.degrees(math.asin(vec.y / length))
pl_z = math.degrees(math.asin(vec.z / length))
info_text = (
f"--- View Direction Info ---\n"
f"[ Actual 3D View Status ]\n"
f"Actual View Pos : ({actual_cam_pos.x:.4f}, {actual_cam_pos.y:.4f}, {actual_cam_pos.z:.4f})\n"
f"Target Pos : ({target_pos.x:.4f}, {target_pos.y:.4f}, {target_pos.z:.4f})\n"
f"Distance : {length:.4f}\n\n"
f"[ Direction Angles (軸そのものとの角度 0〜180°) ]\n"
f"Angle from X Axis : {ang_x:.2f} deg\n"
f"Angle from Y Axis : {ang_y:.2f} deg\n"
f"Angle from Z Axis : {ang_z:.2f} deg\n\n"
f"[ Planar Angles (直感的な傾き・ズレ角 -90〜90°) ]\n"
f"X (横のズレ角) : {pl_x:.2f} deg\n"
f"Y (前後の傾き) : {pl_y:.2f} deg\n"
f"Z (仰角・俯角) : {pl_z:.2f} deg\n"
)
context.window_manager.clipboard = info_text
self.report({'INFO'}, "情報全体をクリップボードにコピーしました")
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PG_BasePanel:
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
def _draw_prop_with_reset(self, layout, obj, prop_name, text=None):
row = layout.row(align=True)
split = row.split(factor=0.85, align=True)
split.prop(obj, prop_name, text=text if text else prop_name.replace("_", " ").title())
op = split.operator(OT_ResetProperty.bl_idname, text="", icon='FILE_REFRESH')
op.prop_name = prop_name
def _draw_vector_xyz(self, layout, obj, prop_name, label):
col = layout.column(align=True)
col.label(text=label)
for i, axis in enumerate(['X', 'Y', 'Z']):
row = col.row(align=True)
split = row.split(factor=0.85, align=True)
split.prop(obj, prop_name, index=i, text=axis)
op = split.operator(OT_ResetProperty.bl_idname, text="", icon='FILE_REFRESH')
op.prop_name = prop_name
def _draw_vector_2d(self, layout, obj, prop_name, label, labels=['U', 'V']):
col = layout.column(align=True)
col.label(text=label)
for i, axis in enumerate(labels):
row = col.row(align=True)
split = row.split(factor=0.85, align=True)
split.prop(obj, prop_name, index=i, text=axis)
op = split.operator(OT_ResetProperty.bl_idname, text="", icon='FILE_REFRESH')
op.prop_name = prop_name
class PT_ViewControlPanel(PG_BasePanel, Panel):
bl_label = "View Control (視座位置)"
bl_idname = f"{OP_PREFIX}_PT_view_control"
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="状態を保持してコードコピー")
layout.separator()
box = layout.box()
box.label(text="Perspective Viewpoint", icon='VIEW_CAMERA')
box.prop(props, "slider_limit", text="Range Limit (+/-)")
col = box.column(align=True)
col.prop(props, "view_pos", text="X", index=0)
col.prop(props, "view_pos", text="Y", index=1)
col.prop(props, "view_pos", text="Z", index=2)
box.separator()
box.operator(OT_View_GetCurrent.bl_idname, icon='RESTRICT_VIEW_OFF')
box.operator(OT_View_Reset.bl_idname, icon='LOOP_BACK')
layout.operator(OT_View_CenterSelected.bl_idname, icon='VIEWZOOM')
layout.separator()
box_info = layout.box()
box_info.label(text="Actual View Status", icon='INFO')
r3d = context.space_data.region_3d if context.space_data else None
if r3d:
target_pos = Vector(r3d.view_location)
actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
vec = target_pos - actual_cam_pos
length = vec.length
col_pos = box_info.column(align=True)
col_pos.label(text="[ Actual Position ]", icon='VIEW_CAMERA')
col_pos.label(text=f" X: {actual_cam_pos.x:.4f}")
col_pos.label(text=f" Y: {actual_cam_pos.y:.4f}")
col_pos.label(text=f" Z: {actual_cam_pos.z:.4f}")
col_pos.label(text=f" Distance: {length:.4f}")
box_info.operator(OT_View_CopyActualPos.bl_idname, icon='COPYDOWN')
box_info.separator()
col_ang = box_info.column(align=True)
if length > 0.0001:
a_x = math.degrees(math.acos(vec.x / length))
a_y = math.degrees(math.acos(vec.y / length))
a_z = math.degrees(math.acos(vec.z / length))
p_x = math.degrees(math.asin(vec.x / length))
p_y = math.degrees(math.asin(vec.y / length))
p_z = math.degrees(math.asin(vec.z / length))
col_ang.label(text="[ Direction Angles (軸との角度) ]", icon='ORIENTATION_GLOBAL')
col_ang.label(text=f" X: {a_x:.2f}°")
col_ang.label(text=f" Y: {a_y:.2f}°")
col_ang.label(text=f" Z: {a_z:.2f}°")
col_ang.separator()
col_ang.label(text="[ Planar Angles (直感的な傾き) ]", icon='DRIVER_ROTATIONAL_DIFFERENCE')
col_ang.label(text=f" X (ズレ角): {p_x:.2f}°")
col_ang.label(text=f" Y (ズレ角): {p_y:.2f}°")
col_ang.label(text=f" Z (仰俯角): {p_z:.2f}°")
else:
col_ang.label(text=" Target is too close")
box_info.separator()
box_info.operator(OT_View_CopyAngles.bl_idname, icon='COPYDOWN')
else:
box_info.label(text="Please use in 3D View")
class PT_MainPanel(PG_BasePanel, Panel):
bl_label = "Primitive Generator [Preview]"
bl_idname = f"{OP_PREFIX}_PT_main"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME)
scene = context.scene
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="状態を保持してコードコピー")
row_view = layout.row()
row_view.operator(OT_ViewCenterFront.bl_idname, icon='VIEWZOOM', text="0,0,0 を正面(Y-)から見る")
layout.separator()
layout.prop(props, "primitive_type", text="図形")
row_sol = layout.row()
row_sol.enabled = (props.primitive_type != 'IMAGE_CYLINDER')
row_sol.prop(props, "use_solidify", text="面に厚みを加える")
if props.use_solidify:
self._draw_prop_with_reset(layout, props, "solidify_thickness", text="厚み")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
row = layout.row()
row.scale_y = 1.5
row.operator(OT_CreatePrimitive.bl_idname, icon='PLUS')
box_naming = layout.box()
box_naming.label(text="Naming & Collection")
self._draw_prop_with_reset(box_naming, props, "collection_name")
self._draw_prop_with_reset(box_naming, props, "sub_collection_name")
self._draw_prop_with_reset(box_naming, props, "object_name_prefix")
row_counter = box_naming.row(align=True)
row_counter.label(text=f"Next Index: {scene.b200_object_counter:03d}")
row_counter.operator(OT_ResetCounter.bl_idname, text="", icon='FILE_REFRESH')
layout.separator()
box_prim = layout.box()
prim_type = props.primitive_type
col = box_prim.column(align=True)
if prim_type == 'CUBE': self._draw_prop_with_reset(col, props, "cube_size")
elif prim_type == 'CUBOID': self._draw_vector_xyz(col, props, "cuboid_dimensions", "Dimensions")
elif prim_type == 'PLANE': self._draw_prop_with_reset(col, props, "plane_size_x"); self._draw_prop_with_reset(col, props, "plane_size_y")
elif prim_type == 'CIRCLE': self._draw_prop_with_reset(col, props, "circle_radius"); self._draw_prop_with_reset(col, props, "circle_vertices")
elif prim_type == 'ELLIPSE': self._draw_prop_with_reset(col, props, "ellipse_radius_x"); self._draw_prop_with_reset(col, props, "ellipse_radius_y"); self._draw_prop_with_reset(col, props, "ellipse_vertices")
elif prim_type == 'UV_SPHERE': self._draw_prop_with_reset(col, props, "uv_sphere_radius"); self._draw_prop_with_reset(col, props, "uv_sphere_segments"); self._draw_prop_with_reset(col, props, "uv_sphere_rings")
elif prim_type == 'ICO_SPHERE': self._draw_prop_with_reset(col, props, "ico_sphere_radius"); self._draw_prop_with_reset(col, props, "ico_sphere_subdivisions")
elif prim_type == 'CYLINDER':
col.prop(props, "cylinder_mode", text="")
col.separator(factor=0.5)
self._draw_prop_with_reset(col, props, "cylinder_radius")
if props.cylinder_mode == 'CENTER':
self._draw_prop_with_reset(col, props, "cylinder_depth")
else:
col.separator()
self._draw_vector_xyz(col, props, "cylinder_point_top", "Top Point:")
col.separator(factor=0.5)
self._draw_vector_xyz(col, props, "cylinder_point_bottom", "Bottom Point:")
col.separator(factor=0.5)
self._draw_prop_with_reset(col, props, "cylinder_vertices")
row_caps = col.row(align=True)
row_caps.prop(props, "cylinder_cap_top")
row_caps.prop(props, "cylinder_cap_bottom")
elif prim_type == 'FRUSTUM':
col.prop(props, "frustum_mode", text="")
col.separator(factor=0.5)
self._draw_prop_with_reset(col, props, "frustum_radius_top")
self._draw_prop_with_reset(col, props, "frustum_radius_bottom")
if props.frustum_mode == 'CENTER':
self._draw_prop_with_reset(col, props, "frustum_height")
else:
col.separator()
self._draw_vector_xyz(col, props, "frustum_point_top", "Top Point:")
col.separator(factor=0.5)
self._draw_vector_xyz(col, props, "frustum_point_bottom", "Bottom Point:")
col.separator(factor=0.5)
self._draw_prop_with_reset(col, props, "frustum_vertices")
row_caps = col.row(align=True); row_caps.prop(props, "frustum_cap_top"); row_caps.prop(props, "frustum_cap_bottom")
elif prim_type == 'IMAGE_CYLINDER':
self._draw_prop_with_reset(col, props, "image_cylinder_radius")
self._draw_prop_with_reset(col, props, "image_cylinder_depth")
self._draw_prop_with_reset(col, props, "image_cylinder_vertices")
col.separator()
box_img = col.box()
box_img.label(text="内側に貼る画像:")
box_img.template_ID(props, "image_cylinder_image", open="image.open")
box_img.separator()
self._draw_vector_2d(box_img, props, "image_cylinder_uv_offset", "UV Offset (位置ずらし):")
box_img.separator()
self._draw_vector_2d(box_img, props, "image_cylinder_uv_scale", "UV Scale (繰り返し/縮尺):")
col.separator()
self._draw_prop_with_reset(col, props, "image_cylinder_alpha_outer")
self._draw_prop_with_reset(col, props, "image_cylinder_alpha_inner")
elif prim_type == 'CUBE_FRAME':
self._draw_prop_with_reset(col, props, "cube_frame_size")
self._draw_prop_with_reset(col, props, "cube_frame_radius")
self._draw_prop_with_reset(col, props, "cube_frame_vertices")
elif prim_type == 'CUBOID_FRAME':
self._draw_vector_xyz(col, props, "cuboid_frame_dimensions", "Dimensions")
self._draw_prop_with_reset(col, props, "cuboid_frame_radius")
self._draw_prop_with_reset(col, props, "cuboid_frame_vertices")
elif prim_type == 'TORUS':
self._draw_prop_with_reset(col, props, "torus_major_radius"); self._draw_prop_with_reset(col, props, "torus_minor_radius")
self._draw_prop_with_reset(col, props, "torus_major_segments"); self._draw_prop_with_reset(col, props, "torus_minor_segments")
elif prim_type == 'MONKEY':
self._draw_prop_with_reset(col, props, "monkey_size")
# 配列図形UI
elif prim_type.startswith('SPHERE_'):
self._draw_prop_with_reset(col, props, "sphere_spacing")
if 'SQUARE' in prim_type or 'CUBE' in prim_type:
self._draw_prop_with_reset(col, props, "sphere_size", text="全体サイズ")
else:
self._draw_prop_with_reset(col, props, "sphere_radius_val", text="全体半径")
col.separator()
box_sph = col.box()
box_sph.label(text="個々の球体設定:")
self._draw_prop_with_reset(box_sph, props, "sphere_elem_radius", text="球の半径")
self._draw_prop_with_reset(box_sph, props, "sphere_segments")
self._draw_prop_with_reset(box_sph, props, "sphere_rings")
elif prim_type == 'TORUS_CONCENTRIC':
self._draw_prop_with_reset(col, props, "torus_count", text="同心円の数")
self._draw_prop_with_reset(col, props, "torus_spacing", text="間隔")
self._draw_prop_with_reset(col, props, "torus_minor_radius_arr", text="トーラスの太さ")
self._draw_prop_with_reset(col, props, "torus_major_segments")
self._draw_prop_with_reset(col, props, "torus_minor_segments")
elif prim_type == 'CYLINDER_GRID':
box_c = col.box()
box_c.label(text="Count (本数):")
row_c = box_c.row(align=True)
row_c.prop(props, "grid_count_x", text="X")
row_c.prop(props, "grid_count_y", text="Y")
row_c.prop(props, "grid_count_z", text="Z")
box_s = col.box()
box_s.label(text="Spacing (間隔):")
row_s = box_s.row(align=True)
row_s.prop(props, "grid_spacing_x", text="X")
row_s.prop(props, "grid_spacing_y", text="Y")
row_s.prop(props, "grid_spacing_z", text="Z")
col.separator()
self._draw_prop_with_reset(col, props, "grid_radius", text="円柱の太さ")
self._draw_prop_with_reset(col, props, "grid_vertices", text="円柱の頂点数")
class PT_TransformPanel(PG_BasePanel, Panel):
bl_label = "Transform (位置・回転・スケール)"
bl_idname = f"{OP_PREFIX}_PT_transform"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
props = getattr(context.scene, PROPS_NAME)
layout = self.layout
disable_transform = False
if props.primitive_type == 'CYLINDER' and props.cylinder_mode != 'CENTER':
disable_transform = True
elif props.primitive_type == 'FRUSTUM' and props.frustum_mode != 'CENTER':
disable_transform = True
main_col = layout.column()
main_col.enabled = not disable_transform
if disable_transform:
main_col.label(text="※ 端点指定モードでは操作できません", icon='INFO')
self._draw_vector_xyz(main_col, props, "location", "Location:")
main_col.separator()
col_scale = main_col.column(align=True)
row_scale_uni = col_scale.row(align=True)
row_scale_uni.prop(props, "scale_uniform", text="Uniform Scale")
if props.scale_uniform:
self._draw_prop_with_reset(row_scale_uni, props, "scale_factor", text="")
else:
self._draw_vector_xyz(col_scale, props, "scale_vector", "Scale:")
main_col.separator()
col_rot = main_col.column(align=True)
col_rot.label(text="Rotation (XYZ Euler):")
self._draw_prop_with_reset(col_rot, props, "additional_rotation_x", text="X")
self._draw_prop_with_reset(col_rot, props, "additional_rotation_y", text="Y")
self._draw_prop_with_reset(col_rot, props, "additional_rotation_z", text="Z")
class PT_MaterialPanel(PG_BasePanel, Panel):
bl_label = "Material & Color"
bl_idname = f"{OP_PREFIX}_PT_material"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
props = getattr(context.scene, PROPS_NAME)
layout = self.layout
if props.primitive_type == 'IMAGE_CYLINDER':
layout.label(text="※ 画像パノラマ筒は上部の専用設定が適用されます", icon='INFO')
return
col_mat = layout.column(align=True)
col_mat.prop(props, "color_mode", text="Mode")
if props.color_mode == 'PRESET':
col_mat.prop(props, "preset_set")
box_presets = col_mat.box()
box_presets.label(text="番号クリックで適用 / 色枠で編集:")
grid = box_presets.grid_flow(row_major=True, columns=5, even_columns=True, align=True)
preset_prefix = f"preset_color_{props.preset_set.lower()}_" if props.preset_set != 'A' else "preset_color_"
for i in range(1, 11):
row = grid.row(align=True)
row.prop_enum(props, "preset_color", str(i), text=str(i))
row.prop(props, f"{preset_prefix}{i}", text="")
elif props.color_mode == 'PICKER':
self._draw_prop_with_reset(col_mat, props, "custom_color", text="")
layout.separator()
self._draw_prop_with_reset(layout, props, "face_alpha")
layout.separator()
layout.operator(OT_ResetAllSettings.bl_idname, icon='LOOP_BACK')
class PT_LinksPanel(PG_BasePanel, Panel):
bl_label = "Links"
bl_idname = f"{OP_PREFIX}_PT_links"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
def draw_section(prop_name, link_list, title):
box = layout.box()
row = box.row()
is_expanded = getattr(props, prop_name)
row.prop(props, prop_name, icon="TRIA_DOWN" if is_expanded else "TRIA_RIGHT", emboss=False, text=title)
if is_expanded:
col = box.column(align=True)
for link in link_list:
op = col.operator(OT_OpenUrl.bl_idname, text=link["label"], icon='URL')
op.url = link["url"]
draw_section("show_main_docs", THIS_LINKS, "This Addon")
draw_section("show_new_docs", NEW_DOC_LINKS, "Documents Index")
draw_section("show_old_docs", DOC_LINKS, "Old Documents")
draw_section("show_social", SOCIAL_LINKS, "Social Links")
class PT_RemovePanel(PG_BasePanel, Panel):
bl_label = "System"
bl_idname = f"{OP_PREFIX}_PT_remove"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
self.layout.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Unregister Addon")
# ==============================================================================
# REGISTER
# ==============================================================================
def initial_setup():
context = bpy.context
if not hasattr(context, "scene") or not context.scene:
return 0.1
update_preview_geometry(context)
return None
def open_sidebar():
if not bpy.context: return None
for window in bpy.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.show_region_ui = True
return None
classes = (
PG_B200Props,
OT_ViewCenterFront, OT_CreatePrimitive, OT_CopyFullScript, OT_ResetProperty, OT_ResetAllSettings, OT_ResetCounter, OT_OpenUrl, OT_RemoveAddon,
OT_View_GetCurrent, OT_View_Reset, OT_View_CenterSelected, OT_View_CopyActualPos, OT_View_CopyAngles,
PT_ViewControlPanel,
PT_MainPanel,
PT_TransformPanel,
PT_MaterialPanel,
PT_LinksPanel,
PT_RemovePanel
)
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_B200Props))
bpy.types.Scene.b200_object_counter = IntProperty(name="Object Counter", default=1, min=1)
if not bpy.app.timers.is_registered(initial_setup):
bpy.app.timers.register(initial_setup, first_interval=0.1)
if not bpy.app.timers.is_registered(open_sidebar):
bpy.app.timers.register(open_sidebar, first_interval=0.1)
if not bpy.app.timers.is_registered(view_sync_timer):
bpy.app.timers.register(view_sync_timer)
def unregister():
if bpy.app.timers.is_registered(initial_setup):
bpy.app.timers.unregister(initial_setup)
if bpy.app.timers.is_registered(view_sync_timer):
bpy.app.timers.unregister(view_sync_timer)
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
if hasattr(bpy.types.Scene, 'b200_object_counter'): delattr(bpy.types.Scene, 'b200_object_counter')
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except RuntimeError: pass
if __name__ == "__main__":
try: unregister()
except Exception: pass
register()
# ▲▲▲ ここまで ▲▲▲
'''
# ② Viewport Color & Sun (view2026316) のコードを以下に貼り付け
VIEWPORT_SCRIPT_CONTENT = r'''
# ▼▼▼ ここに「3D Viewport Color & Sun」の全コードを貼り付けてください ▼▼▼
# Copied: 20260319 15:00:01
import bpy
import os
import math
from bpy.props import FloatVectorProperty, FloatProperty, EnumProperty, StringProperty, BoolProperty, PointerProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector, Euler
from math import radians
from datetime import datetime
# ★ このスクリプト自身のID (コピー機能で使用)
# ### ZIONAD_SOURCE_ID: VIEWPORT_COLOR_2026_03_16 ###
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: VIEWPORT_COLOR_2026_03_16 ###"
# アドオンのメタデータ
bl_info = {
"name": "zionad 5520[ 3D Viewport Color & Sun & Perspective ] 20260319",
"author": "zionadchat",
"version": (3, 0, 0),
"blender": (4, 4, 0),
"category": " 5520[ 3D Viewport ] ",
"description": "3Dビューポートの色、太陽、透視投影視座位置をリアルタイム制御します",
"location": "3Dビュー > サイドバー",
}
# 定数
ADDON_CATEGORY_NAME = bl_info["category"]
PREFIX = "view2026316"
VIEW_RESET_BTN_TEXT = "Reset View (0, -10, 10)"
VIEW_POS_INIT = (0.0, -10.0, 10.0)
# ==============================================================================
# デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# プリセットによる上書きを防ぐため、プリセット項目を先に読み込む順序にしています
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"sun_control_mode": 'ANGLE',
"grid_preset": 'white',
"wire_preset": 'orange',
"camera_preset": 'Cam',
"background_type": 'LINEAR',
"header_preset": 'Dark Green',
"preset": 'Dark Green',
"render_preset": 'Blue',
"outliner_preset": 'Outliner 4.4.0',
"text_editor_preset": 'Text 4.4.0',
"sun_target_location": (0.0000, 0.0000, 0.0000),
"sun_rotation": (0.7854, 0.0000, 0.7854),
"sun_location": (0.0000, 0.0000, 10.0000),
"sun_strength": 2.5000,
"custom_grid_scale": 1.0000,
"grid_color": (1.0000, 1.0000, 1.0000, 1.0000),
"wire_color": (0.0000, 0.0000, 0.0000),
"camera_color": (0.4700, 0.5500, 1.0000),
"header_color": (0.0000, 0.0300, 0.0000, 1.0000),
"custom_gradient_high": (0.2256, 0.2800, 0.1424),
"custom_gradient_low": (0.1000, 0.1500, 0.0500),
"reverse_gradient": False,
"render_color": (0.1900, 0.6000, 1.0000, 1.0000),
"render_environment_strength": 1.0000,
"outliner_header_color": (0.1900, 0.1900, 0.1900, 0.7000),
"outliner_background_color": (0.1400, 0.1400, 0.1400, 1.0000),
"text_editor_header_color": (0.1900, 0.1900, 0.1900, 0.7000),
"text_editor_background_color": (0.1400, 0.1400, 0.1400, 1.0000),
"view_pos": (0.0000, -10.0000, 10.0000),
}
# <END_DICT>
# パネル定義
COPY_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_copy_panel"
PERSP_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_persp_control"
OVERLAY_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_overlay_panel"
BG_PANEL_IDNAME_1 = f"{PREFIX}_VIEW3D_PT_solid_background_panel"
HEADER_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_header_panel"
RENDER_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_render_panel"
SUN_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_sun_panel"
GRID_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_gridpanel"
WIRE_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_wirepanel"
CAMERA_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_camerapanel"
OUTLINER_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_outliner_panel"
TEXT_EDITOR_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_text_editor_panel"
LINK_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_link_panel"
REMOVE_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_remove"
# パネルラベル
PANEL_LABELS = {
"COPY": "コードコピー",
"PERSP": "透視投影 視座位置",
"OVERLAY": "Overlays",
"BACKGROUND": "3D Viewport Color",
"HEADER": "Header Color",
"RENDER": "Render Color",
"SUN": "太陽 設定",
"GRID": "Grid Color",
"WIRE": "Wire Color",
"CAMERA": "Camera Color",
"OUTLINER": "Outliner Color",
"TEXT_EDITOR": "Text Editor Color",
"LINK": "リンク",
"REMOVE": "アドオン削除",
}
# プリセット群
BASE_PRESETS =[
("Dark Green", "Dark Green", "Dark Green background", (0.00, 0.28, 0.02), (0.10, 0.15, 0.05)),
("purple", "purple", "purple background", (0.49, 0.45, 1.00), (0.74, 0.65, 0.88)),
("BlueGreen", "BlueGreen", "BlueGreen background", (0.14, 0.34, 0.83), (0.57, 0.88, 0.63)),
("DARK_BLUE", "Dark Blue", "Dark Blue background", (0.07, 0.13, 0.31), (0.05, 0.05, 0.15)),
("Viewport 4.4.0", "Viewport 4.4.0", "Viewport 4.4.0 background", (0.24, 0.24, 0.24), (0.19, 0.19, 0.19)),
("FOREST_GREEN", "Forest Green", "Forest Green background", (0.50, 0.70, 0.50), (0.10, 0.15, 0.05)),
]
HEADER_PRESETS =[
("Dark Green", "Dark Green", "Dark Green header", (0.00, 0.03, 0.00, 1.00)),
("purple", "purple", "purple header", (0.00, 0.00, 0.00, 1.00)),
("BlueGreen", "BlueGreen", "BlueGreen header", (0.00, 0.00, 0.00, 1.00)),
("DARK_BLUE", "Dark Blue", "Dark Blue header", (0.10, 0.10, 0.30, 1.00)),
("Viewport 4.4.0", "Viewport 4.4.0", "Viewport 4.4.0 header", (0.19, 0.19, 0.19, 0.70)),
("FOREST_GREEN", "Forest Green", "Forest Green header", (0.20, 0.30, 0.10, 1.00)),
]
RENDER_PRESETS =[
("Blue", "Blue", "Blue render color", (0.19, 0.60, 1.00, 1.00)),
("Render 4.4.0", "Render 4.4.0", "Render 4.4.0 color", (0.05, 0.05, 0.05, 1.00)),
("LIGHT_GRAY", "Light Gray", "Light gray render", (0.80, 0.80, 0.80, 1.00)),
]
GRID_PRESETS =[
("white", "white", "white grid color", (1.00, 1.00, 1.00, 1.00)),
("Grid 4.4.0", "Grid 4.4.0", "Grid 4.4.0 color", (0.33, 0.33, 0.33, 0.50)),
("DARK_GRAY", "Dark Gray", "Dark gray grid", (0.10, 0.10, 0.10, 1.00)),
]
WIRE_PRESETS =[
("orange", "orange", "orange wire", (0.71, 0.21, 0.05)),
("Wire 4.4.0", "Wire 4.4.0", "Wire 4.4.0 color", (0.00, 0.00, 0.00)),
("WHITE", "White", "White wire", (1.00, 1.00, 1.00)),
]
CAMERA_PRESETS =[
("Cam", "Cam", "Cam camera color", (0.47, 0.55, 1.00)),
("Cam 4.4.0", "Cam 4.4.0", "Cam 4.4.0 color", (0.00, 0.00, 0.00)),
("YELLOW", "Yellow", "Yellow camera", (1.00, 1.00, 0.00)),
]
OUTLINER_PRESETS =[
("Outliner 4.4.0", "Outliner 4.4.0", "Outliner 4.4.0 colors", (0.19, 0.19, 0.19, 0.70), (0.14, 0.14, 0.14, 1.00)),
("DARK_TEAL", "Dark Teal", "Dark teal outliner", (0.00, 0.20, 0.20, 1.00), (0.00, 0.10, 0.10, 1.00)),
]
TEXT_EDITOR_PRESETS =[
("Text 4.4.0", "Text 4.4.0", "Text Editor 4.4.0 colors", (0.19, 0.19, 0.19, 0.70), (0.14, 0.14, 0.14, 1.00)),
("DARK_GREEN", "Dark Green", "Dark green text editor", (0.00, 0.20, 0.00, 1.00), (0.00, 0.10, 0.00, 1.00)),
]
BACKGROUND_TYPES =[
('SINGLE_COLOR', "Single Color", "Uniform background color"),
('LINEAR', "Linear", "Linear gradient background"),
('RADIAL', "Vignette", "Radial gradient simulating a vignette effect"),
]
# ==============================================================================
# リアルタイム更新用コールバック関数(ビューポート色・太陽)
# ==============================================================================
def format_tuple(t):
return '(' + ', '.join(f"{x:.3f}" for x in t) + ')'
def update_custom_grid_scale(self, context):
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
area.spaces.active.overlay.grid_scale = self.custom_grid_scale
def update_grid_color(self, context):
bpy.context.preferences.themes[0].view_3d.grid = self.grid_color
def update_wire_color(self, context):
bpy.context.preferences.themes[0].view_3d.wire = self.wire_color
def update_camera_color(self, context):
bpy.context.preferences.themes[0].view_3d.camera = self.camera_color
def update_background_color(self, context):
gradients = bpy.context.preferences.themes[0].view_3d.space.gradients
if self.background_type == 'SINGLE_COLOR':
gradients.background_type = 'LINEAR'
gradients.high_gradient = gradients.gradient = self.custom_gradient_low if self.reverse_gradient else self.custom_gradient_high
else:
gradients.background_type = self.background_type
gradients.high_gradient = self.custom_gradient_low if self.reverse_gradient else self.custom_gradient_high
gradients.gradient = self.custom_gradient_high if self.reverse_gradient else self.custom_gradient_low
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D': area.tag_redraw()
def update_header_color(self, context):
bpy.context.preferences.themes[0].view_3d.space.header = self.header_color
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D': area.tag_redraw()
def update_render_color(self, context):
bpy.context.preferences.themes[0].image_editor.space.back = self.render_color[:3]
world = bpy.data.worlds.get('MyWorld') or bpy.data.worlds.new('MyWorld')
context.scene.world = world
world.use_nodes = True
bg_node = world.node_tree.nodes.get('Background') or world.node_tree.nodes.new(type='ShaderNodeBackground')
bg_node.name = 'Background'
bg_node.inputs[0].default_value = self.render_color
bg_node.inputs[1].default_value = self.render_environment_strength
output_node = world.node_tree.nodes.get('World Output') or world.node_tree.nodes.new(type='ShaderNodeOutputWorld')
output_node.name = 'World Output'
world.node_tree.links.new(bg_node.outputs[0], output_node.inputs['Surface'])
def update_outliner_color(self, context):
space = bpy.context.preferences.themes[0].outliner.space
space.header = self.outliner_header_color
space.back = self.outliner_background_color[:3]
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'OUTLINER': area.tag_redraw()
def update_text_editor_color(self, context):
space = bpy.context.preferences.themes[0].text_editor.space
space.header = self.text_editor_header_color
space.back = self.text_editor_background_color[:3]
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'TEXT_EDITOR': area.tag_redraw()
def get_or_create_sun():
sun_obj = bpy.data.objects.get("Sun")
if sun_obj is None or sun_obj.type != 'LIGHT' or sun_obj.data.type != 'SUN':
if sun_obj:
try: bpy.data.objects.remove(sun_obj, do_unlink=True)
except: pass
bpy.ops.object.light_add(type='SUN', align='WORLD', location=(0, 0, 0))
sun_obj = bpy.context.active_object
sun_obj.name = "Sun"; sun_obj.data.name = "Sun"
return sun_obj
def update_sun(self, context):
sun = get_or_create_sun()
sun.location = self.sun_location
if self.sun_control_mode == 'ANGLE':
sun.rotation_euler = self.sun_rotation
else:
target_vec = Vector(self.sun_target_location)
sun_vec = Vector(self.sun_location)
if (target_vec - sun_vec).length_squared < 0.0001: return
direction = target_vec - sun_vec
sun.rotation_euler = direction.to_track_quat('-Z', 'Y').to_euler()
sun.data.energy = self.sun_strength
# ==============================================================================
# リアルタイム更新用コールバック関数(透視投影 視座位置)
# ==============================================================================
_is_updating_view = False
def update_view_position(self, context):
"""スライダーが操作されたときに視点を更新する"""
global _is_updating_view
if _is_updating_view: return
props = getattr(context.scene, "persp_view_props", None)
if not props: return
limit = props.slider_limit
v = list(props.view_pos)
clamped = False
for i in range(3):
if v[i] > limit: v[i] = limit; clamped = True
elif v[i] < -limit: v[i] = -limit; clamped = True
if clamped:
_is_updating_view = True
props.view_pos = v
_is_updating_view = False
_is_updating_view = True
try:
cam_pos = Vector(props.view_pos)
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':
r3d = space.region_3d
r3d.view_perspective = 'PERSP'
target_pos = Vector(r3d.view_location)
rel_pos = cam_pos - target_pos
dist = rel_pos.length
if dist > 0.001:
r3d.view_distance = dist
r3d.view_rotation = rel_pos.to_track_quat('Z', 'Y')
finally:
_is_updating_view = False
def view_sync_timer():
"""マウス操作での視点移動を検知し、スライダー(UI)を同期するタイマー"""
global _is_updating_view
if _is_updating_view: return 0.05
context = bpy.context
if getattr(context, "scene", None) is None: return 0.05
props = getattr(context.scene, "persp_view_props", None)
if not props: return 0.05
r3d = None
target_area = None
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':
r3d = space.region_3d
target_area = area
break
if r3d: break
if r3d: break
if r3d and target_area:
target_pos = Vector(r3d.view_location)
actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
current_pos = Vector(props.view_pos)
if (current_pos - actual_cam_pos).length > 0.001:
_is_updating_view = True
max_val = max(abs(actual_cam_pos.x), abs(actual_cam_pos.y), abs(actual_cam_pos.z))
if max_val > props.slider_limit:
props.slider_limit = max_val + 50.0
props.view_pos = actual_cam_pos
_is_updating_view = False
target_area.tag_redraw()
return 0.05
# ------------------------------------------------------------------------
# Property Groups
# ------------------------------------------------------------------------
class SunSettingsProperties(PropertyGroup):
sun_control_mode: EnumProperty(name="制御モード", items=[('ANGLE', "角度", "太陽の回転を直接指定"), ('TARGET', "ターゲット", "指定位置に太陽を向ける")], default=CURRENT_DEFAULTS["sun_control_mode"], update=update_sun)
sun_target_location: FloatVectorProperty(name="ターゲット位置", subtype='XYZ', default=CURRENT_DEFAULTS["sun_target_location"], update=update_sun)
sun_rotation: FloatVectorProperty(name="角度", subtype='EULER', unit='ROTATION', default=CURRENT_DEFAULTS["sun_rotation"], update=update_sun)
sun_location: FloatVectorProperty(name="位置", subtype='XYZ', default=CURRENT_DEFAULTS["sun_location"], update=update_sun)
sun_strength: FloatProperty(name="強さ", default=CURRENT_DEFAULTS["sun_strength"], min=0.0, update=update_sun)
class ViewportColorProperties(PropertyGroup):
custom_grid_scale: FloatProperty(name="Scale", default=CURRENT_DEFAULTS["custom_grid_scale"], min=0.001, update=update_custom_grid_scale)
grid_color: FloatVectorProperty(name="Grid Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["grid_color"], update=update_grid_color)
grid_preset: EnumProperty(name="Grid Preset", items=[(p[0], p[1], p[2]) for p in GRID_PRESETS], default=CURRENT_DEFAULTS["grid_preset"], update=lambda self, context: self.update_grid_preset(context))
wire_color: FloatVectorProperty(name="Wire Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=CURRENT_DEFAULTS["wire_color"], update=update_wire_color)
wire_preset: EnumProperty(name="Wire Preset", items=[(p[0], p[1], p[2]) for p in WIRE_PRESETS], default=CURRENT_DEFAULTS["wire_preset"], update=lambda self, context: self.update_wire_preset(context))
camera_color: FloatVectorProperty(name="Camera Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=CURRENT_DEFAULTS["camera_color"], update=update_camera_color)
camera_preset: EnumProperty(name="Camera Preset", items=[(p[0], p[1], p[2]) for p in CAMERA_PRESETS], default=CURRENT_DEFAULTS["camera_preset"], update=lambda self, context: self.update_camera_preset(context))
background_type: EnumProperty(name="Background Type", items=BACKGROUND_TYPES, default=CURRENT_DEFAULTS["background_type"], update=update_background_color)
header_color: FloatVectorProperty(name="Header Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["header_color"], update=update_header_color)
header_preset: EnumProperty(name="Header Preset", items=[(p[0], p[1], p[2]) for p in HEADER_PRESETS], default=CURRENT_DEFAULTS["header_preset"], update=lambda self, context: self.update_header_preset(context))
custom_gradient_high: FloatVectorProperty(name="Gradient High Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=CURRENT_DEFAULTS["custom_gradient_high"], update=update_background_color)
custom_gradient_low: FloatVectorProperty(name="Gradient Low Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=CURRENT_DEFAULTS["custom_gradient_low"], update=update_background_color)
reverse_gradient: BoolProperty(name="Reverse Gradient", default=CURRENT_DEFAULTS["reverse_gradient"], update=update_background_color)
preset: EnumProperty(name="Color Preset", items=[(p[0], p[1], p[2]) for p in BASE_PRESETS], default=CURRENT_DEFAULTS["preset"], update=lambda self, context: self.update_preset(context))
render_color: FloatVectorProperty(name="Render Background Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["render_color"], update=update_render_color)
render_preset: EnumProperty(name="Render Preset", items=[(p[0], p[1], p[2]) for p in RENDER_PRESETS], default=CURRENT_DEFAULTS["render_preset"], update=lambda self, context: self.update_render_preset(context))
render_environment_strength: FloatProperty(name="Render Environment Strength", default=CURRENT_DEFAULTS["render_environment_strength"], min=0.0, max=1900.0, update=update_render_color)
outliner_header_color: FloatVectorProperty(name="Outliner Header Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["outliner_header_color"], update=update_outliner_color)
outliner_background_color: FloatVectorProperty(name="Outliner Background Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["outliner_background_color"], update=update_outliner_color)
outliner_preset: EnumProperty(name="Outliner Preset", items=[(p[0], p[1], p[2]) for p in OUTLINER_PRESETS], default=CURRENT_DEFAULTS["outliner_preset"], update=lambda self, context: self.update_outliner_preset(context))
text_editor_header_color: FloatVectorProperty(name="Text Editor Header Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["text_editor_header_color"], update=update_text_editor_color)
text_editor_background_color: FloatVectorProperty(name="Text Editor Background Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["text_editor_background_color"], update=update_text_editor_color)
text_editor_preset: EnumProperty(name="Text Editor Preset", items=[(p[0], p[1], p[2]) for p in TEXT_EDITOR_PRESETS], default=CURRENT_DEFAULTS["text_editor_preset"], update=lambda self, context: self.update_text_editor_preset(context))
def update_grid_preset(self, context):
for p in GRID_PRESETS:
if p[0] == self.grid_preset: self.grid_color = p[3]; break
def update_wire_preset(self, context):
for p in WIRE_PRESETS:
if p[0] == self.wire_preset: self.wire_color = p[3]; break
def update_camera_preset(self, context):
for p in CAMERA_PRESETS:
if p[0] == self.camera_preset: self.camera_color = p[3]; break
def update_preset(self, context):
for p in BASE_PRESETS:
if p[0] == self.preset: self.custom_gradient_high = p[3]; self.custom_gradient_low = p[4]; break
def update_header_preset(self, context):
for p in HEADER_PRESETS:
if p[0] == self.header_preset: self.header_color = p[3]; break
def update_render_preset(self, context):
for p in RENDER_PRESETS:
if p[0] == self.render_preset: self.render_color = p[3]; break
def update_outliner_preset(self, context):
for p in OUTLINER_PRESETS:
if p[0] == self.outliner_preset: self.outliner_header_color = p[3]; self.outliner_background_color = p[4]; break
def update_text_editor_preset(self, context):
for p in TEXT_EDITOR_PRESETS:
if p[0] == self.text_editor_preset: self.text_editor_header_color = p[3]; self.text_editor_background_color = p[4]; break
class PerspViewProperties(PropertyGroup):
slider_limit: FloatProperty(name="Range Limit", default=300.0, min=10.0, max=10000.0)
view_pos: FloatVectorProperty(name="View Position", size=3, soft_min=-10000.0, soft_max=10000.0,
default=CURRENT_DEFAULTS.get('view_pos', VIEW_POS_INIT),
update=update_view_position)
# ------------------------------------------------------------------------
# Operators
# ------------------------------------------------------------------------
class OT_ViewCenterFront(Operator):
bl_idname = f"{PREFIX}_wm.view_center_front"
bl_label = "Center 0,0,0 (Front View)"
bl_description = "原点(0,0,0)を画面中央に配置し、Yマイナス方向からの視点(正面)にします"
def execute(self, context):
for area in context.screen.areas:
if area.type == 'VIEW_3D':
rv3d = area.spaces.active.region_3d
if rv3d:
rv3d.view_location = (0.0, 0.0, 0.0)
rv3d.view_rotation = Euler((radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
if rv3d.view_distance < 10.0:
rv3d.view_distance = 60.0
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{PREFIX}_wm.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = context.scene.viewport_color_props
sun_props = context.scene.sun_settings_props
persp_props = context.scene.persp_view_props
code = ""
file_path = globals().get('__file__')
if file_path:
if file_path.endswith('.pyc') or file_path.endswith('.pyo'):
file_path = file_path[:-1]
try:
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
code = f.read()
except Exception:
pass
if not code or SOURCE_ID_TAG not in code:
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string():
code = t.as_string()
break
if not code:
self.report({'ERROR'}, "スクリプトのソースが見つかりません。")
return {'CANCELLED'}
def fmt_vec(v): return "(" + ", ".join(f"{x:.4f}" for x in v) + ")"
def fmt_str(s): return f"'{s}'"
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "sun_control_mode": {fmt_str(sun_props.sun_control_mode)},\n'
new_dict += f' "grid_preset": {fmt_str(props.grid_preset)},\n'
new_dict += f' "wire_preset": {fmt_str(props.wire_preset)},\n'
new_dict += f' "camera_preset": {fmt_str(props.camera_preset)},\n'
new_dict += f' "background_type": {fmt_str(props.background_type)},\n'
new_dict += f' "header_preset": {fmt_str(props.header_preset)},\n'
new_dict += f' "preset": {fmt_str(props.preset)},\n'
new_dict += f' "render_preset": {fmt_str(props.render_preset)},\n'
new_dict += f' "outliner_preset": {fmt_str(props.outliner_preset)},\n'
new_dict += f' "text_editor_preset": {fmt_str(props.text_editor_preset)},\n'
new_dict += f' "sun_target_location": {fmt_vec(sun_props.sun_target_location)},\n'
new_dict += f' "sun_rotation": {fmt_vec(sun_props.sun_rotation)},\n'
new_dict += f' "sun_location": {fmt_vec(sun_props.sun_location)},\n'
new_dict += f' "sun_strength": {sun_props.sun_strength:.4f},\n'
new_dict += f' "custom_grid_scale": {props.custom_grid_scale:.4f},\n'
new_dict += f' "grid_color": {fmt_vec(props.grid_color)},\n'
new_dict += f' "wire_color": {fmt_vec(props.wire_color)},\n'
new_dict += f' "camera_color": {fmt_vec(props.camera_color)},\n'
new_dict += f' "header_color": {fmt_vec(props.header_color)},\n'
new_dict += f' "custom_gradient_high": {fmt_vec(props.custom_gradient_high)},\n'
new_dict += f' "custom_gradient_low": {fmt_vec(props.custom_gradient_low)},\n'
new_dict += f' "reverse_gradient": {props.reverse_gradient},\n'
new_dict += f' "render_color": {fmt_vec(props.render_color)},\n'
new_dict += f' "render_environment_strength": {props.render_environment_strength:.4f},\n'
new_dict += f' "outliner_header_color": {fmt_vec(props.outliner_header_color)},\n'
new_dict += f' "outliner_background_color": {fmt_vec(props.outliner_background_color)},\n'
new_dict += f' "text_editor_header_color": {fmt_vec(props.text_editor_header_color)},\n'
new_dict += f' "text_editor_background_color": {fmt_vec(props.text_editor_background_color)},\n'
new_dict += f' "view_pos": {fmt_vec(persp_props.view_pos)},\n'
new_dict += "}\n"
try:
start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
parts = code.split(start)
if len(parts) < 2: return {'CANCELLED'}
pre = parts[0]
post = code.split(end)[1]
lines = pre.split('\n')
if len(lines) > 0 and lines[0].startswith("# Copied:"): lines.pop(0)
pre = '\n'.join(lines).lstrip('\n')
final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n{pre}{start}\n{new_dict}{end}{post}"
context.window_manager.clipboard = final
self.report({'INFO'}, "現在の数値でコードをコピーしました!")
except Exception as e:
self.report({'ERROR'}, f"Failed to parse code: {str(e)}")
return {'CANCELLED'}
return {'FINISHED'}
class OVERLAY_OT_set_grid_scale(Operator):
bl_idname = f"{PREFIX}_overlay.set_grid_scale"
bl_label = "Set Grid Scale"
scale_value: FloatProperty()
def execute(self, context):
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D': area.spaces.active.overlay.grid_scale = self.scale_value
return {'FINISHED'}
class SUN_OT_Create(Operator):
bl_idname = f"{PREFIX}.create_sun"; bl_label = "太陽を作成"
def execute(self, context):
get_or_create_sun()
self.report({'INFO'}, "太陽を作成しました。"); return {'FINISHED'}
class SUN_OT_Reset(Operator):
bl_idname = f"{PREFIX}.reset_sun"; bl_label = "太陽の設定を初期値にリセット"
def execute(self, context):
props = context.scene.sun_settings_props
props.sun_control_mode, props.sun_target_location = 'ANGLE', (0.0, 0.0, 0.0)
props.sun_rotation = (radians(45.0), radians(0.0), radians(45.0))
props.sun_location, props.sun_strength = (0.0, 0.0, 10.0), 2.5
update_sun(props, context)
self.report({'INFO'}, "太陽の設定をリセットしました。"); return {'FINISHED'}
class RemoveAllPanels(Operator):
bl_idname = f"{PREFIX}_wm.remove_all_panels"; bl_label = PANEL_LABELS["REMOVE"]
def execute(self, context): unregister(); return {'FINISHED'}
# --- 透視投影用のオペレーター ---
class PERSP_OT_GetCurrentView(Operator):
bl_idname = f"{PREFIX}_persp.get_current_view"
bl_label = "Get Current View & Update"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, "persp_view_props", None)
r3d = context.space_data.region_3d if context.space_data else None
if not props or not r3d: return {'CANCELLED'}
target_pos = Vector(r3d.view_location)
actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
max_val = max(abs(actual_cam_pos.x), abs(actual_cam_pos.y), abs(actual_cam_pos.z))
if max_val > props.slider_limit: props.slider_limit = max_val + 50.0
props.view_pos = actual_cam_pos
return {'FINISHED'}
class PERSP_OT_ResetView(Operator):
bl_idname = f"{PREFIX}_persp.reset_view"
bl_label = VIEW_RESET_BTN_TEXT
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, "persp_view_props", None)
if props:
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.region_3d.view_location = (0.0, 0.0, 0.0)
props.view_pos = VIEW_POS_INIT
return {'FINISHED'}
class PERSP_OT_CenterSelected(Operator):
bl_idname = f"{PREFIX}_persp.center_selected"
bl_label = "Center Selected Object"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.view3d.view_selected()
r3d = context.space_data.region_3d if context.space_data else None
props = getattr(context.scene, "persp_view_props", None)
if r3d and props:
target_pos = Vector(r3d.view_location)
props.view_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
return {'FINISHED'}
class PERSP_OT_CopyActualViewPos(Operator):
bl_idname = f"{PREFIX}_persp.copy_actual_pos"
bl_label = "Copy Position Only"
def execute(self, context):
r3d = context.space_data.region_3d if context.space_data else None
if not r3d: return {'CANCELLED'}
target_pos = Vector(r3d.view_location)
p = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
context.window_manager.clipboard = f"Actual View Pos: ({p.x:.4f}, {p.y:.4f}, {p.z:.4f})"
self.report({'INFO'}, "視座位置をコピーしました")
return {'FINISHED'}
class PERSP_OT_CopyAngles(Operator):
bl_idname = f"{PREFIX}_persp.copy_angles"
bl_label = "Copy Full Info (Pos & Angles)"
def execute(self, context):
props = getattr(context.scene, "persp_view_props", None)
r3d = context.space_data.region_3d if context.space_data else None
if not r3d or not props: return {'CANCELLED'}
target_pos = Vector(r3d.view_location)
actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
vec = target_pos - actual_cam_pos
length = vec.length
if length < 0.0001: return {'CANCELLED'}
ang_x = math.degrees(math.acos(vec.x / length))
ang_y = math.degrees(math.acos(vec.y / length))
ang_z = math.degrees(math.acos(vec.z / length))
pl_x = math.degrees(math.asin(vec.x / length))
pl_y = math.degrees(math.asin(vec.y / length))
pl_z = math.degrees(math.asin(vec.z / length))
info_text = (
f"--- View Direction Info ---\n"
f"[ Actual 3D View Status ]\n"
f"Actual View Pos : ({actual_cam_pos.x:.4f}, {actual_cam_pos.y:.4f}, {actual_cam_pos.z:.4f})\n"
f"Target Pos : ({target_pos.x:.4f}, {target_pos.y:.4f}, {target_pos.z:.4f})\n"
f"Distance : {length:.4f}\n\n"
f"[ Direction Angles (軸そのものとの角度 0〜180°) ]\n"
f"Angle from X Axis : {ang_x:.2f} deg\n"
f"Angle from Y Axis : {ang_y:.2f} deg\n"
f"Angle from Z Axis : {ang_z:.2f} deg\n\n"
f"[ Planar Angles (直感的な傾き・ズレ角 -90〜90°) ]\n"
f"X (横のズレ角) : {pl_x:.2f} deg\n"
f"Y (前後の傾き) : {pl_y:.2f} deg\n"
f"Z (仰角・俯角) : {pl_z:.2f} deg\n"
)
context.window_manager.clipboard = info_text
self.report({'INFO'}, "情報全体をクリップボードにコピーしました")
return {'FINISHED'}
# ------------------------------------------------------------------------
# Panels
# ------------------------------------------------------------------------
class BasePanel(Panel):
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME
class VIEW3D_PT_CopyPanel(BasePanel):
bl_label = PANEL_LABELS["COPY"]; bl_idname = COPY_PANEL_IDNAME; bl_order = 0
def draw(self, context):
layout = self.layout
row = layout.row()
row.scale_y = 1.2
row.operator(f"{PREFIX}_wm.copy_script", icon='COPY_ID', text="最新数値付きコードコピー")
row_view = layout.row()
row_view.operator(f"{PREFIX}_wm.view_center_front", icon='VIEWZOOM', text="0,0,0 を正面(Y-)から見る")
class VIEW3D_PT_PerspControlPanel(BasePanel):
bl_label = PANEL_LABELS["PERSP"]; bl_idname = PERSP_PANEL_IDNAME; bl_order = 1
def draw(self, context):
layout = self.layout
props = getattr(context.scene, "persp_view_props", None)
if not props: return
box = layout.box()
box.label(text="Perspective Viewpoint", icon='VIEW_CAMERA')
box.prop(props, "slider_limit", text="Range Limit (+/-)")
col = box.column(align=True)
col.prop(props, "view_pos", text="X", index=0)
col.prop(props, "view_pos", text="Y", index=1)
col.prop(props, "view_pos", text="Z", index=2)
box.separator()
box.operator(f"{PREFIX}_persp.get_current_view", icon='RESTRICT_VIEW_OFF')
box.operator(f"{PREFIX}_persp.reset_view", icon='LOOP_BACK')
layout.operator(f"{PREFIX}_persp.center_selected", icon='VIEWZOOM')
layout.separator()
box_info = layout.box()
box_info.label(text="Actual View Status", icon='INFO')
r3d = context.space_data.region_3d if context.space_data else None
if r3d:
target_pos = Vector(r3d.view_location)
actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
vec = target_pos - actual_cam_pos
length = vec.length
col_pos = box_info.column(align=True)
col_pos.label(text="[ Actual Position ]", icon='VIEW_CAMERA')
col_pos.label(text=f" X: {actual_cam_pos.x:.4f}")
col_pos.label(text=f" Y: {actual_cam_pos.y:.4f}")
col_pos.label(text=f" Z: {actual_cam_pos.z:.4f}")
col_pos.label(text=f" Distance: {length:.4f}")
box_info.operator(f"{PREFIX}_persp.copy_actual_pos", icon='COPYDOWN')
box_info.separator()
col_ang = box_info.column(align=True)
if length > 0.0001:
a_x = math.degrees(math.acos(vec.x / length))
a_y = math.degrees(math.acos(vec.y / length))
a_z = math.degrees(math.acos(vec.z / length))
p_x = math.degrees(math.asin(vec.x / length))
p_y = math.degrees(math.asin(vec.y / length))
p_z = math.degrees(math.asin(vec.z / length))
col_ang.label(text="[ Direction Angles (軸との角度) ]", icon='ORIENTATION_GLOBAL')
col_ang.label(text=f" X: {a_x:.2f}°")
col_ang.label(text=f" Y: {a_y:.2f}°")
col_ang.label(text=f" Z: {a_z:.2f}°")
col_ang.separator()
col_ang.label(text="[ Planar Angles (直感的な傾き) ]", icon='DRIVER_ROTATIONAL_DIFFERENCE')
col_ang.label(text=f" X (ズレ角): {p_x:.2f}°")
col_ang.label(text=f" Y (ズレ角): {p_y:.2f}°")
col_ang.label(text=f" Z (仰俯角): {p_z:.2f}°")
else:
col_ang.label(text=" Target is too close")
box_info.separator()
box_info.operator(f"{PREFIX}_persp.copy_angles", icon='COPYDOWN')
else:
box_info.label(text="Please use in 3D View")
class VIEW3D_PT_OverlayPanel(BasePanel):
bl_label = PANEL_LABELS["OVERLAY"]; bl_idname = OVERLAY_PANEL_IDNAME; bl_order = 2
def draw(self, context):
layout, props = self.layout, context.scene.viewport_color_props
if context.space_data.type == 'VIEW_3D':
layout.prop(context.space_data.overlay, "show_floor", text="Floor")
layout.prop(props, "custom_grid_scale", text="Scale 数値入力")
row = layout.row(align=True)
row.operator(f"{PREFIX}_overlay.set_grid_scale", text="入力値").scale_value = props.custom_grid_scale
row.operator(f"{PREFIX}_overlay.set_grid_scale", text="10.0").scale_value = 10.0
row.operator(f"{PREFIX}_overlay.set_grid_scale", text="100.0").scale_value = 100.0
else:
layout.label(text="3D Viewport is required.")
class VIEW3D_PT_solid_background_panel(BasePanel):
bl_label = PANEL_LABELS["BACKGROUND"]; bl_idname = BG_PANEL_IDNAME_1; bl_order = 3
def draw(self, context):
layout, props = self.layout, context.scene.viewport_color_props
layout.prop(props, "preset", text="Background Preset")
layout.prop(props, "background_type", expand=True)
layout.prop(props, "custom_gradient_high", text="Color High" if props.background_type != 'SINGLE_COLOR' else "Color")
if props.background_type != 'SINGLE_COLOR': layout.prop(props, "custom_gradient_low")
layout.prop(props, "reverse_gradient", text="Reverse Gradient")
class VIEW3D_PT_HeaderPanel(BasePanel):
bl_label = PANEL_LABELS["HEADER"]; bl_idname = HEADER_PANEL_IDNAME; bl_order = 4
def draw(self, context):
layout, props = self.layout, context.scene.viewport_color_props
layout.prop(props, "header_preset", text="Header Preset")
layout.prop(props, "header_color")
class VIEW3D_PT_RenderPanel(BasePanel):
bl_label = PANEL_LABELS["RENDER"]; bl_idname = RENDER_PANEL_IDNAME; bl_order = 5
def draw(self, context):
layout, props = self.layout, context.scene.viewport_color_props
layout.prop(props, "render_preset")
layout.prop(props, "render_color")
layout.prop(props, "render_environment_strength")
class VIEW3D_PT_SunPanel(BasePanel):
bl_label = PANEL_LABELS["SUN"]; bl_idname = SUN_PANEL_IDNAME; bl_order = 6
def draw(self, context):
layout, props = self.layout, context.scene.sun_settings_props
row = layout.row(align=True)
row.operator(f"{PREFIX}.create_sun", text="太陽作成ボタン")
row.operator(f"{PREFIX}.reset_sun", icon='FILE_REFRESH', text="")
layout.separator()
layout.prop(props, "sun_control_mode", expand=True)
if props.sun_control_mode == 'ANGLE': layout.prop(props, "sun_rotation")
else: layout.prop(props, "sun_target_location")
layout.prop(props, "sun_location"); layout.prop(props, "sun_strength")
class VIEW3D_PT_GridPanel(BasePanel):
bl_label = PANEL_LABELS["GRID"]; bl_idname = GRID_PANEL_IDNAME; bl_order = 7
def draw(self, context):
layout, props = self.layout, context.scene.viewport_color_props
layout.prop(props, "grid_preset")
layout.prop(props, "grid_color")
class VIEW3D_PT_WirePanel(BasePanel):
bl_label = PANEL_LABELS["WIRE"]; bl_idname = WIRE_PANEL_IDNAME; bl_order = 8
def draw(self, context):
layout, props = self.layout, context.scene.viewport_color_props
layout.prop(props, "wire_preset")
layout.prop(props, "wire_color")
class VIEW3D_PT_CameraPanel(BasePanel):
bl_label = PANEL_LABELS["CAMERA"]; bl_idname = CAMERA_PANEL_IDNAME; bl_order = 9
def draw(self, context):
layout, props = self.layout, context.scene.viewport_color_props
layout.prop(props, "camera_preset")
layout.prop(props, "camera_color")
class VIEW3D_PT_OutlinerPanel(BasePanel):
bl_label = PANEL_LABELS["OUTLINER"]; bl_idname = OUTLINER_PANEL_IDNAME; bl_order = 10
def draw(self, context):
layout, props = self.layout, context.scene.viewport_color_props
layout.prop(props, "outliner_preset")
layout.prop(props, "outliner_header_color")
layout.prop(props, "outliner_background_color")
class VIEW3D_PT_TextEditorPanel(BasePanel):
bl_label = PANEL_LABELS["TEXT_EDITOR"]; bl_idname = TEXT_EDITOR_PANEL_IDNAME; bl_order = 11
def draw(self, context):
layout, props = self.layout, context.scene.viewport_color_props
layout.prop(props, "text_editor_preset")
layout.prop(props, "text_editor_header_color")
layout.prop(props, "text_editor_background_color")
class VIEW3D_PT_LinkPanel(BasePanel):
bl_label = PANEL_LABELS["LINK"]; bl_idname = LINK_PANEL_IDNAME; bl_order = 12
def draw(self, context):
layout = self.layout
layout.operator("wm.url_open", text="進化版 画面中央 透視投影視座位置 20260319bb", icon='URL').url = "<https://www.notion.so/20260319bb-327f5dacaf43801e8e37ce489dc1d593>"
layout.operator("wm.url_open", text="5520 背景色 変更 20260316版", icon='URL').url = "<https://www.notion.so/5520-20260316-314f5dacaf4380da9be4c05551d40710>"
class VIEW3D_PT_RemovePanel(BasePanel):
bl_label = PANEL_LABELS["REMOVE"]; bl_idname = REMOVE_PANEL_IDNAME; bl_order = 13
def draw(self, context):
self.layout.operator(f"{PREFIX}_wm.remove_all_panels", text=PANEL_LABELS["REMOVE"])
# ------------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------------
classes =[
SunSettingsProperties, ViewportColorProperties, PerspViewProperties,
OT_CopyFullScript, OT_ViewCenterFront,
OVERLAY_OT_set_grid_scale,
SUN_OT_Create, SUN_OT_Reset,
PERSP_OT_GetCurrentView, PERSP_OT_ResetView, PERSP_OT_CenterSelected, PERSP_OT_CopyActualViewPos, PERSP_OT_CopyAngles,
RemoveAllPanels,
VIEW3D_PT_CopyPanel, VIEW3D_PT_PerspControlPanel,
VIEW3D_PT_OverlayPanel, VIEW3D_PT_solid_background_panel,
VIEW3D_PT_HeaderPanel, VIEW3D_PT_RenderPanel, VIEW3D_PT_SunPanel, VIEW3D_PT_GridPanel,
VIEW3D_PT_WirePanel, VIEW3D_PT_CameraPanel, VIEW3D_PT_OutlinerPanel, VIEW3D_PT_TextEditorPanel,
VIEW3D_PT_LinkPanel, VIEW3D_PT_RemovePanel
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.viewport_color_props = PointerProperty(type=ViewportColorProperties)
bpy.types.Scene.sun_settings_props = PointerProperty(type=SunSettingsProperties)
bpy.types.Scene.persp_view_props = PointerProperty(type=PerspViewProperties)
def apply_initial_settings():
if bpy.context.scene and hasattr(bpy.context.scene, 'viewport_color_props'):
props = bpy.context.scene.viewport_color_props
sun_props = bpy.context.scene.sun_settings_props
persp_props = bpy.context.scene.persp_view_props
for key, val in CURRENT_DEFAULTS.items():
if hasattr(props, key): setattr(props, key, val)
elif hasattr(sun_props, key): setattr(sun_props, key, val)
elif hasattr(persp_props, key): setattr(persp_props, key, val)
update_background_color(props, bpy.context)
update_header_color(props, bpy.context)
update_render_color(props, bpy.context)
update_grid_color(props, bpy.context)
update_wire_color(props, bpy.context)
update_camera_color(props, bpy.context)
update_outliner_color(props, bpy.context)
update_text_editor_color(props, bpy.context)
update_sun(sun_props, bpy.context)
bpy.app.timers.register(apply_initial_settings, first_interval=0.1)
if not bpy.app.timers.is_registered(view_sync_timer):
bpy.app.timers.register(view_sync_timer)
def unregister():
if hasattr(bpy.types.Scene, 'viewport_color_props'): del bpy.types.Scene.viewport_color_props
if hasattr(bpy.types.Scene, 'sun_settings_props'): del bpy.types.Scene.sun_settings_props
if hasattr(bpy.types.Scene, 'persp_view_props'): del bpy.types.Scene.persp_view_props
for cls in reversed(classes):
try: bpy.utils.unregister_class(cls)
except RuntimeError: pass
if bpy.app.timers.is_registered(view_sync_timer):
bpy.app.timers.unregister(view_sync_timer)
if __name__ == "__main__":
register()
# ▲▲▲ ここまで ▲▲▲
'''
# ③ Fixed Camera & World (cam_kotei...) のコードを以下に貼り付け
CAMERA_SCRIPT_CONTENT = r'''
# ▼▼▼ ここに「v100 Fixed Camera & World」の全コードを貼り付けてください ▼▼▼
import bpy
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
from datetime import datetime
# --- ユニークID生成 ---
START_TIMESTAMP = datetime.now().strftime("%Y%m%d%H%M%S")
PREFIX = f"cam_kotei{START_TIMESTAMP}"
# --- bl_info ---
bl_info = {
"name": "zionad v100 [Fixed Camera & World]",
"author": "zionadchat",
"version": (35, 0, 5), # バージョンアップ
"blender": (4, 1, 0),
"location": "View3D > Sidebar > zionad Control",
"description": "カメラの位置固定、向き(YPR)、レンズ制御に加え、ワールド(HDRI/背景)設定機能を提供します。",
"category": " v100[ 固定 Camera ] ",
}
# ======================================================================
# --- ユーザー設定 / Parameters to Customize ---
# ======================================================================
ADDON_CATEGORY_NAME = bl_info["category"]
# --- HDRI画像ファイルのフルパスリスト ---
# ▼▼▼【変更点】ご指定のHDRIパスをリストの2番目に追加しました ▼▼▼
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",
]
# --- ワイヤーフレームの色プリセット ---
# 形式: ("ID", "ラベル", "説明", (R, G, B))
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)),
]
# --- グリッドの色プリセット ---
# 形式: ("ID", "ラベル", "説明", (R, G, B, A))
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)),
]
# --- 専用カメラのコレクション名とオブジェクト名 ---
CAMERA_COLLECTION_NAME = "Cam"
DEDICATED_CAMERA_NAME = "Fixed_Cam"
# ======================================================================
# --- 定数定義 / Constants ---
# ======================================================================
SENSOR_WIDTH = 36.0
FOV_PRESETS = [1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]
CAMERA_COLOR_PRESETS = [("CYAN", "Cyan", "水色", (0.0, 1.0, 1.0)), ("Cam 4.4.0", "Cam 4.4.0", "Blenderデフォルト色", (0.0, 0.0, 0.0)), ("YELLOW", "Yellow", "黄色", (1.0, 1.0, 0.0)), ("PURPLE", "Purple", "紫色", (0.5, 0.0, 0.5)),]
# --- リンクパネル用データ ---
ADDON_LINKS = ({"label": "カメラ 固定 Git 管理 20250711", "url":"<https://memo2017.hatenablog.com/entry/2025/07/11/131157>"},)
NEW_DOC_LINKS = [
{"label": "完成品 目次", "url": "<https://mokuji000zionad.hatenablog.com/entry/2025/05/30/135936>"},
]
DOC_LINKS = [
{"label": "812 地球儀 経度 緯度でのコントロール 20250302", "url": "<https://sortphotos2025.hatenablog.jp/entry/2025/03/02/211757>"},
{"label": "アドオン目次 from 20250227", "url": "<https://sortphotos2025.hatenablog.jp/entry/2025/02/27/201251>"},
{"label": "addon 目次整理 from 20250116", "url": "<https://blenderzionad.hatenablog.com/entry/2025/01/17/002322>"},
]
SOCIAL_LINKS = [
{"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
{"label": "Posfie zionad2022", "url": "<https://posfie.com/t/zionad2022>"},
{"label": "X (Twitter) zionadchat", "url": "<https://x.com/zionadchat>"},
{"label": "単純トリック 2025 open", "url": "<https://www.notion.so/2025-open-221b3deba7a2809a85a9f5ab5600ab06>"},
]
# --- パネルIDと順序 ---
PANEL_IDS = {
"SETUP": f"{PREFIX}_PT_setup", "POSITION": f"{PREFIX}_PT_position", "AIMING": f"{PREFIX}_PT_aiming",
"LENS": f"{PREFIX}_PT_lens", "CAMERA_DISPLAY": f"{PREFIX}_PT_camera_display", "WORLD_CONTROL": f"{PREFIX}_PT_world_control",
"INFO": f"{PREFIX}_PT_info", "GRID": f"{PREFIX}_PT_grid_panel", "WIRE": f"{PREFIX}_PT_wire_panel",
"LINKS": f"{PREFIX}_PT_links",
"LINKS_NEWDOC": f"{PREFIX}_PT_links_newdoc", "LINKS_DOC": f"{PREFIX}_PT_links_doc", "LINKS_SOCIAL": f"{PREFIX}_PT_links_social",
"REMOVE": f"{PREFIX}_PT_remove",
}
PANEL_ORDER = {PANEL_IDS["SETUP"]: 0, PANEL_IDS["POSITION"]: 1, PANEL_IDS["AIMING"]: 2, PANEL_IDS["LENS"]: 3, PANEL_IDS["CAMERA_DISPLAY"]: 4, PANEL_IDS["WORLD_CONTROL"]: 5, PANEL_IDS["INFO"]: 6, PANEL_IDS["GRID"]: 89, PANEL_IDS["WIRE"]: 90, PANEL_IDS["LINKS"]: 100, PANEL_IDS["REMOVE"]: 200,}
# --- グローバル状態管理 ---
_is_updating_by_addon = False; _update_timer = None
def reset_update_flag(): global _is_updating_by_addon, _update_timer; _is_updating_by_addon = False; _update_timer = None; return None
def schedule_update_flag_reset():
global _update_timer
if _update_timer and bpy.app.timers.is_registered(reset_update_flag): bpy.app.timers.unregister(reset_update_flag)
bpy.app.timers.register(reset_update_flag, first_interval=0.01)
# --- World Tools ヘルパー関数 ---
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 RuntimeError as e: print(f"Error loading image: {e}"); return False
print(f"File not found: {filepath}"); return False
def update_viewport(context):
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D': space.shading.type = 'RENDERED'; 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)
# --- プロパティグループ ---
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))
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))
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()
class SurfaceCameraProperties(PropertyGroup):
camera_obj: PointerProperty(name="操作カメラ", type=bpy.types.Object, poll=lambda self, obj: obj.type == 'CAMERA', update=lambda s,c: update_surface_camera(s,c))
fixed_location: FloatVectorProperty(name="固定位置", default=(0.0, -10.0, 0.0), subtype='XYZ', update=lambda s,c: update_surface_camera(s,c))
target_location: FloatVectorProperty(name="固定注視点", default=(0, 0, 0), subtype='XYZ', update=lambda s,c: update_surface_camera(s,c))
offset_yaw: FloatProperty(name="Yaw", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c)); offset_pitch: FloatProperty(name="Pitch", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c)); offset_roll: FloatProperty(name="Roll", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
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=lambda s,c: update_surface_camera(s,c))
clip_start: FloatProperty(name="クリップ開始", default=0.1, min=0.001, update=lambda s,c: update_surface_camera(s,c)); clip_end: FloatProperty(name="クリップ終了", default=1000.0, min=1.0, update=lambda s,c: update_surface_camera(s,c))
info_precision: EnumProperty(name="桁数", items=[('1', '1', ''), ('2', '2', ''), ('3', '3', '')], default='1', update=lambda s,c: update_info_panel_text(s,c))
info_focal_length: StringProperty(name="焦点距離"); info_horizontal_fov: StringProperty(name="水平視野角"); info_camera_location: StringProperty(name="カメラ位置"); info_target_location: StringProperty(name="注視点位置"); info_distance_to_target: StringProperty(name="注視点までの距離"); info_clip_setting: StringProperty(name="クリップ範囲"); info_viewable_width: StringProperty(name="注視点での横幅")
camera_color: FloatVectorProperty(name="カメラカラー", subtype='COLOR', size=3, min=0.0, max=1.0, default=(0.0, 1.0, 1.0))
camera_preset: EnumProperty(name="カメラプリセット", items=[(p[0], p[1], p[2]) for p in CAMERA_COLOR_PRESETS], default="CYAN", update=lambda self, context: SFC_OT_ApplyCameraColor.update_preset(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 (ZeroDivisionError, ValueError): 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 (ZeroDivisionError, ValueError): return 50.0
def get_target_location(props):
return mathutils.Vector(props.target_location)
def update_object_transform(obj, props):
location = mathutils.Vector(props.fixed_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.location = location; obj.rotation_euler = final_quat.to_euler('XYZ')
def update_surface_camera(self, context):
global _is_updating_by_addon
if _is_updating_by_addon: return
_is_updating_by_addon = True
try:
props, camera_obj = context.scene.surface_camera_properties, context.scene.surface_camera_properties.camera_obj
if props.is_updating_settings or not camera_obj: update_info_panel_text(props, context); return
cam_data = camera_obj.data
if cam_data: cam_data.sensor_fit, cam_data.lens_unit, cam_data.lens, cam_data.clip_start, cam_data.clip_end = 'HORIZONTAL', 'MILLIMETERS', props.lens_focal_length, props.clip_start, props.clip_end
update_object_transform(camera_obj, props); update_info_panel_text(props, context)
finally: schedule_update_flag_reset()
def update_info_panel_text(props, context):
if not hasattr(context, 'scene') or not props: return
precision, fmt = int(props.info_precision), f".{props.info_precision}f"
camera_location, target_location = mathutils.Vector(props.fixed_location), get_target_location(props)
props.info_camera_location = f"({camera_location.x:{fmt}}, {camera_location.y:{fmt}}, {camera_location.z:{fmt}})"; current_fov = calculate_horizontal_fov(props.lens_focal_length); props.info_horizontal_fov = f"{current_fov:{fmt}} °"; props.info_focal_length = f"{props.lens_focal_length:{fmt}} mm"
props.info_target_location = f"({target_location.x:{fmt}}, {target_location.y:{fmt}}, {target_location.z:{fmt}})"; distance = (target_location - camera_location).length; props.info_distance_to_target = f"{distance:{fmt}}"
if distance > 0 and current_fov > 0: props.info_viewable_width = f"{2 * distance * math.tan(math.radians(current_fov) / 2):{fmt}}"
else: props.info_viewable_width = "N/A"
props.info_clip_setting = f"{props.clip_start:{fmt}} - {props.clip_end:{fmt}}"
def sync_ui_from_manual_transform(props, obj, context):
global _is_updating_by_addon
if _is_updating_by_addon: return
_is_updating_by_addon = True
try:
props.fixed_location = obj.location
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, final_quat = direction.to_track_quat('-Z', 'Y'), obj.matrix_world.to_quaternion()
offset_quat, offset_euler = base_track_quat.inverted() @ final_quat, offset_quat.to_euler('XYZ')
props.offset_pitch, props.offset_yaw, props.offset_roll = offset_euler.x, offset_euler.y, offset_euler.z
finally: _is_updating_by_addon = False
update_info_panel_text(props, context)
@bpy.app.handlers.persistent
def on_depsgraph_update(scene, depsgraph):
if _is_updating_by_addon: return
context = bpy.context
if not (hasattr(context, 'scene') and context.scene): return
sfc_props = context.scene.surface_camera_properties
for update in depsgraph.updates:
if not update.is_updated_transform: continue
obj_id = update.id.original
if sfc_props.camera_obj and obj_id == sfc_props.camera_obj: sync_ui_from_manual_transform(sfc_props, sfc_props.camera_obj, context); return
# --- オペレーター ---
class SFC_OT_ApplyCameraColor(Operator):
bl_idname = f"{PREFIX}.apply_camera_color"; bl_label = "カメラカラー適用"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context): context.preferences.themes[0].view_3d.camera = context.scene.surface_camera_properties.camera_color; return {'FINISHED'}
@staticmethod
def update_preset(self, context): props = context.scene.surface_camera_properties; props.camera_color = next((p[3] for p in CAMERA_COLOR_PRESETS if p[0] == props.camera_preset), props.camera_color); getattr(bpy.ops, f"{PREFIX}.apply_camera_color")()
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'}, f"グリッドの色をコピーしました: {context.window_manager.clipboard}"); return {'FINISHED'}
class SFC_OT_CreateDedicatedCamera(Operator):
bl_idname = f"{PREFIX}.create_dedicated_camera"; bl_label = "専用カメラ作成"
def execute(self, context):
if DEDICATED_CAMERA_NAME not in bpy.data.objects:
cam_data = bpy.data.cameras.new(name=DEDICATED_CAMERA_NAME); cam_obj = bpy.data.objects.new(DEDICATED_CAMERA_NAME, cam_data)
cam_collection = bpy.data.collections.get(CAMERA_COLLECTION_NAME) or bpy.data.collections.new(CAMERA_COLLECTION_NAME)
if CAMERA_COLLECTION_NAME not in context.scene.collection.children: context.scene.collection.children.link(cam_collection)
cam_collection.objects.link(cam_obj)
if cam_obj.name in context.scene.collection.objects: context.scene.collection.objects.unlink(cam_obj)
else: cam_obj = bpy.data.objects[DEDICATED_CAMERA_NAME]
props = context.scene.surface_camera_properties; props.camera_obj = cam_obj; props.is_updating_settings = True
for key in props.bl_rna.properties.keys():
if key not in ['camera_obj', 'bl_rna', 'is_updating_settings'] and not props.bl_rna.properties[key].is_readonly: props.property_unset(key)
props.is_updating_settings = False; update_surface_camera(props, context); self.report({'INFO'}, f"カメラ '{DEDICATED_CAMERA_NAME}' を作成/選択し、初期化しました。"); return {'FINISHED'}
class SFC_OT_SyncWithCamera(Operator):
bl_idname = f"{PREFIX}.sync_with_camera"; bl_label = "UIを同期"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props, cam_obj = context.scene.surface_camera_properties, context.scene.surface_camera_properties.camera_obj
if not cam_obj or cam_obj.type != 'CAMERA': self.report({'WARNING'}, "有効なカメラが選択されていません。"); return {'CANCELLED'}
context.scene.camera = cam_obj; cam_data = cam_obj.data; props.is_updating_settings = True
props.lens_focal_length, props.clip_start, props.clip_end = cam_data.lens, cam_data.clip_start, cam_data.clip_end
props.is_updating_settings = False; sync_ui_from_manual_transform(props, cam_obj, context); self.report({'INFO'}, f"カメラ '{cam_obj.name}' の設定をUIに読み込みました。"); return {'FINISHED'}
class SFC_OT_UnlinkObject(Operator):
bl_idname = f"{PREFIX}.unlink_object"; bl_label = "解除"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props, update_func, obj_prop = context.scene.surface_camera_properties, update_surface_camera, 'camera_obj'
if getattr(props, obj_prop): self.report({'INFO'}, f"'{getattr(props, obj_prop).name}' との関連付けを解除しました。"); setattr(props, obj_prop, None)
props.is_updating_settings = True
for key in props.bl_rna.properties.keys():
if key not in ['bl_rna', 'is_updating_settings', 'camera_obj'] and not props.bl_rna.properties[key].is_readonly: props.property_unset(key)
props.is_updating_settings = False; update_func(props, context); 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, update_func = context.scene.surface_camera_properties, update_surface_camera
prop_groups = {"location": ["fixed_location"],"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 group_props in prop_groups.values(): props_to_reset.update(group_props)
else:
for name in target_names: props_to_reset.update(prop_groups.get(name, []))
props.is_updating_settings = True
for prop_name in props_to_reset:
if hasattr(props, prop_name): props.property_unset(prop_name)
props.is_updating_settings = False; update_func(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_CopyAllInfo(Operator):
bl_idname = f"{PREFIX}.copy_all_info"; bl_label = "全情報コピー"
def execute(self, context):
props=context.scene.surface_camera_properties; context.window_manager.clipboard = (f"カメラ情報 ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})\n----------------------------------------\n" f"焦点距離: {props.info_focal_length}\n水平視野角: {props.info_horizontal_fov}\nカメラ位置: {props.info_camera_location}\n" f"注視点: {props.info_target_location}\n注視点までの距離: {props.info_distance_to_target}\n注視点での横幅: {props.info_viewable_width}\n" f"クリップ範囲: {props.info_clip_setting}\n----------------------------------------"); self.report({'INFO'}, "全情報をクリップボードにコピーしました。"); 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); self.report({'INFO'}, f"アドオン '{bl_info.get('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}),'; self.report({'INFO'}, f"ワイアの色をコピーしました: {context.window_manager.clipboard}"); return {'FINISHED'}
class SFC_OT_SetFixedLocationFromView(Operator):
bl_idname = f"{PREFIX}.set_fixed_location_from_view"; bl_label = "現在のカメラ位置をセット"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props, cam_obj = context.scene.surface_camera_properties, context.scene.surface_camera_properties.camera_obj
if not cam_obj: self.report({'WARNING'}, "操作対象のカメラが選択されていません。"); return {'CANCELLED'}
props.fixed_location = cam_obj.location; self.report({'INFO'}, f"固定位置を {tuple(round(c, 2) for c in cam_obj.location)} に設定しました。"); 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)
self.report({'INFO'}, f"Loaded: {os.path.basename(HDRI_PATHS[self.hdri_index])}")
else: self.report({'ERROR'}, "Invalid HDRI index")
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; box = layout.box(); col = box.column(); col.prop(props, "camera_obj", text="カメラ")
if props.camera_obj: row = col.row(align=True); row.operator(f"{PREFIX}.sync_with_camera", icon='UV_SYNC_SELECT'); row.operator(f"{PREFIX}.unlink_object", icon='X')
else: col.label(text="カメラを選択してください", icon='ERROR'); col.operator(f"{PREFIX}.create_dedicated_camera", text=f"'{DEDICATED_CAMERA_NAME}' を作成/選択", icon='ADD')
col.separator(); box.prop(props, "camera_preset", text="色プリセット"); box.prop(props, "camera_color", text="カラー"); box.operator(f"{PREFIX}.apply_camera_color", text="ビューポート色を適用")
class SFC_PT_PositionPanel(Panel):
bl_label = "2. カメラ位置 (固定)"; bl_idname = PANEL_IDS["POSITION"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["POSITION"]]
def draw(self, context):
layout = self.layout; props = context.scene.surface_camera_properties; 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 = "location"; op.prop_group_name = "camera"
col.prop(props, "fixed_location", text=""); col.operator(f"{PREFIX}.set_fixed_location_from_view", icon='OBJECT_ORIGIN')
class SFC_PT_AimingPanel(Panel):
bl_label = "3. カメラ視線制御"; 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"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout, props = self.layout, context.scene.surface_camera_properties
box_aim = layout.box(); col_aim = box_aim.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_offset = layout.box(); col_offset = box_offset.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_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; 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:
box_display.label(text="3D Viewport only", icon='INFO')
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:
col = layout.column(align=True)
if not world: col.label(text="No World in Scene", icon='ERROR'); col.operator("world.new", text="Create New World")
else: col.label(text="Enable Nodes in World"); col.prop(world, "use_nodes", text="Use 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); col_list.label(text="HDRI Presets:")
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"); mapping_node = find_node(nodes, 'ShaderNodeMapping', 'Mapping')
if mapping_node:
box_transform = box_env.box(); box_transform.label(text="Transform", icon='OBJECT_DATA'); col = box_transform.column(align=True)
for prop_name in ['Location', 'Rotation', 'Scale']:
row = col.row(align=True); split = row.split(factor=0.8, align=True); split.prop(mapping_node.inputs[prop_name], "default_value", text=prop_name)
op = split.operator(f"{PREFIX}.reset_transform", text="", icon='FILE_REFRESH'); op.property_to_reset = prop_name
elif props.background_mode == 'SKY':
box_sky = layout.box(); box_sky.label(text="Sky Texture", icon='WORLD_DATA'); sky_node = find_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture')
if sky_node:
col_sky = box_sky.column(align=True); col_sky.prop(sky_node, "sky_type", text="Sky Type")
if sky_node.sky_type == 'NISHITA':
if hasattr(sky_node, 'sun_elevation'): col_sky.prop(sky_node, "sun_elevation", text="Sun Elevation")
if hasattr(sky_node, 'sun_rotation'): col_sky.prop(sky_node, "sun_rotation", text="Sun Rotation")
if hasattr(sky_node, 'altitude'): col_sky.prop(sky_node, "altitude", text="Altitude")
if hasattr(sky_node, 'air_density'): col_sky.prop(sky_node, "air_density", text="Air Density")
if hasattr(sky_node, 'dust_density'): col_sky.prop(sky_node, "dust_density", text="Dust Density")
if hasattr(sky_node, 'ozone_density'): col_sky.prop(sky_node, "ozone_density", text="Ozone Density")
elif sky_node.sky_type in {'PREETHAM', 'HOSEK_WILKIE'}:
if hasattr(sky_node, 'turbidity'): col_sky.prop(sky_node, "turbidity", text="Turbidity")
if hasattr(sky_node, 'ground_albedo'): col_sky.prop(sky_node, "ground_albedo", text="Ground Albedo")
class SFC_PT_InfoPanel(Panel):
bl_label = "カメラ情報"; bl_idname = PANEL_IDS["INFO"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["INFO"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): layout, props = self.layout, context.scene.surface_camera_properties; col = layout.column(align=True); row = col.row(align=True); row.label(text="焦点距離:"); row.label(text=props.info_focal_length); row = col.row(align=True); row.label(text="水平視野角:"); row.label(text=props.info_horizontal_fov); col.separator(); row = col.row(align=True); row.label(text="カメラ位置:"); row.label(text=props.info_camera_location); row = col.row(align=True); row.label(text="注視点:"); row.label(text=props.info_target_location); row = col.row(align=True); row.label(text="注視点までの距離:"); row.label(text=props.info_distance_to_target); row = col.row(align=True); row.label(text="注視点での横幅:"); row.label(text=props.info_viewable_width); col.separator(); row = col.row(align=True); row.label(text="クリップ範囲:"); row.label(text=props.info_clip_setting); col.separator(); col.prop(props, "info_precision", text="表示桁数"); col.operator(f"{PREFIX}.copy_all_info", text="全情報をコピー", icon='COPY_ID')
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, theme = self.layout, context.scene.theme_grid_properties, bpy.context.preferences.themes[0]; layout.label(text=f"Current: {tuple(round(c, 3) for c in theme.view_3d.grid)}"); layout.operator(f"{PREFIX}.copy_grid_color", text="Copy Grid Color"); layout.separator(); 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, theme = self.layout, context.scene.theme_wire_properties, bpy.context.preferences.themes[0]; layout.label(text=f"Current: {tuple(round(c, 3) for c in theme.view_3d.wire)}"); layout.operator(f"{PREFIX}.copy_wire_color", text="Copy Wire Color"); layout.separator(); 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
for link in ADDON_LINKS:
op = layout.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
op.url = link["url"]
class SFC_PT_NewDocsLinksPanel(Panel):
bl_label = "アドオン管理"; bl_idname = PANEL_IDS["LINKS_NEWDOC"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
def draw(self, context):
layout = self.layout
if not NEW_DOC_LINKS:
layout.label(text="No links available.", icon='INFO')
for link in NEW_DOC_LINKS:
op = layout.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
op.url = link["url"]
class SFC_PT_DocsLinksPanel(Panel):
bl_label = "関連ドキュメント"; bl_idname = PANEL_IDS["LINKS_DOC"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
def draw(self, context):
layout = self.layout
for link in DOC_LINKS:
op = layout.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
op.url = link["url"]
class SFC_PT_SocialLinksPanel(Panel):
bl_label = "ソーシャルリンク"; bl_idname = PANEL_IDS["LINKS_SOCIAL"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
def draw(self, context):
layout = self.layout
for link in SOCIAL_LINKS:
op = layout.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():
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
if source_node.type == 'TEX_SKY': props.background_mode = 'SKY'
else: props.background_mode = '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_WireApplyColor, SFC_OT_WireCopyColor, SFC_OT_ApplyCameraColor,
SFC_OT_CreateDedicatedCamera, SFC_OT_SyncWithCamera, SFC_OT_UnlinkObject, SFC_OT_ResetProperty, SFC_OT_SetFOV,
SFC_OT_CopyAllInfo, SFC_OT_OpenURL, SFC_OT_RemoveAddon, SFC_OT_SetFixedLocationFromView,
ZIONAD_SWT_OT_LoadHdriFromList, ZIONAD_SWT_OT_ResetTransform,
SFC_PT_CameraSetupPanel, SFC_PT_PositionPanel, SFC_PT_AimingPanel, SFC_PT_LensPanel, SFC_PT_CameraDisplayPanel,
ZIONAD_SWT_PT_WorldControlPanel, SFC_PT_InfoPanel, SFC_PT_GridPanel, SFC_PT_WirePanel,
SFC_PT_LinksPanel, SFC_PT_NewDocsLinksPanel, SFC_PT_DocsLinksPanel, SFC_PT_SocialLinksPanel,
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"Error registering class {cls.__name__}: {e}"); unregister(); raise
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 _update_timer and bpy.app.timers.is_registered(reset_update_flag): bpy.app.timers.unregister(reset_update_flag)
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 hasattr(bpy.types.Scene, prop_name):
try: delattr(bpy.types.Scene, prop_name)
except (AttributeError, RuntimeError): pass
for cls in reversed(classes):
if hasattr(bpy.utils, 'unregister_class') and cls in _registered_classes:
try: bpy.utils.unregister_class(cls)
except RuntimeError: pass
_registered_classes.clear()
if __name__ == "__main__":
try: unregister()
except Exception: pass
register()
# ▲▲▲ ここまで ▲▲▲
'''
# ==============================================================================
# Square Torus システムロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_OBJ_NAME = f"[Preview] SqTorus_{PREFIX}"
PREVIEW_GUIDE_NAME = f"[Preview] SqGuide_{PREFIX}"
PREVIEW_MAT_NAME = f"PreviewMat_{PREFIX}"
def cleanup_preview_data():
for name in[PREVIEW_OBJ_NAME, PREVIEW_GUIDE_NAME]:
obj = bpy.data.objects.get(name)
if obj:
mesh = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if mesh and mesh.users == 0:
bpy.data.meshes.remove(mesh)
meshes_to_remove =[m for m in bpy.data.meshes if m.name.startswith(f"PreviewMesh_{PREFIX}")]
for m in meshes_to_remove:
if m.users == 0: bpy.data.meshes.remove(m)
mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
if mat and mat.users == 0: bpy.data.materials.remove(mat)
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if col and len(col.objects) == 0: bpy.data.collections.remove(col)
def cleanup_old_materials(prefix="Mat_UniqueSqTorus", limit=50):
mats = [m for m in bpy.data.materials if m.name.startswith(prefix)]
if len(mats) > limit:
for m in mats[:-limit]:
if m.users == 0: bpy.data.materials.remove(m)
def create_square_guide_bmesh(bm, square_size):
S = square_size / 2.0
v1 = bm.verts.new((S, S, 0))
v2 = bm.verts.new((-S, S, 0))
v3 = bm.verts.new((-S, -S, 0))
v4 = bm.verts.new((S, -S, 0))
bm.verts.ensure_lookup_table()
bm.edges.new((v1, v2))
bm.edges.new((v2, v3))
bm.edges.new((v3, v4))
bm.edges.new((v4, v1))
return bm
def create_square_torus_bmesh(bm, square_size, corner_radius, minor_radius, corner_segments, minor_segments):
square_size = min(max(square_size, 0.01), 10000.0)
minor_radius = min(max(minor_radius, 0.001), square_size)
minor_segments = max(minor_segments, 3)
half_size = square_size / 2.0
actual_corner_radius = min(max(corner_radius, 0.0), half_size)
rings =[]
EPS = 1e-6
if actual_corner_radius < EPS:
L = half_size
corners =[
(mathutils.Vector((L, L, 0)), mathutils.Vector((1, 1, 0)).normalized()),
(mathutils.Vector((-L, L, 0)), mathutils.Vector((-1, 1, 0)).normalized()),
(mathutils.Vector((-L, -L, 0)), mathutils.Vector((-1, -1, 0)).normalized()),
(mathutils.Vector((L, -L, 0)), mathutils.Vector((1, -1, 0)).normalized())
]
scale_xy = 1.0 / math.cos(math.pi / 4)
for p, n in corners:
b = mathutils.Vector((0, 0, 1))
ring =[]
for j in range(minor_segments):
theta = j * 2.0 * math.pi / minor_segments
offset = n * (minor_radius * math.cos(theta) * scale_xy) + b * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
else:
L = half_size - actual_corner_radius
pts =[]
for q in range(4):
cx = L if q in [0, 3] else -L
cy = L if q in [0, 1] else -L
for i in range(corner_segments + 1):
angle = q * (math.pi / 2) + i * (math.pi / 2) / corner_segments
x = cx + actual_corner_radius * math.cos(angle)
y = cy + actual_corner_radius * math.sin(angle)
pts.append((mathutils.Vector((x, y, 0)), mathutils.Vector((math.cos(angle), math.sin(angle), 0))))
unique_pts =[]
for p, n in pts:
if not unique_pts or (unique_pts[-1][0] - p).length > EPS:
unique_pts.append((p, n))
if len(unique_pts) > 1 and (unique_pts[-1][0] - unique_pts[0][0]).length < EPS:
unique_pts.pop()
for p, n in unique_pts:
b = mathutils.Vector((0, 0, 1))
ring =[]
for j in range(minor_segments):
theta = j * 2.0 * math.pi / minor_segments
offset = n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
bm.verts.ensure_lookup_table()
total_rings = len(rings)
if total_rings < 3: return bm
edge_loops = []
for ring in rings:
edges =[]
for j in range(minor_segments):
v1 = ring[j]
v2 = ring[(j + 1) % minor_segments]
edges.append(bm.edges.new((v1, v2)))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(total_rings):
next_i = (i + 1) % total_rings
try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[next_i])
except Exception: pass
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
for f in bm.faces: f.smooth = True
if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
def apply_auto_smooth(mesh):
if bpy.app.version < (4, 1, 0):
try:
if hasattr(mesh, "use_auto_smooth"):
mesh.use_auto_smooth = True
mesh.auto_smooth_angle = math.radians(30)
except AttributeError: pass
def create_unique_material(color, name_prefix="Mat_UniqueSqTorus"):
timestamp = datetime.now().strftime('%M%S%f')[:5]
mat_name = f"{name_prefix}_{timestamp}"
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
if mat.use_nodes:
tree = mat.node_tree
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (0, 0)
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (300, 0)
tree.links.new(bsdf.outputs[0], out.inputs[0])
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]
cleanup_old_materials(name_prefix)
return mat
def get_or_create_preview_material():
mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
if not mat:
mat = bpy.data.materials.new(name=PREVIEW_MAT_NAME)
mat.use_nodes = True
mat.blend_method = 'BLEND'
return mat
def update_preview_material(mat, color):
if mat.use_nodes:
bsdf = None
for node in mat.node_tree.nodes:
if node.type == 'BSDF_PRINCIPLED':
bsdf = node
break
if not bsdf:
mat.node_tree.nodes.clear()
bsdf = mat.node_tree.nodes.new("ShaderNodeBsdfPrincipled")
out = mat.node_tree.nodes.new("ShaderNodeOutputMaterial")
mat.node_tree.links.new(bsdf.outputs[0], out.inputs[0])
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]
def get_transform_matrix(props):
rot_matrix = mathutils.Matrix.Identity(4)
if props.torus_plane == 'YZ': rot_matrix = mathutils.Matrix.Rotation(math.radians(90.0), 4, 'Y')
elif props.torus_plane == 'ZX': rot_matrix = mathutils.Matrix.Rotation(math.radians(-90.0), 4, 'X')
user_rot = mathutils.Euler((math.radians(props.torus_rot[0]), math.radians(props.torus_rot[1]), math.radians(props.torus_rot[2])), 'XYZ').to_matrix().to_4x4()
loc_matrix = mathutils.Matrix.Translation(mathutils.Vector(props.torus_loc))
return loc_matrix @ user_rot @ rot_matrix
def update_preview_geometry(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col: col = bpy.data.collections.new(PREVIEW_COL_NAME)
if col.name not in context.scene.collection.children: context.scene.collection.children.link(col)
obj = bpy.data.objects.get(PREVIEW_OBJ_NAME)
guide_obj = bpy.data.objects.get(PREVIEW_GUIDE_NAME)
if not props.show_preview:
if obj: bpy.data.objects.remove(obj, do_unlink=True)
if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)
return
final_matrix = get_transform_matrix(props)
scene_mesh_name = f"PreviewMesh_{PREFIX}_{context.scene.name}"
bm = bmesh.new()
try:
create_square_torus_bmesh(bm, square_size=props.square_size, corner_radius=props.corner_radius, minor_radius=props.minor_radius, corner_segments=props.corner_segments, minor_segments=props.minor_segments)
bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
mesh = bpy.data.meshes.get(scene_mesh_name)
if not mesh: mesh = bpy.data.meshes.new(scene_mesh_name)
else: mesh.clear_geometry()
bm.to_mesh(mesh)
apply_auto_smooth(mesh)
mesh.update()
finally: bm.free()
if not obj:
obj = bpy.data.objects.new(PREVIEW_OBJ_NAME, mesh)
col.objects.link(obj)
elif obj.data != mesh: obj.data = mesh
mat = get_or_create_preview_material()
update_preview_material(mat, props.torus_color)
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
if props.show_square_guide:
bm_g = bmesh.new()
try:
create_square_guide_bmesh(bm_g, props.square_size)
bmesh.ops.transform(bm_g, matrix=final_matrix, verts=bm_g.verts)
guide_mesh_name = scene_mesh_name + "_Guide"
mesh_g = bpy.data.meshes.get(guide_mesh_name)
if not mesh_g: mesh_g = bpy.data.meshes.new(guide_mesh_name)
else: mesh_g.clear_geometry()
bm_g.to_mesh(mesh_g)
mesh_g.update()
finally: bm_g.free()
if not guide_obj:
guide_obj = bpy.data.objects.new(PREVIEW_GUIDE_NAME, mesh_g)
col.objects.link(guide_obj)
elif guide_obj.data != mesh_g: guide_obj.data = mesh_g
guide_obj.display_type = 'WIRE'
guide_obj.show_in_front = True
else:
if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)
_timer = None
_last_update_time = 0
def delayed_update():
global _timer, _last_update_time
_timer = None
now = time.time()
if now - _last_update_time < 0.05:
if _timer is None: _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
return None
_last_update_time = now
ctx = bpy.context
if not ctx or not ctx.scene: return None
if ctx.object and ctx.object.mode != 'OBJECT': return None
update_preview_geometry(ctx)
return None
def on_update(self, context):
global _timer
if _timer is None: _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_TorusProps(PropertyGroup):
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
show_square_guide: BoolProperty(name="Show Square Guide", default=CURRENT_DEFAULTS['show_square_guide'], update=on_update)
torus_color: FloatVectorProperty(name="Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['torus_color'], update=on_update)
torus_plane: EnumProperty(name="Plane", items=[('XY', "XY Plane", ""), ('YZ', "YZ Plane", ""), ('ZX', "ZX Plane", "")], default=CURRENT_DEFAULTS['torus_plane'], update=on_update)
torus_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['torus_loc'], update=on_update)
torus_rot: FloatVectorProperty(name="Rotation (Deg)", size=3, default=CURRENT_DEFAULTS['torus_rot'], update=on_update)
square_size: FloatProperty(name="Square Size", default=CURRENT_DEFAULTS['square_size'], min=0.1, max=10000.0, update=on_update)
corner_radius: FloatProperty(name="Corner Radius", default=CURRENT_DEFAULTS['corner_radius'], min=0.0, max=5000.0, update=on_update)
minor_radius: FloatProperty(name="Tube Thickness", default=CURRENT_DEFAULTS['minor_radius'], min=0.01, max=5000.0, update=on_update)
corner_segments: IntProperty(name="Corner Segs", default=CURRENT_DEFAULTS['corner_segments'], min=1, soft_max=128, update=on_update)
minor_segments: IntProperty(name="Tube Segs", default=CURRENT_DEFAULTS['minor_segments'], min=3, soft_max=128, update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CreateTorus(Operator):
bl_idname = f"{OP_PREFIX}.create_torus"
bl_label = "Create Square Torus"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
bm = bmesh.new()
create_square_torus_bmesh(bm, square_size=props.square_size, corner_radius=props.corner_radius, minor_radius=props.minor_radius, corner_segments=props.corner_segments, minor_segments=props.minor_segments)
final_matrix = get_transform_matrix(props)
bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
mesh = bpy.data.meshes.new(f"SquareTorus_Mesh")
bm.to_mesh(mesh)
bm.free()
apply_auto_smooth(mesh)
obj = bpy.data.objects.new(f"SqTorus_{datetime.now().strftime('%H%M%S')}", mesh)
if context.collection: context.collection.objects.link(obj)
else: context.scene.collection.objects.link(obj)
unique_mat = create_unique_material(props.torus_color, "Mat_UniqueSqTorus")
obj.data.materials.append(unique_mat)
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
context.view_layer.objects.active = obj
self.report({'INFO'}, "Created Topology-Perfect Square Torus!")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string(): target_text = t; break
if not target_text:
self.report({'WARNING'}, "Source script not found in Text Editor.")
return {'CANCELLED'}
code = target_text.as_string()
c, l, r = props.torus_color, props.torus_loc, props.torus_rot
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "show_square_guide": {props.show_square_guide},\n'
new_dict += f' "torus_color": ({c[0]:.4f}, {c[1]:.4f}, {c[2]:.4f}, {c[3]:.4f}),\n'
new_dict += f' "torus_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
new_dict += f' "torus_rot": ({r[0]:.4f}, {r[1]:.4f}, {r[2]:.4f}),\n'
new_dict += f' "square_size": {props.square_size:.4f},\n'
new_dict += f' "corner_radius": {props.corner_radius:.4f},\n'
new_dict += f' "minor_radius": {props.minor_radius:.4f},\n'
new_dict += f' "corner_segments": {props.corner_segments},\n'
new_dict += f' "minor_segments": {props.minor_segments},\n'
new_dict += f' "torus_plane": "{props.torus_plane}",\n'
new_dict += "}\n"
try:
tag_start = "# <BEGIN" + "_DICT>"
tag_end = "# <END" + "_DICT>"
if tag_start not in code or tag_end not in code:
self.report({'ERROR'}, "DICT tags missing! Script might be corrupted.")
return {'CANCELLED'}
pre_code, rest = code.split(tag_start, 1)
_, post_code = rest.split(tag_end, 1)
final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
if SOURCE_ID_TAG not in final_code:
self.report({'ERROR'}, "Critical Error: SOURCE_ID_TAG lost during copy.")
return {'CANCELLED'}
lines = final_code.split("\n")
if len(lines) > 0 and lines[0].startswith("# Copied:"):
time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
lines[0] = f"# Copied: {time_str}"
final_code = "\n".join(lines)
context.window_manager.clipboard = final_code
self.report({'INFO'}, "Code copied with absolute safety!")
except Exception as e:
self.report({'ERROR'}, f"Copy failed: {e}")
return {'CANCELLED'}
return {'FINISHED'}
class OT_Reset(Operator):
bl_idname = f"{OP_PREFIX}.reset"
bl_label = "Reset Transform"
def execute(self, context):
p = getattr(context.scene, PROPS_NAME)
p.torus_loc, p.torus_rot, p.torus_plane = (0,0,0), (0,0,0), 'XY'
p.square_size, p.corner_radius, p.minor_radius = 10.0, 0.0, 0.5
return {'FINISHED'}
class OT_OpenUrl(Operator):
bl_idname = f"{OP_PREFIX}.open_url"; bl_label = "Open URL"; url: StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class OT_RemoveAddon(Operator):
bl_idname = f"{OP_PREFIX}.remove_addon"; bl_label = "Remove Addon"
def execute(self, context):
bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
return {'FINISHED'}
# ==============================================================================
# 追加スクリプト書き出しオペレーター
# ==============================================================================
class OT_AddZukkeiScript(Operator):
bl_idname = f"{OP_PREFIX}.add_zukkei_script"
bl_label = "Load Zukkei Script"
bl_description = "図形&配列ジェネレータースクリプトをテキストエディターに読み込みます"
def execute(self, context):
text_name = "B5200_Zukkei_Array_View_20260319.py"
if text_name in bpy.data.texts: text_data = bpy.data.texts[text_name]; text_data.clear()
else: text_data = bpy.data.texts.new(name=text_name)
text_data.write(ZUKKEI_SCRIPT_CONTENT)
found_editor = False
for area in context.screen.areas:
if area.type == 'TEXT_EDITOR': area.spaces.active.text = text_data; found_editor = True; break
if found_editor: self.report({'INFO'}, f"テキストエディターに '{text_name}' を読み込みました!")
else: self.report({'INFO'}, f"'{text_name}' を作成しました。Text Editor を開いて確認してください。")
return {'FINISHED'}
class OT_AddViewportScript(Operator):
bl_idname = f"{OP_PREFIX}.add_viewport_script"
bl_label = "Load Viewport & Sun Script"
bl_description = "3D Viewport Color & Sun スクリプトをテキストエディターに読み込みます"
def execute(self, context):
text_name = "Viewport_Color_Sun_20260316.py"
if text_name in bpy.data.texts: text_data = bpy.data.texts[text_name]; text_data.clear()
else: text_data = bpy.data.texts.new(name=text_name)
text_data.write(VIEWPORT_SCRIPT_CONTENT)
found_editor = False
for area in context.screen.areas:
if area.type == 'TEXT_EDITOR': area.spaces.active.text = text_data; found_editor = True; break
if found_editor: self.report({'INFO'}, f"テキストエディターに '{text_name}' を読み込みました!")
else: self.report({'INFO'}, f"'{text_name}' を作成しました。")
return {'FINISHED'}
class OT_AddCameraScript(Operator):
bl_idname = f"{OP_PREFIX}.add_camera_script"
bl_label = "Load Fixed Camera Script"
bl_description = "Fixed Camera & World スクリプトをテキストエディターに読み込みます"
def execute(self, context):
text_name = "Fixed_Camera_World_2026.py"
if text_name in bpy.data.texts: text_data = bpy.data.texts[text_name]; text_data.clear()
else: text_data = bpy.data.texts.new(name=text_name)
text_data.write(CAMERA_SCRIPT_CONTENT)
found_editor = False
for area in context.screen.areas:
if area.type == 'TEXT_EDITOR': area.spaces.active.text = text_data; found_editor = True; break
if found_editor: self.report({'INFO'}, f"テキストエディターに '{text_name}' を読み込みました!")
else: self.report({'INFO'}, f"'{text_name}' を作成しました。")
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_MainPanel(Panel):
bl_label = PANEL_TITLE
bl_idname = f"{PREFIX}_PT_main"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: layout.label(text="Reload Script"); return
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
box = layout.box()
if not props.show_preview: box.label(text="Preview is Hidden", icon='INFO')
box.prop(props, "torus_color")
col = box.column(align=True)
col.prop(props, "torus_plane")
col.prop(props, "torus_loc")
col.prop(props, "torus_rot")
box.separator()
box.prop(props, "show_square_guide", icon='MESH_PLANE')
col_s = box.column(align=True)
col_s.prop(props, "square_size")
row_cr = col_s.row()
row_cr.prop(props, "corner_radius")
if props.corner_radius <= 0.001: row_cr.label(text="[90° Mode]", icon='SNAP_VERTEX')
col_s.prop(props, "minor_radius")
row_seg = box.row()
row_seg.prop(props, "corner_segments")
row_seg.prop(props, "minor_segments")
box.operator(OT_Reset.bl_idname, icon='LOOP_BACK')
layout.separator()
col_exec = layout.column()
col_exec.scale_y = 1.5
col_exec.operator(OT_CreateTorus.bl_idname, icon='MESH_TORUS', text="Create Square Torus")
class PT_LinksPanel(Panel):
bl_label = "Links"; bl_idname = f"{PREFIX}_PT_links"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
for l in ADDON_LINKS: self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"]).url = l["url"]
class PT_RemovePanel(Panel):
bl_label = "System"; bl_idname = f"{PREFIX}_PT_remove"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): self.layout.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Remove Addon")
class PT_ScriptPanel(Panel):
bl_label = "Additional Scripts"
bl_idname = f"{PREFIX}_PT_script"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
layout.label(text="B5200 図形ジェネレーターを追加:")
layout.operator(OT_AddZukkeiScript.bl_idname, icon='TEXT', text="Load Zukkei Script")
layout.separator()
layout.label(text="5520 Viewport & Sun を追加:")
layout.operator(OT_AddViewportScript.bl_idname, icon='TEXT', text="Load Viewport Script")
layout.separator()
layout.label(text="v100 Fixed Camera を追加:")
layout.operator(OT_AddCameraScript.bl_idname, icon='TEXT', text="Load Camera Script")
# ==============================================================================
# REGISTER
# ==============================================================================
classes = (
PG_TorusProps,
OT_CreateTorus,
OT_CopyFullScript,
OT_Reset,
OT_OpenUrl,
OT_RemoveAddon,
OT_AddZukkeiScript,
OT_AddViewportScript,
OT_AddCameraScript,
PT_MainPanel,
PT_LinksPanel,
PT_RemovePanel,
PT_ScriptPanel
)
def auto_open_sidebar():
try:
for window in bpy.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':
if not space.show_region_ui: space.show_region_ui = True
except: pass
return None
def register():
for c in classes:
try: bpy.utils.register_class(c)
except ValueError: pass
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_TorusProps))
bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)
def unregister():
global _timer
if _timer is not None:
try: bpy.app.timers.unregister(_timer)
except Exception: pass
_timer = None
cleanup_preview_data()
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except ValueError: pass
if __name__ == "__main__":
register()
# 20260325 エディタに3つ 図形作成 背景色 固定カメラ
import bpy
import bmesh
import webbrowser
import math
import mathutils
import time
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime
# ==============================================================================
# 【 内包する2つ目のスクリプト (B5200 Zukkei & Array) 】
# ==============================================================================
ZUKKEI_SCRIPT_CONTENT = r'''#20260319 合体版
import bpy
import bmesh
import math
import random
import datetime
import webbrowser
from mathutils import Vector, Euler, Matrix
from bpy.props import FloatProperty, FloatVectorProperty, EnumProperty, IntProperty, BoolProperty, StringProperty, PointerProperty
from bpy.types import Operator, Panel, PropertyGroup
# ==============================================================================
# 設定エリア & ID管理
# ==============================================================================
PREFIX = "B5200_Zukkei_Array_View_20260319_v2"
TAB_NAME = " b5200[ 図形作成 ] "
OP_PREFIX = "b200_zukkei"
PROPS_NAME = f"{OP_PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: B5200_ZUKKEI_ARRAY_6_16_0 ###"
# 透視投影 視座関連の定数パラメーター
VIEW_RESET_BTN_TEXT = "Reset View (0, -10, 10)"
VIEW_POS_INIT = (0.0, -10.0, 10.0)
bl_info = {
"name": f"zionad b5200 Zukkei & Array & View {PREFIX}",
"author": "zionadchat",
"version": (6, 17, 0),
"blender": (5, 0, 0),
"location": "View3D > Sidebar",
"description": "視座コントロール & リアルタイムプレビュー対応 実体化切り離し機能付き図形・配列ジェネレーター",
"category": "3D View",
}
# ==============================================================================
# リンク集
# ==============================================================================
THIS_LINKS =[
{"label": "b5200 図形作成 20260317版", "url": "<https://www.notion.so/b5200-20260317-326f5dacaf4380b4ad6afb3fe0f9e619>"},
{"label": "b200 図形作成 20250721", "url": "<https://memo2017.hatenablog.com/entry/2025/07/21/115312>"},
{"label": "カメラ 固定 Git 管理 20250711", "url": "<https://memo2017.hatenablog.com/entry/2025/07/11/131157>"},
]
NEW_DOC_LINKS =[
{"label": "blender アドオン 公開", "url": "<https://ivory-handsaw-95b.notion.site/blender-230b3deba7a280d7b610e0e3cdc178da>"},
{"label": "完成品 目次", "url": "<https://mokuji000zionad.hatenablog.com/entry/2025/05/30/135936>"},
]
DOC_LINKS =[
{"label": "812 地球儀 経度 緯度でのコントロール 20250302", "url": "<https://sortphotos2025.hatenablog.jp/entry/2025/03/02/211757>"},
{"label": "アドオン目次 from 20250227", "url": "<https://sortphotos2025.hatenablog.jp/entry/2025/02/27/201251>"},
]
SOCIAL_LINKS =[
{"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
{"label": "Posfie zionad2022", "url": "<https://posfie.com/t/zionad2022>"},
{"label": "X (Twitter) zionadchat", "url": "<https://x.com/zionadchat>"},
]
# ==============================================================================
# デフォルト値設定 (コピー機能で書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"slider_limit": 300.0000,
"view_pos": (0.0000, -10.0000, 10.0000),
"show_preview": True,
"collection_name": "MyCollection",
"sub_collection_name": "Generated_Shapes",
"object_name_prefix": "Shape",
"primitive_type": "CUBE",
"use_solidify": False,
"solidify_thickness": 0.1000,
"cube_size": 2.0000,
"cuboid_dimensions": (2.0000, 2.0000, 2.0000),
"plane_size_x": 2.0000,
"plane_size_y": 2.0000,
"circle_radius": 1.0000,
"circle_vertices": 32,
"ellipse_radius_x": 1.0000,
"ellipse_radius_y": 0.5000,
"ellipse_vertices": 64,
"uv_sphere_radius": 1.0000,
"uv_sphere_segments": 32,
"uv_sphere_rings": 16,
"ico_sphere_radius": 1.0000,
"ico_sphere_subdivisions": 2,
"cylinder_radius": 1.0000,
"cylinder_mode": "CENTER",
"cylinder_depth": 2.0000,
"cylinder_point_top": (0.0000, 0.0000, 2.0000),
"cylinder_point_bottom": (0.0000, 0.0000, 0.0000),
"cylinder_vertices": 32,
"cylinder_cap_top": False,
"cylinder_cap_bottom": False,
"frustum_mode": "CENTER",
"frustum_radius_top": 0.0000,
"frustum_radius_bottom": 1.0000,
"frustum_height": 2.0000,
"frustum_vertices": 32,
"frustum_cap_top": False,
"frustum_cap_bottom": False,
"frustum_point_top": (0.0000, 0.0000, 2.0000),
"frustum_point_bottom": (0.0000, 0.0000, 0.0000),
"image_cylinder_radius": 30.0000,
"image_cylinder_depth": 60.0000,
"image_cylinder_vertices": 64,
"image_cylinder_uv_offset": (0.0000, 0.0000),
"image_cylinder_uv_scale": (1.0000, 1.0000),
"image_cylinder_alpha_outer": 0.0000,
"image_cylinder_alpha_inner": 1.0000,
"cube_frame_size": 2.0000,
"cube_frame_radius": 0.0500,
"cube_frame_vertices": 16,
"cuboid_frame_dimensions": (2.0000, 2.0000, 2.0000),
"cuboid_frame_radius": 0.0500,
"cuboid_frame_vertices": 16,
"torus_major_radius": 1.0000,
"torus_minor_radius": 0.2500,
"torus_major_segments": 48,
"torus_minor_segments": 12,
"monkey_size": 1.0000,
"sphere_spacing": 0.5000,
"sphere_elem_radius": 0.1000,
"sphere_size": 4.0000,
"sphere_radius_val": 2.0000,
"sphere_segments": 12,
"sphere_rings": 6,
"torus_count": 5,
"torus_spacing": 1.0000,
"torus_minor_radius_arr": 0.1000,
"grid_count_x": 5,
"grid_count_y": 5,
"grid_count_z": 5,
"grid_spacing_x": 1.0000,
"grid_spacing_y": 1.0000,
"grid_spacing_z": 1.0000,
"grid_radius": 0.0500,
"grid_vertices": 16,
"location": (0.0000, 0.0000, 0.0000),
"scale_uniform": True,
"scale_factor": 1.0000,
"scale_vector": (1.0000, 1.0000, 1.0000),
"additional_rotation_x": 0.0000,
"additional_rotation_y": 0.0000,
"additional_rotation_z": 0.0000,
"color_mode": "PRESET",
"preset_set": "A",
"preset_color": "1",
"custom_color": (0.8000, 0.8000, 0.8000, 1.0000),
"face_alpha": 1.0000,
"preset_color_1": (0.9000, 0.2000, 0.2000, 1.0000),
"preset_color_2": (1.0000, 0.5000, 0.1000, 1.0000),
"preset_color_3": (1.0000, 0.8000, 0.0000, 1.0000),
"preset_color_4": (0.4000, 0.8000, 0.2000, 1.0000),
"preset_color_5": (0.1000, 0.7000, 0.8000, 1.0000),
"preset_color_6": (0.2000, 0.4000, 0.9000, 1.0000),
"preset_color_7": (0.6000, 0.3000, 0.8000, 1.0000),
"preset_color_8": (1.0000, 0.9000, 0.6000, 1.0000),
"preset_color_9": (0.1000, 0.5000, 0.4000, 1.0000),
"preset_color_10": (0.7000, 0.4000, 0.2000, 1.0000),
"preset_color_b_1": (0.9000, 0.1000, 0.1000, 1.0000),
"preset_color_b_2": (1.0000, 0.5000, 0.2000, 1.0000),
"preset_color_b_3": (0.9000, 0.9000, 0.1000, 1.0000),
"preset_color_b_4": (0.4000, 0.9000, 0.1000, 1.0000),
"preset_color_b_5": (0.1000, 0.9000, 0.9000, 1.0000),
"preset_color_b_6": (0.1000, 0.1000, 0.9000, 1.0000),
"preset_color_b_7": (0.6000, 0.1000, 0.9000, 1.0000),
"preset_color_b_8": (0.9500, 0.9500, 0.8000, 1.0000),
"preset_color_b_9": (0.1000, 0.6000, 0.6000, 1.0000),
"preset_color_b_10": (0.2500, 0.5000, 0.1500, 1.0000),
"preset_color_c_1": (0.6000, 1.0000, 0.6000, 1.0000),
"preset_color_c_2": (0.3000, 0.9000, 0.4000, 1.0000),
"preset_color_c_3": (0.1000, 0.7000, 0.2000, 1.0000),
"preset_color_c_4": (0.0000, 0.5000, 0.1000, 1.0000),
"preset_color_c_5": (0.5000, 0.8000, 0.2000, 1.0000),
"preset_color_c_6": (0.2000, 0.6000, 0.5000, 1.0000),
"preset_color_c_7": (0.7000, 0.9000, 0.3000, 1.0000),
"preset_color_c_8": (0.4000, 0.7000, 0.7000, 1.0000),
"preset_color_c_9": (0.0000, 0.8000, 0.6000, 1.0000),
"preset_color_c_10": (0.1000, 0.3000, 0.1000, 1.0000),
"show_main_docs": True,
"show_new_docs": True,
"show_old_docs": False,
"show_social": False,
}
# <END_DICT>
# ==============================================================================
# 透視投影 ビュー同期ロジック
# ==============================================================================
_is_updating_view = False
def update_view_position(self, context):
global _is_updating_view
if _is_updating_view: return
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
limit = props.slider_limit
v = list(props.view_pos)
clamped = False
for i in range(3):
if v[i] > limit: v[i] = limit; clamped = True
elif v[i] < -limit: v[i] = -limit; clamped = True
if clamped:
_is_updating_view = True
props.view_pos = v
_is_updating_view = False
_is_updating_view = True
try:
cam_pos = Vector(props.view_pos)
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':
r3d = space.region_3d
r3d.view_perspective = 'PERSP'
target_pos = Vector(r3d.view_location)
rel_pos = cam_pos - target_pos
dist = rel_pos.length
if dist > 0.001:
r3d.view_distance = dist
r3d.view_rotation = rel_pos.to_track_quat('Z', 'Y')
finally:
_is_updating_view = False
def view_sync_timer():
global _is_updating_view
if _is_updating_view: return 0.05
context = bpy.context
if getattr(context, "scene", None) is None: return 0.05
props = getattr(context.scene, PROPS_NAME, None)
if not props: return 0.05
r3d = None
target_area = None
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':
r3d = space.region_3d
target_area = area
break
if r3d: break
if r3d: break
if r3d and target_area:
target_pos = Vector(r3d.view_location)
actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
current_pos = Vector(props.view_pos)
if (current_pos - actual_cam_pos).length > 0.001:
_is_updating_view = True
max_val = max(abs(actual_cam_pos.x), abs(actual_cam_pos.y), abs(actual_cam_pos.z))
if max_val > props.slider_limit:
props.slider_limit = max_val + 50.0
props.view_pos = actual_cam_pos
_is_updating_view = False
target_area.tag_redraw()
return 0.05
# ==============================================================================
# コアロジック (図形メッシュ生成 & プレビュー管理)
# ==============================================================================
PREVIEW_COL_NAME = f"{OP_PREFIX}_Preview_Zone"
PREVIEW_TAG = f"{OP_PREFIX}_preview_tag"
_timer = None
def delayed_update():
global _timer
_timer = None
if bpy.context and bpy.context.scene:
update_preview_geometry(bpy.context)
return None
def on_update(self, context):
global _timer
if _timer:
try: bpy.app.timers.unregister(_timer)
except: pass
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
def post_creation_cap_handling(obj, cap_top, cap_bottom, direction=None):
if cap_top and cap_bottom: return
if not obj or obj.type != 'MESH': return
if not cap_top and not cap_bottom: return
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='DESELECT')
bm = bmesh.from_edit_mesh(obj.data)
bm.faces.ensure_lookup_table()
top_face, bottom_face = None, None
max_proj, min_proj = -float('inf'), float('inf')
world_matrix = obj.matrix_world
if direction is None:
direction = Vector((0, 0, 1))
else:
direction = direction.normalized()
for face in bm.faces:
face_center_world = world_matrix @ face.calc_center_median()
proj = face_center_world.dot(direction)
if proj > max_proj:
max_proj = proj
top_face = face
if proj < min_proj:
min_proj = proj
bottom_face = face
faces_to_delete =[]
if not cap_top and top_face: faces_to_delete.append(top_face)
if not cap_bottom and bottom_face: faces_to_delete.append(bottom_face)
if faces_to_delete:
bmesh.ops.delete(bm, geom=faces_to_delete, context='FACES')
bmesh.update_edit_mesh(obj.data)
bpy.ops.object.mode_set(mode='OBJECT')
def setup_sphere_instances_node_tree(obj, props):
"""ポイント上に球体を爆速でインスタンス化するジオメトリノードを構築"""
modifier_name = "Array_GeoNodes"
mod = obj.modifiers.get(modifier_name)
if not mod:
mod = obj.modifiers.new(name=modifier_name, type='NODES')
node_tree = bpy.data.node_groups.new(name="SphereArray_Nodes", type='GeometryNodeTree')
mod.node_group = node_tree
# 5.0対応 Group Input/Output
node_in = node_tree.nodes.new('NodeGroupInput')
node_tree.interface.new_socket(name="Geometry", in_out='INPUT', socket_type='NodeSocketGeometry')
node_out = node_tree.nodes.new('NodeGroupOutput')
node_tree.interface.new_socket(name="Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry')
node_inst = node_tree.nodes.new('GeometryNodeInstanceOnPoints')
node_sphere = node_tree.nodes.new('GeometryNodeMeshUVSphere')
node_sphere.inputs['Radius'].default_value = props.sphere_elem_radius
node_sphere.inputs['Segments'].default_value = props.sphere_segments
node_sphere.inputs['Rings'].default_value = props.sphere_rings
node_realize = node_tree.nodes.new('GeometryNodeRealizeInstances')
node_set_mat = node_tree.nodes.new('GeometryNodeSetMaterial')
# リンク接続
node_tree.links.new(node_in.outputs['Geometry'], node_inst.inputs['Points'])
node_tree.links.new(node_sphere.outputs['Mesh'], node_inst.inputs['Instance'])
node_tree.links.new(node_inst.outputs['Instances'], node_realize.inputs['Geometry'])
node_tree.links.new(node_realize.outputs['Geometry'], node_set_mat.inputs['Geometry'])
node_tree.links.new(node_set_mat.outputs['Geometry'], node_out.inputs['Geometry'])
def create_primitive_object(context, props, name):
old_objs = set(bpy.data.objects)
prim_type = props.primitive_type
use_panel_transform = True
win = context.window_manager.windows[0]
area = next((a for a in win.screen.areas if a.type == 'VIEW_3D'), None)
region = next((r for r in area.regions if r.type == 'WINDOW'), None) if area else None
try:
with context.temp_override(window=win, area=area, region=region):
if prim_type == 'CUBE':
bpy.ops.mesh.primitive_cube_add(size=props.cube_size, align='WORLD', location=(0, 0, 0))
elif prim_type == 'CUBOID':
bpy.ops.mesh.primitive_cube_add(size=1.0, align='WORLD', location=(0, 0, 0))
context.active_object.dimensions = props.cuboid_dimensions
elif prim_type == 'PLANE':
bpy.ops.mesh.primitive_plane_add(size=1.0, align='WORLD', location=(0, 0, 0))
context.active_object.dimensions = (props.plane_size_x, props.plane_size_y, 0)
elif prim_type == 'CIRCLE':
bpy.ops.mesh.primitive_circle_add(vertices=props.circle_vertices, radius=props.circle_radius, fill_type='NGON', align='WORLD', location=(0, 0, 0))
elif prim_type == 'ELLIPSE':
bpy.ops.mesh.primitive_circle_add(vertices=props.ellipse_vertices, radius=1.0, fill_type='NGON', align='WORLD', location=(0, 0, 0))
context.active_object.scale.x = props.ellipse_radius_x
context.active_object.scale.y = props.ellipse_radius_y
elif prim_type == 'UV_SPHERE':
bpy.ops.mesh.primitive_uv_sphere_add(radius=props.uv_sphere_radius, segments=props.uv_sphere_segments, ring_count=props.uv_sphere_rings, align='WORLD', location=(0, 0, 0))
elif prim_type == 'ICO_SPHERE':
bpy.ops.mesh.primitive_ico_sphere_add(radius=props.ico_sphere_radius, subdivisions=props.ico_sphere_subdivisions, align='WORLD', location=(0, 0, 0))
elif prim_type == 'CYLINDER':
cap_ends = props.cylinder_cap_top or props.cylinder_cap_bottom
direction = Vector((0, 0, 1))
if props.cylinder_mode == 'CENTER':
bpy.ops.mesh.primitive_cylinder_add(vertices=props.cylinder_vertices, radius=props.cylinder_radius, depth=props.cylinder_depth, end_fill_type='NGON' if cap_ends else 'NOTHING', align='WORLD', location=(0, 0, 0))
elif props.cylinder_mode == 'POINTS_NORMAL':
use_panel_transform = False
p_top, p_bottom = Vector(props.cylinder_point_top), Vector(props.cylinder_point_bottom)
direction = p_top - p_bottom
height = direction.length
if height < 1e-4: return None
bpy.ops.mesh.primitive_cylinder_add(vertices=props.cylinder_vertices, radius=props.cylinder_radius, depth=height, end_fill_type='NGON' if cap_ends else 'NOTHING', align='WORLD', location=(0, 0, 0))
obj = context.active_object
obj.location = (p_top + p_bottom) / 2
obj.rotation_euler = direction.to_track_quat('Z', 'Y').to_euler('XYZ')
elif props.cylinder_mode == 'POINTS_HORIZONTAL':
use_panel_transform = False
p_top, p_bottom = Vector(props.cylinder_point_top), Vector(props.cylinder_point_bottom)
direction = p_top - p_bottom
dz = direction.z
if abs(dz) < 1e-4: return None
height = abs(dz)
bpy.ops.mesh.primitive_cylinder_add(vertices=props.cylinder_vertices, radius=props.cylinder_radius, depth=height, end_fill_type='NGON' if cap_ends else 'NOTHING', align='WORLD', location=(0, 0, 0))
obj = context.active_object
dx, dy = direction.x, direction.y
sign_z = 1 if dz > 0 else -1
bpy.ops.object.mode_set(mode='EDIT')
bm = bmesh.from_edit_mesh(obj.data)
for v in bm.verts:
factor = v.co.z / height
v.co.x += factor * dx * sign_z
v.co.y += factor * dy * sign_z
bmesh.update_edit_mesh(obj.data)
bpy.ops.object.mode_set(mode='OBJECT')
obj.location = (p_top + p_bottom) / 2
post_creation_cap_handling(context.active_object, props.cylinder_cap_top, props.cylinder_cap_bottom, direction)
elif prim_type == 'FRUSTUM':
cap_ends = props.frustum_cap_top or props.frustum_cap_bottom
direction = Vector((0, 0, 1))
radius_bottom, radius_top = props.frustum_radius_bottom, props.frustum_radius_top
if props.frustum_mode == 'CENTER':
bpy.ops.mesh.primitive_cone_add(vertices=props.frustum_vertices, radius1=radius_bottom, radius2=radius_top, depth=props.frustum_height, end_fill_type='NGON' if cap_ends else 'NOTHING', align='WORLD', location=(0, 0, 0))
elif props.frustum_mode == 'POINTS_NORMAL':
use_panel_transform = False
p_top, p_bottom = Vector(props.frustum_point_top), Vector(props.frustum_point_bottom)
direction = p_top - p_bottom
height = direction.length
if height < 1e-4: return None
bpy.ops.mesh.primitive_cone_add(vertices=props.frustum_vertices, radius1=radius_bottom, radius2=radius_top, depth=height, end_fill_type='NGON' if cap_ends else 'NOTHING', align='WORLD', location=(0, 0, 0))
obj = context.active_object
obj.location = (p_top + p_bottom) / 2
obj.rotation_euler = direction.to_track_quat('Z', 'Y').to_euler('XYZ')
elif props.frustum_mode == 'POINTS_HORIZONTAL':
use_panel_transform = False
p_top, p_bottom = Vector(props.frustum_point_top), Vector(props.frustum_point_bottom)
direction = p_top - p_bottom
dz = direction.z
if abs(dz) < 1e-4: return None
height = abs(dz)
if dz > 0:
rad_bottom, rad_top = radius_bottom, radius_top
else:
rad_bottom, rad_top = radius_top, radius_bottom
bpy.ops.mesh.primitive_cone_add(vertices=props.frustum_vertices, radius1=rad_bottom, radius2=rad_top, depth=height, end_fill_type='NGON' if cap_ends else 'NOTHING', align='WORLD', location=(0, 0, 0))
obj = context.active_object
dx, dy = direction.x, direction.y
sign_z = 1 if dz > 0 else -1
bpy.ops.object.mode_set(mode='EDIT')
bm = bmesh.from_edit_mesh(obj.data)
for v in bm.verts:
factor = v.co.z / height
v.co.x += factor * dx * sign_z
v.co.y += factor * dy * sign_z
bmesh.update_edit_mesh(obj.data)
bpy.ops.object.mode_set(mode='OBJECT')
obj.location = (p_top + p_bottom) / 2
post_creation_cap_handling(context.active_object, props.frustum_cap_top, props.frustum_cap_bottom, direction)
elif prim_type == 'IMAGE_CYLINDER':
bpy.ops.mesh.primitive_cylinder_add(
vertices=props.image_cylinder_vertices,
radius=props.image_cylinder_radius,
depth=props.image_cylinder_depth,
end_fill_type='NOTHING',
align='WORLD',
location=(0, 0, 0)
)
elif prim_type in ('CUBE_FRAME', 'CUBOID_FRAME'):
if prim_type == 'CUBE_FRAME':
s_x = s_y = s_z = props.cube_frame_size / 2.0
r = props.cube_frame_radius
v = props.cube_frame_vertices
else:
s_x = props.cuboid_frame_dimensions[0] / 2.0
s_y = props.cuboid_frame_dimensions[1] / 2.0
s_z = props.cuboid_frame_dimensions[2] / 2.0
r = props.cuboid_frame_radius
v = props.cuboid_frame_vertices
pts =[
Vector((-s_x, -s_y, -s_z)), Vector(( s_x, -s_y, -s_z)), Vector(( s_x, s_y, -s_z)), Vector((-s_x, s_y, -s_z)),
Vector((-s_x, -s_y, s_z)), Vector(( s_x, -s_y, s_z)), Vector(( s_x, s_y, s_z)), Vector((-s_x, s_y, s_z))
]
edges =[
(0,1), (1,2), (2,3), (3,0),
(4,5), (5,6), (6,7), (7,4),
(0,4), (1,5), (2,6), (3,7)
]
mesh = bpy.data.meshes.new("TempBase")
base_obj = bpy.data.objects.new("TempBase", mesh)
context.collection.objects.link(base_obj)
base_obj.location = (0, 0, 0)
parts =[base_obj]
for idx1, idx2 in edges:
p1, p2 = pts[idx1], pts[idx2]
direction = p2 - p1
length = direction.length
bpy.ops.mesh.primitive_cylinder_add(vertices=v, radius=r, depth=length, end_fill_type='NGON', align='WORLD', location=(p1+p2)/2)
obj_part = context.active_object
obj_part.rotation_euler = direction.to_track_quat('Z', 'Y').to_euler('XYZ')
parts.append(obj_part)
if len(parts) > 1:
bpy.ops.object.select_all(action='DESELECT')
for obj_part in parts:
obj_part.select_set(True)
context.view_layer.objects.active = base_obj
bpy.ops.object.join()
elif prim_type == 'TORUS':
bpy.ops.mesh.primitive_torus_add(major_segments=props.torus_major_segments, minor_segments=props.torus_minor_segments, major_radius=props.torus_major_radius, minor_radius=props.torus_minor_radius, align='WORLD', location=(0, 0, 0))
elif prim_type == 'MONKEY':
bpy.ops.mesh.primitive_monkey_add(size=props.monkey_size, align='WORLD', location=(0, 0, 0))
# --- 配列図形 ---
elif prim_type.startswith('SPHERE_'):
locations =[]
spacing = props.sphere_spacing
size = props.sphere_size
radius = props.sphere_radius_val
if prim_type == 'SPHERE_SQUARE_EDGE':
count = max(1, round(size / spacing))
actual_spacing = size / count
half = size / 2.0
for i in range(count):
f = i * actual_spacing
locations.append(Vector((-half + f, -half, 0)))
locations.append(Vector((half, -half + f, 0)))
locations.append(Vector((half - f, half, 0)))
locations.append(Vector((-half, half - f, 0)))
elif prim_type == 'SPHERE_SQUARE_AREA':
count = max(1, round(size / spacing))
actual_spacing = size / count
half = size / 2.0
for i in range(count + 1):
x = -half + i * actual_spacing
for j in range(count + 1):
y = -half + j * actual_spacing
locations.append(Vector((x, y, 0)))
elif prim_type == 'SPHERE_CUBE_EDGE':
count = max(1, round(size / spacing))
actual_spacing = size / count
half = size / 2.0
for i in range(count):
f = i * actual_spacing
locations.extend([
Vector((-half + f, -half, -half)), Vector((half, -half + f, -half)),
Vector((half - f, half, -half)), Vector((-half, half - f, -half)),
Vector((-half + f, -half, half)), Vector((half, -half + f, half)),
Vector((half - f, half, half)), Vector((-half, half - f, half)),
Vector((-half, -half, -half + f)), Vector((half, -half, -half + f)),
Vector((half, half, -half + f)), Vector((-half, half, -half + f))
])
elif prim_type in ('SPHERE_CUBE_SURFACE', 'SPHERE_CUBE_VOLUME'):
count = max(1, round(size / spacing))
actual_spacing = size / count
half = size / 2.0
for i in range(count + 1):
x = -half + i * actual_spacing
for j in range(count + 1):
y = -half + j * actual_spacing
for k in range(count + 1):
z = -half + k * actual_spacing
if prim_type == 'SPHERE_CUBE_SURFACE':
if not (i == 0 or i == count or j == 0 or j == count or k == 0 or k == count):
continue
locations.append(Vector((x, y, z)))
elif prim_type == 'SPHERE_CIRCLE_EDGE':
circ = 2.0 * math.pi * radius
count = max(3, round(circ / spacing))
d_theta = 2.0 * math.pi / count
for i in range(count):
theta = i * d_theta
locations.append(Vector((radius * math.cos(theta), radius * math.sin(theta), 0)))
elif prim_type == 'SPHERE_CIRCLE_AREA': # グリッド状の円内
count = math.ceil(radius / spacing)
for i in range(-count, count + 1):
x = i * spacing
for j in range(-count, count + 1):
y = j * spacing
if x*x + y*y <= radius * radius + 1e-5:
locations.append(Vector((x, y, 0)))
elif prim_type == 'SPHERE_CIRCLE_SPIRAL': # 螺旋渦の円内(ひまわりの種)
area = math.pi * radius**2
count = max(1, int(area / (spacing**2)))
phi = math.pi * (3.0 - math.sqrt(5.0)) # 黄金角
for i in range(count):
r = radius * math.sqrt((i + 0.5) / count)
theta = i * phi
x = r * math.cos(theta)
y = r * math.sin(theta)
locations.append(Vector((x, y, 0.0)))
elif prim_type == 'SPHERE_SPHERE_SURFACE': # フィボナッチ球(表面)
area = 4 * math.pi * radius * radius
N = max(4, int(round(area / (spacing * spacing))))
phi = math.pi * (3.0 - math.sqrt(5.0))
for i in range(N):
z = 1.0 - (i / float(N - 1)) * 2.0
r_xy = math.sqrt(max(0.0, 1.0 - z * z))
theta = phi * i
x = math.cos(theta) * r_xy
y = math.sin(theta) * r_xy
locations.append(Vector((x * radius, y * radius, z * radius)))
elif prim_type == 'SPHERE_SPHERE_VOLUME': # グリッド状の球内
count = math.ceil(radius / spacing)
for i in range(-count, count + 1):
x = i * spacing
for j in range(-count, count + 1):
y = j * spacing
for k in range(-count, count + 1):
z = k * spacing
if x*x + y*y + z*z <= radius * radius + 1e-5:
locations.append(Vector((x, y, z)))
elif prim_type == 'SPHERE_SPHERE_SPIRAL': # 螺旋渦の球内(タマネギ状のフィボナッチ球)
r_count = max(1, int(radius / spacing))
phi = math.pi * (3.0 - math.sqrt(5.0))
locations.append(Vector((0,0,0))) # 中心点
for r_idx in range(1, r_count + 1):
r_current = r_idx * spacing
if r_current > radius: continue
layer_count = max(1, int((4 * math.pi * r_current**2) / (spacing**2)))
for i in range(layer_count):
z = 1.0 - (i / float(layer_count - 1 if layer_count > 1 else 1)) * 2.0
r_xy = math.sqrt(max(0.0, 1.0 - z * z))
theta = i * phi
x = r_xy * math.cos(theta)
y = r_xy * math.sin(theta)
locations.append(Vector((x * r_current, y * r_current, z * r_current)))
# 安全装置 (ノードなので10万個まで許容)
MAX_SPHERES = 100000
if len(locations) > MAX_SPHERES:
print(f"[{PREFIX}] Warning: Too many spheres ({len(locations)}). Limited to {MAX_SPHERES}.")
locations = locations[:MAX_SPHERES]
if locations:
mesh = bpy.data.meshes.new("TempSphereArray")
mesh.from_pydata(locations, [],[])
mesh.update()
obj = bpy.data.objects.new("TempSphereArray", mesh)
context.collection.objects.link(obj)
context.view_layer.objects.active = obj
obj.select_set(True)
setup_sphere_instances_node_tree(obj, props)
elif prim_type == 'TORUS_CONCENTRIC':
mesh = bpy.data.meshes.new("TempConcentricTorus")
base_obj = bpy.data.objects.new("TempConcentricTorus", mesh)
context.collection.objects.link(base_obj)
base_obj.location = (0, 0, 0)
parts =[base_obj]
for i in range(1, props.torus_count + 1):
maj_r = i * props.torus_spacing
bpy.ops.mesh.primitive_torus_add(
major_segments=props.torus_major_segments,
minor_segments=props.torus_minor_segments,
major_radius=maj_r,
minor_radius=props.torus_minor_radius_arr,
align='WORLD', location=(0, 0, 0)
)
parts.append(context.active_object)
if len(parts) > 1:
bpy.ops.object.select_all(action='DESELECT')
for p in parts:
p.select_set(True)
context.view_layer.objects.active = base_obj
bpy.ops.object.join()
elif prim_type == 'CYLINDER_GRID':
bm = bmesh.new()
cx = props.grid_count_x
cy = props.grid_count_y
cz = props.grid_count_z
sx = props.grid_spacing_x
sy = props.grid_spacing_y
sz = props.grid_spacing_z
r = props.grid_radius
v = props.grid_vertices
wx = (cx - 1) * sx
wy = (cy - 1) * sy
wz = (cz - 1) * sz
# X軸に平行な線
if cx > 1:
for j in range(cy):
for k in range(cz):
y = -wy/2.0 + j * sy
z = -wz/2.0 + k * sz
mat = Matrix.Translation((0, y, z)) @ Euler((0, math.radians(90), 0), 'XYZ').to_matrix().to_4x4()
bmesh.ops.create_cone(bm, cap_ends=True, segments=v, radius1=r, radius2=r, depth=wx, matrix=mat)
# Y軸に平行な線
if cy > 1:
for i in range(cx):
for k in range(cz):
x = -wx/2.0 + i * sx
z = -wz/2.0 + k * sz
mat = Matrix.Translation((x, 0, z)) @ Euler((math.radians(90), 0, 0), 'XYZ').to_matrix().to_4x4()
bmesh.ops.create_cone(bm, cap_ends=True, segments=v, radius1=r, radius2=r, depth=wy, matrix=mat)
# Z軸に平行な線
if cz > 1:
for i in range(cx):
for j in range(cy):
x = -wx/2.0 + i * sx
y = -wy/2.0 + j * sy
mat = Matrix.Translation((x, y, 0))
bmesh.ops.create_cone(bm, cap_ends=True, segments=v, radius1=r, radius2=r, depth=wz, matrix=mat)
mesh = bpy.data.meshes.new("TempGrid")
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new("TempGrid", mesh)
context.collection.objects.link(obj)
context.view_layer.objects.active = obj
obj.select_set(True)
except Exception as e:
print(f"Error creating primitive: {e}")
return None
new_objs = set(bpy.data.objects) - old_objs
if not new_objs: return None
obj = list(new_objs)[0]
obj.name = name
if use_panel_transform:
if props.scale_uniform:
scale_vec = Vector((props.scale_factor, props.scale_factor, props.scale_factor))
else:
scale_vec = Vector(props.scale_vector)
obj.scale.x *= scale_vec.x
obj.scale.y *= scale_vec.y
obj.scale.z *= scale_vec.z
obj.location = props.location
obj.rotation_euler.rotate(Euler(map(math.radians, (props.additional_rotation_x, props.additional_rotation_y, props.additional_rotation_z)), 'XYZ'))
if props.use_solidify and props.primitive_type != 'IMAGE_CYLINDER':
mod = obj.modifiers.new(name="Solidify", type='SOLIDIFY')
mod.thickness = props.solidify_thickness
return obj
def apply_primitive_material(obj, props, is_preview=False):
if not obj.data: return
timestamp = datetime.datetime.now().strftime('%M%S%f')[:5]
mat_prefix = "Mat_Prev" if is_preview else "Mat_Entity"
mat = bpy.data.materials.new(name=f"{mat_prefix}_{obj.name}_{timestamp}")
mat.use_nodes = True
obj.data.materials.append(mat)
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if not bsdf: return
if props.primitive_type == 'IMAGE_CYLINDER':
if hasattr(mat, "blend_method"): mat.blend_method = 'BLEND'
if hasattr(mat, "shadow_method"): mat.shadow_method = 'NONE'
tex_node = mat.node_tree.nodes.new('ShaderNodeTexImage')
tex_node.location = (-400, 200)
tex_coord = mat.node_tree.nodes.new('ShaderNodeTexCoord')
tex_coord.location = (-800, 200)
mapping = mat.node_tree.nodes.new('ShaderNodeMapping')
mapping.location = (-600, 200)
mapping.inputs['Location'].default_value[0] = props.image_cylinder_uv_offset[0]
mapping.inputs['Location'].default_value[1] = props.image_cylinder_uv_offset[1]
mapping.inputs['Scale'].default_value[0] = -1.0 * props.image_cylinder_uv_scale[0]
mapping.inputs['Scale'].default_value[1] = props.image_cylinder_uv_scale[1]
mat.node_tree.links.new(tex_coord.outputs['UV'], mapping.inputs['Vector'])
mat.node_tree.links.new(mapping.outputs['Vector'], tex_node.inputs['Vector'])
if props.image_cylinder_image:
tex_node.image = props.image_cylinder_image
mat.node_tree.links.new(tex_node.outputs['Color'], bsdf.inputs['Base Color'])
if 'Emission Color' in bsdf.inputs: mat.node_tree.links.new(tex_node.outputs['Color'], bsdf.inputs['Emission Color'])
elif 'Emission' in bsdf.inputs: mat.node_tree.links.new(tex_node.outputs['Color'], bsdf.inputs['Emission'])
if 'Emission Strength' in bsdf.inputs: bsdf.inputs['Emission Strength'].default_value = 1.0
else:
bsdf.inputs['Base Color'].default_value = (0.2, 0.2, 0.2, 1.0)
geo_node = mat.node_tree.nodes.new('ShaderNodeNewGeometry')
geo_node.location = (-400, -100)
map_node = mat.node_tree.nodes.new('ShaderNodeMapRange')
map_node.location = (-200, -100)
map_node.inputs['To Min'].default_value = props.image_cylinder_alpha_outer
map_node.inputs['To Max'].default_value = props.image_cylinder_alpha_inner
mat.node_tree.links.new(geo_node.outputs['Backfacing'], map_node.inputs['Value'])
mat.node_tree.links.new(map_node.outputs['Result'], bsdf.inputs['Alpha'])
else:
color_to_set = (0.8, 0.8, 0.8, 1.0)
if props.color_mode == 'RANDOM_ALL':
color_to_set = (random.random(), random.random(), random.random(), 1.0)
elif props.color_mode == 'PICKER':
color_to_set = props.custom_color
elif props.color_mode == 'PRESET':
preset_key = f"preset_color_{props.preset_set.lower()}_{props.preset_color}" if props.preset_set != 'A' else f"preset_color_{props.preset_color}"
color_to_set = getattr(props, preset_key)
bsdf.inputs['Base Color'].default_value = color_to_set
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = props.face_alpha
if hasattr(mat, "blend_method"):
mat.blend_method = 'BLEND' if props.face_alpha < 1.0 else 'OPAQUE'
# GeoNodes経由でマテリアルを確実に反映
if props.primitive_type.startswith('SPHERE_'):
mod = obj.modifiers.get("Array_GeoNodes")
if mod and mod.node_group:
for node in mod.node_group.nodes:
if node.type == 'SET_MATERIAL':
node.inputs['Material'].default_value = mat
def update_preview_geometry(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_NAME)
context.scene.collection.children.link(col)
for o in list(col.objects):
if o.get(PREVIEW_TAG):
bpy.data.objects.remove(o, do_unlink=True)
for m in bpy.data.meshes:
if m.users == 0: bpy.data.meshes.remove(m)
for mat in bpy.data.materials:
if mat.users == 0 and mat.name.startswith("Mat_Prev"): bpy.data.materials.remove(mat)
# 配列図形用ノードグループのお掃除
for ng in bpy.data.node_groups:
if ng.users == 0 and ng.name.startswith("SphereArray_Nodes"):
bpy.data.node_groups.remove(ng)
if not props.show_preview: return
name = f"[Preview] {props.primitive_type}"
obj = create_primitive_object(context, props, name)
if not obj: return
for c in obj.users_collection: c.objects.unlink(obj)
col.objects.link(obj)
obj[PREVIEW_TAG] = True
apply_primitive_material(obj, props, is_preview=True)
obj.select_set(False)
def place_object_in_hierarchical_collection(obj, collection_path_names):
if not obj or not collection_path_names: return
target_parent_collection = bpy.context.scene.collection
for name in collection_path_names:
found_child = target_parent_collection.children.get(name)
if not found_child:
found_child = bpy.data.collections.new(name)
target_parent_collection.children.link(found_child)
target_parent_collection = found_child
for coll in obj.users_collection:
coll.objects.unlink(obj)
try:
if obj.name not in target_parent_collection.objects:
target_parent_collection.objects.link(obj)
except Exception as e:
print(f"Error linking object {obj.name}: {e}")
# ==============================================================================
# PROPERTIES
# ==============================================================================
def make_color_update(set_val, idx):
def update_cb(self, context):
if self.color_mode == 'PRESET':
self.preset_set = set_val
self.preset_color = str(idx)
on_update(self, context)
return update_cb
class PG_B200Props(PropertyGroup):
# --- View Control Props ---
slider_limit: FloatProperty(name="Range Limit", default=CURRENT_DEFAULTS.get('slider_limit', 300.0), min=10.0, max=10000.0)
view_pos: FloatVectorProperty(name="View Position", size=3, soft_min=-10000.0, soft_max=10000.0,
default=CURRENT_DEFAULTS.get('view_pos', VIEW_POS_INIT),
update=update_view_position)
# --- Zukkei & Array Props ---
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
collection_name: StringProperty(name="Collection", default=CURRENT_DEFAULTS['collection_name'])
sub_collection_name: StringProperty(name="Sub-Collection", default=CURRENT_DEFAULTS['sub_collection_name'])
object_name_prefix: StringProperty(name="Object Name", default=CURRENT_DEFAULTS['object_name_prefix'])
primitive_type: EnumProperty(
name="Primitive Type",
items=[
('CUBE', "Cube", ""), ('CUBOID', "Cuboid", ""), ('PLANE', "Plane", ""),
('CIRCLE', "Circle", ""), ('ELLIPSE', "Ellipse", ""), ('UV_SPHERE', "UV Sphere", ""),
('ICO_SPHERE', "Ico Sphere", ""), ('CYLINDER', "Cylinder", ""),
('FRUSTUM', "Frustum (Cone)", ""), ('IMAGE_CYLINDER', "Image Cylinder (パノラマ筒)", ""),
('CUBE_FRAME', "Cube Frame (辺を円柱で)", ""),
('CUBOID_FRAME', "Cuboid Frame (辺を円柱で)", ""),
('TORUS', "Torus", ""), ('MONKEY', "Monkey", ""),
# 配列図形の追加分
('SPHERE_SQUARE_EDGE', "Sphere: Square Perimeter (正方形の辺)", ""),
('SPHERE_SQUARE_AREA', "Sphere: Square Area (正方形の面)", ""),
('SPHERE_CUBE_EDGE', "Sphere: Cube Edges (立方体の辺)", ""),
('SPHERE_CUBE_SURFACE', "Sphere: Cube Surface (立方体の表面)", ""),
('SPHERE_CUBE_VOLUME', "Sphere: Cube Volume (立方体の中身)", ""),
('SPHERE_CIRCLE_EDGE', "Sphere: Circle Perimeter (円の円周)", ""),
('SPHERE_CIRCLE_AREA', "Sphere: Circle Area (円の面内グリッド)", ""),
('SPHERE_CIRCLE_SPIRAL', "Sphere: Circle Spiral (円内を螺旋渦で)", ""),
('SPHERE_SPHERE_SURFACE', "Sphere: Sphere Surface (球の表面)", ""),
('SPHERE_SPHERE_VOLUME', "Sphere: Sphere Volume (球体内グリッド)", ""),
('SPHERE_SPHERE_SPIRAL', "Sphere: Sphere Spiral (球内を螺旋渦で)", ""),
('TORUS_CONCENTRIC', "Torus: Concentric (同心円トーラス)", ""),
('CYLINDER_GRID', "Cylinder: Grid 3D (円柱の3D格子)", "")
],
default=CURRENT_DEFAULTS['primitive_type'], update=on_update
)
use_solidify: BoolProperty(name="面に厚みを加える", default=CURRENT_DEFAULTS['use_solidify'], update=on_update)
solidify_thickness: FloatProperty(name="厚み", default=CURRENT_DEFAULTS['solidify_thickness'], update=on_update)
cube_size: FloatProperty(name="Size", default=CURRENT_DEFAULTS['cube_size'], min=0.001, update=on_update)
cuboid_dimensions: FloatVectorProperty(name="Dimensions", default=CURRENT_DEFAULTS['cuboid_dimensions'], min=0.001, subtype='XYZ', size=3, update=on_update)
plane_size_x: FloatProperty(name="Size X", default=CURRENT_DEFAULTS['plane_size_x'], min=0.001, update=on_update)
plane_size_y: FloatProperty(name="Size Y", default=CURRENT_DEFAULTS['plane_size_y'], min=0.001, update=on_update)
circle_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['circle_radius'], min=0.001, update=on_update)
circle_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['circle_vertices'], min=3, update=on_update)
ellipse_radius_x: FloatProperty(name="Radius X", default=CURRENT_DEFAULTS['ellipse_radius_x'], min=0.001, update=on_update)
ellipse_radius_y: FloatProperty(name="Radius Y", default=CURRENT_DEFAULTS['ellipse_radius_y'], min=0.001, update=on_update)
ellipse_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['ellipse_vertices'], min=3, update=on_update)
uv_sphere_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['uv_sphere_radius'], min=0.001, update=on_update)
uv_sphere_segments: IntProperty(name="Segments", default=CURRENT_DEFAULTS['uv_sphere_segments'], min=3, update=on_update)
uv_sphere_rings: IntProperty(name="Rings", default=CURRENT_DEFAULTS['uv_sphere_rings'], min=2, update=on_update)
ico_sphere_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['ico_sphere_radius'], min=0.001, update=on_update)
ico_sphere_subdivisions: IntProperty(name="Subdivisions", default=CURRENT_DEFAULTS['ico_sphere_subdivisions'], min=1, max=10, update=on_update)
CYL_FRUSTUM_MODES =[
('CENTER', "Center, Height", "中心座標と高さを指定して作成"),
('POINTS_NORMAL', "Points (Axis Normal)", "両端指定: 上下面が中心軸に対して直角"),
('POINTS_HORIZONTAL', "Points (Horizontal Caps)", "両端指定: 上下面がXY平面に平行(シアー変形)")
]
cylinder_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['cylinder_radius'], min=0.001, update=on_update)
cylinder_mode: EnumProperty(name="Creation Mode", items=CYL_FRUSTUM_MODES, default=CURRENT_DEFAULTS['cylinder_mode'], update=on_update)
cylinder_depth: FloatProperty(name="Depth", default=CURRENT_DEFAULTS['cylinder_depth'], min=0.001, update=on_update)
cylinder_point_top: FloatVectorProperty(name="Top Point", subtype='TRANSLATION', default=CURRENT_DEFAULTS['cylinder_point_top'], size=3, update=on_update)
cylinder_point_bottom: FloatVectorProperty(name="Bottom Point", subtype='TRANSLATION', default=CURRENT_DEFAULTS['cylinder_point_bottom'], size=3, update=on_update)
cylinder_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['cylinder_vertices'], min=3, update=on_update)
cylinder_cap_top: BoolProperty(name="Fill Top Cap", default=CURRENT_DEFAULTS['cylinder_cap_top'], update=on_update)
cylinder_cap_bottom: BoolProperty(name="Fill Bottom Cap", default=CURRENT_DEFAULTS['cylinder_cap_bottom'], update=on_update)
frustum_mode: EnumProperty(name="Creation Mode", items=CYL_FRUSTUM_MODES, default=CURRENT_DEFAULTS['frustum_mode'], update=on_update)
frustum_radius_top: FloatProperty(name="Top Radius (+Z)", default=CURRENT_DEFAULTS['frustum_radius_top'], min=0.0, update=on_update)
frustum_radius_bottom: FloatProperty(name="Bottom Radius (-Z)", default=CURRENT_DEFAULTS['frustum_radius_bottom'], min=0.001, update=on_update)
frustum_height: FloatProperty(name="Height", default=CURRENT_DEFAULTS['frustum_height'], min=0.001, update=on_update)
frustum_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['frustum_vertices'], min=3, update=on_update)
frustum_cap_top: BoolProperty(name="Fill Top Cap", default=CURRENT_DEFAULTS['frustum_cap_top'], update=on_update)
frustum_cap_bottom: BoolProperty(name="Fill Bottom Cap", default=CURRENT_DEFAULTS['frustum_cap_bottom'], update=on_update)
frustum_point_top: FloatVectorProperty(name="Top Point", subtype='TRANSLATION', default=CURRENT_DEFAULTS['frustum_point_top'], size=3, update=on_update)
frustum_point_bottom: FloatVectorProperty(name="Bottom Point", subtype='TRANSLATION', default=CURRENT_DEFAULTS['frustum_point_bottom'], size=3, update=on_update)
image_cylinder_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['image_cylinder_radius'], min=0.001, update=on_update)
image_cylinder_depth: FloatProperty(name="Height", default=CURRENT_DEFAULTS['image_cylinder_depth'], min=0.001, update=on_update)
image_cylinder_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['image_cylinder_vertices'], min=3, update=on_update)
image_cylinder_image: PointerProperty(type=bpy.types.Image, name="Image", update=on_update)
image_cylinder_uv_offset: FloatVectorProperty(name="UV Offset", size=2, default=CURRENT_DEFAULTS['image_cylinder_uv_offset'], update=on_update)
image_cylinder_uv_scale: FloatVectorProperty(name="UV Scale", size=2, default=CURRENT_DEFAULTS['image_cylinder_uv_scale'], update=on_update)
image_cylinder_alpha_outer: FloatProperty(name="Outer Alpha (外側の透明度)", default=CURRENT_DEFAULTS['image_cylinder_alpha_outer'], min=0.0, max=1.0, update=on_update)
image_cylinder_alpha_inner: FloatProperty(name="Inner Alpha (内側の透明度)", default=CURRENT_DEFAULTS['image_cylinder_alpha_inner'], min=0.0, max=1.0, update=on_update)
cube_frame_size: FloatProperty(name="Size", default=CURRENT_DEFAULTS['cube_frame_size'], min=0.001, update=on_update)
cube_frame_radius: FloatProperty(name="Radius (Thickness)", default=CURRENT_DEFAULTS['cube_frame_radius'], min=0.001, update=on_update)
cube_frame_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['cube_frame_vertices'], min=3, update=on_update)
cuboid_frame_dimensions: FloatVectorProperty(name="Dimensions", default=CURRENT_DEFAULTS['cuboid_frame_dimensions'], min=0.001, subtype='XYZ', size=3, update=on_update)
cuboid_frame_radius: FloatProperty(name="Radius (Thickness)", default=CURRENT_DEFAULTS['cuboid_frame_radius'], min=0.001, update=on_update)
cuboid_frame_vertices: IntProperty(name="Vertices", default=CURRENT_DEFAULTS['cuboid_frame_vertices'], min=3, update=on_update)
torus_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['torus_major_radius'], min=0.001, update=on_update)
torus_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['torus_minor_radius'], min=0.001, update=on_update)
torus_major_segments: IntProperty(name="Major Segments", default=CURRENT_DEFAULTS['torus_major_segments'], min=3, update=on_update)
torus_minor_segments: IntProperty(name="Minor Segments", default=CURRENT_DEFAULTS['torus_minor_segments'], min=3, update=on_update)
monkey_size: FloatProperty(name="Size", default=CURRENT_DEFAULTS['monkey_size'], min=0.001, update=on_update)
# 配列図形用プロパティ
sphere_spacing: FloatProperty(name="間隔", default=CURRENT_DEFAULTS.get('sphere_spacing', 0.5), min=0.01, update=on_update)
sphere_elem_radius: FloatProperty(name="球の半径", default=CURRENT_DEFAULTS.get('sphere_elem_radius', 0.1), min=0.001, update=on_update)
sphere_size: FloatProperty(name="全体サイズ", default=CURRENT_DEFAULTS.get('sphere_size', 4.0), min=0.001, update=on_update)
sphere_radius_val: FloatProperty(name="全体半径", default=CURRENT_DEFAULTS.get('sphere_radius_val', 2.0), min=0.001, update=on_update)
sphere_segments: IntProperty(name="Segments", default=CURRENT_DEFAULTS.get('sphere_segments', 12), min=3, update=on_update)
sphere_rings: IntProperty(name="Rings", default=CURRENT_DEFAULTS.get('sphere_rings', 6), min=2, update=on_update)
torus_count: IntProperty(name="同心円の数", default=CURRENT_DEFAULTS.get('torus_count', 5), min=1, update=on_update)
torus_spacing: FloatProperty(name="間隔", default=CURRENT_DEFAULTS.get('torus_spacing', 1.0), min=0.01, update=on_update)
torus_minor_radius_arr: FloatProperty(name="トーラスの太さ", default=CURRENT_DEFAULTS.get('torus_minor_radius_arr', 0.1), min=0.001, update=on_update)
grid_count_x: IntProperty(name="Count X", default=CURRENT_DEFAULTS.get('grid_count_x', 5), min=1, update=on_update)
grid_count_y: IntProperty(name="Count Y", default=CURRENT_DEFAULTS.get('grid_count_y', 5), min=1, update=on_update)
grid_count_z: IntProperty(name="Count Z", default=CURRENT_DEFAULTS.get('grid_count_z', 5), min=1, update=on_update)
grid_spacing_x: FloatProperty(name="Spacing X", default=CURRENT_DEFAULTS.get('grid_spacing_x', 1.0), min=0.01, update=on_update)
grid_spacing_y: FloatProperty(name="Spacing Y", default=CURRENT_DEFAULTS.get('grid_spacing_y', 1.0), min=0.01, update=on_update)
grid_spacing_z: FloatProperty(name="Spacing Z", default=CURRENT_DEFAULTS.get('grid_spacing_z', 1.0), min=0.01, update=on_update)
grid_radius: FloatProperty(name="円柱の太さ", default=CURRENT_DEFAULTS.get('grid_radius', 0.05), min=0.001, update=on_update)
grid_vertices: IntProperty(name="円柱の頂点数", default=CURRENT_DEFAULTS.get('grid_vertices', 16), min=3, update=on_update)
location: FloatVectorProperty(name="Location", subtype='TRANSLATION', default=CURRENT_DEFAULTS['location'], size=3, update=on_update)
scale_uniform: BoolProperty(name="Uniform Scale", default=CURRENT_DEFAULTS['scale_uniform'], update=on_update)
scale_factor: FloatProperty(name="Scale", default=CURRENT_DEFAULTS['scale_factor'], min=0.001, update=on_update)
scale_vector: FloatVectorProperty(name="Scale", default=CURRENT_DEFAULTS['scale_vector'], min=0.001, size=3, update=on_update)
additional_rotation_x: FloatProperty(name="Rotation X", default=CURRENT_DEFAULTS['additional_rotation_x'], update=on_update)
additional_rotation_y: FloatProperty(name="Rotation Y", default=CURRENT_DEFAULTS['additional_rotation_y'], update=on_update)
additional_rotation_z: FloatProperty(name="Rotation Z", default=CURRENT_DEFAULTS['additional_rotation_z'], update=on_update)
color_mode: EnumProperty(name="Color Mode", items=[('RANDOM_ALL', "Random All", ""), ('PICKER', "Color Picker", ""), ('PRESET', "Preset", "")], default=CURRENT_DEFAULTS['color_mode'], update=on_update)
preset_set: EnumProperty(name="Preset Set", items=[('A', "Preset A", ""), ('B', "Preset B", ""), ('C', "Preset C (Green)", "")], default=CURRENT_DEFAULTS['preset_set'], update=on_update)
preset_color: EnumProperty(name="Preset Color", items=[(str(i), str(i), "") for i in range(1, 11)], default=CURRENT_DEFAULTS['preset_color'], update=on_update)
custom_color: FloatVectorProperty(name="Custom Color", subtype='COLOR', default=CURRENT_DEFAULTS['custom_color'], min=0.0, max=1.0, size=4, update=on_update)
face_alpha: FloatProperty(name="Alpha", default=CURRENT_DEFAULTS['face_alpha'], min=0.0, max=1.0, update=on_update)
show_main_docs: BoolProperty(default=CURRENT_DEFAULTS['show_main_docs'])
show_new_docs: BoolProperty(default=CURRENT_DEFAULTS['show_new_docs'])
show_old_docs: BoolProperty(default=CURRENT_DEFAULTS['show_old_docs'])
show_social: BoolProperty(default=CURRENT_DEFAULTS['show_social'])
for s_key, set_val in[('preset_color', 'A'), ('preset_color_b', 'B'), ('preset_color_c', 'C')]:
for i in range(1, 11):
key = f"{s_key}_{i}"
PG_B200Props.__annotations__[key] = FloatVectorProperty(subtype='COLOR', default=CURRENT_DEFAULTS[key], min=0.0, max=1.0, size=4, update=make_color_update(set_val, i))
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_ViewCenterFront(Operator):
bl_idname = f"{OP_PREFIX}.view_center_front"
bl_label = "Center 0,0,0 (Front View)"
bl_description = "原点(0,0,0)を画面中央に配置し、Yマイナス方向からの視点(正面)にします"
def execute(self, context):
for area in context.screen.areas:
if area.type == 'VIEW_3D':
rv3d = area.spaces.active.region_3d
if rv3d:
rv3d.view_location = (0.0, 0.0, 0.0)
rv3d.view_rotation = Euler((math.radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
if rv3d.view_distance < 10.0:
rv3d.view_distance = 60.0
return {'FINISHED'}
class OT_CreatePrimitive(Operator):
bl_idname = f"{OP_PREFIX}.create_primitive"
bl_label = "実体メッシュを生成 (切り離し)"
bl_description = "現在のパラメータで実体を生成し、アドオン管理から切り離して指定コレクションに配置します"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME)
scene = context.scene
prefix = props.object_name_prefix.strip() or "Object"
final_name = f"{prefix}{scene.b200_object_counter:03d}"
obj = create_primitive_object(context, props, final_name)
if not obj:
self.report({'ERROR'}, "形状の生成に失敗しました。")
return {'CANCELLED'}
scene.b200_object_counter += 1
for c in obj.users_collection:
c.objects.unlink(obj)
collection_path =[n for n in[props.collection_name.strip(), props.sub_collection_name.strip()] if n]
if collection_path:
place_object_in_hierarchical_collection(obj, collection_path)
else:
context.scene.collection.objects.link(obj)
apply_primitive_material(obj, props, is_preview=False)
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
context.view_layer.objects.active = obj
self.report({'INFO'}, f"{props.primitive_type} を生成し、アドオンから切り離しました: {obj.name}")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = next((t for t in bpy.data.texts if SOURCE_ID_TAG in t.as_string()), None)
if not target_text:
self.report({'ERROR'}, "スクリプトのソースが見つかりません。")
return {'CANCELLED'}
def format_val(v):
if isinstance(v, str): return repr(v)
if isinstance(v, bool): return str(v)
if isinstance(v, (int, float)): return f"{v:.4f}" if isinstance(v, float) else str(v)
try:
return "(" + ", ".join(f"{float(x):.4f}" for x in v) + ")"
except: pass
return str(v)
new_dict = "CURRENT_DEFAULTS = {\n"
for k in CURRENT_DEFAULTS.keys():
val = getattr(props, k)
new_dict += f' "{k}": {format_val(val)},\n'
new_dict += "}\n"
code = target_text.as_string()
try:
start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
pre, post = code.split(start)[0], code.split(end)[1]
final = f"# Copied: {datetime.datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
context.window_manager.clipboard = final
self.report({'INFO'}, "現在のパラメータでコードをコピーしました!")
except Exception as e:
self.report({'ERROR'}, f"コピーに失敗: {e}")
return {'CANCELLED'}
return {'FINISHED'}
class OT_ResetProperty(Operator):
bl_idname = f"{OP_PREFIX}.reset_property"
bl_label = "Reset Property"
bl_options = {'REGISTER', 'UNDO'}
prop_name: StringProperty()
def execute(self, context):
props = getattr(context.scene, PROPS_NAME)
if self.prop_name in CURRENT_DEFAULTS:
setattr(props, self.prop_name, CURRENT_DEFAULTS[self.prop_name])
return {'FINISHED'}
class OT_ResetAllSettings(Operator):
bl_idname = f"{OP_PREFIX}.reset_all_settings"
bl_label = "Reset All Settings"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME)
for k, v in CURRENT_DEFAULTS.items():
setattr(props, k, v)
context.scene.b200_object_counter = 1
self.report({'INFO'}, "All settings and counter have been reset.")
return {'FINISHED'}
class OT_ResetCounter(Operator):
bl_idname = f"{OP_PREFIX}.reset_counter"
bl_label = "Reset Counter"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
context.scene.b200_object_counter = 1
return {'FINISHED'}
class OT_OpenUrl(Operator):
bl_idname = f"{OP_PREFIX}.open_url"
bl_label = "Open URL"
url: StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class OT_RemoveAddon(Operator):
bl_idname = f"{OP_PREFIX}.remove_addon"
bl_label = "Remove Addon"
def execute(self, context):
def delayed_unregister(): unregister(); return None
bpy.app.timers.register(delayed_unregister, first_interval=0.1)
return {'FINISHED'}
# --- View Control Operators ---
class OT_View_GetCurrent(Operator):
bl_idname = f"{OP_PREFIX}.view_get_current"
bl_label = "Get Current View & Update"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return {'CANCELLED'}
r3d = context.space_data.region_3d if context.space_data else None
if not r3d: return {'CANCELLED'}
target_pos = Vector(r3d.view_location)
actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
max_val = max(abs(actual_cam_pos.x), abs(actual_cam_pos.y), abs(actual_cam_pos.z))
if max_val > props.slider_limit: props.slider_limit = max_val + 50.0
props.view_pos = actual_cam_pos
return {'FINISHED'}
class OT_View_Reset(Operator):
bl_idname = f"{OP_PREFIX}.view_reset"
bl_label = VIEW_RESET_BTN_TEXT
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props:
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.region_3d.view_location = (0.0, 0.0, 0.0)
props.view_pos = VIEW_POS_INIT
return {'FINISHED'}
class OT_View_CenterSelected(Operator):
bl_idname = f"{OP_PREFIX}.view_center_selected"
bl_label = "Center Selected Object"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.view3d.view_selected()
r3d = context.space_data.region_3d if context.space_data else None
if r3d:
props = getattr(context.scene, PROPS_NAME, None)
if props:
target_pos = Vector(r3d.view_location)
props.view_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
return {'FINISHED'}
class OT_View_CopyActualPos(Operator):
bl_idname = f"{OP_PREFIX}.view_copy_actual_pos"
bl_label = "Copy Position Only"
def execute(self, context):
r3d = context.space_data.region_3d if context.space_data else None
if not r3d: return {'CANCELLED'}
target_pos = Vector(r3d.view_location)
p = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
context.window_manager.clipboard = f"Actual View Pos: ({p.x:.4f}, {p.y:.4f}, {p.z:.4f})"
self.report({'INFO'}, "視座位置をコピーしました")
return {'FINISHED'}
class OT_View_CopyAngles(Operator):
bl_idname = f"{OP_PREFIX}.view_copy_angles"
bl_label = "Copy Full Info (Pos & Angles)"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
r3d = context.space_data.region_3d if context.space_data else None
if not r3d or not props: return {'CANCELLED'}
target_pos = Vector(r3d.view_location)
actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
vec = target_pos - actual_cam_pos
length = vec.length
if length < 0.0001: return {'CANCELLED'}
ang_x = math.degrees(math.acos(vec.x / length))
ang_y = math.degrees(math.acos(vec.y / length))
ang_z = math.degrees(math.acos(vec.z / length))
pl_x = math.degrees(math.asin(vec.x / length))
pl_y = math.degrees(math.asin(vec.y / length))
pl_z = math.degrees(math.asin(vec.z / length))
info_text = (
f"--- View Direction Info ---\n"
f"[ Actual 3D View Status ]\n"
f"Actual View Pos : ({actual_cam_pos.x:.4f}, {actual_cam_pos.y:.4f}, {actual_cam_pos.z:.4f})\n"
f"Target Pos : ({target_pos.x:.4f}, {target_pos.y:.4f}, {target_pos.z:.4f})\n"
f"Distance : {length:.4f}\n\n"
f"[ Direction Angles (軸そのものとの角度 0〜180°) ]\n"
f"Angle from X Axis : {ang_x:.2f} deg\n"
f"Angle from Y Axis : {ang_y:.2f} deg\n"
f"Angle from Z Axis : {ang_z:.2f} deg\n\n"
f"[ Planar Angles (直感的な傾き・ズレ角 -90〜90°) ]\n"
f"X (横のズレ角) : {pl_x:.2f} deg\n"
f"Y (前後の傾き) : {pl_y:.2f} deg\n"
f"Z (仰角・俯角) : {pl_z:.2f} deg\n"
)
context.window_manager.clipboard = info_text
self.report({'INFO'}, "情報全体をクリップボードにコピーしました")
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PG_BasePanel:
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
def _draw_prop_with_reset(self, layout, obj, prop_name, text=None):
row = layout.row(align=True)
split = row.split(factor=0.85, align=True)
split.prop(obj, prop_name, text=text if text else prop_name.replace("_", " ").title())
op = split.operator(OT_ResetProperty.bl_idname, text="", icon='FILE_REFRESH')
op.prop_name = prop_name
def _draw_vector_xyz(self, layout, obj, prop_name, label):
col = layout.column(align=True)
col.label(text=label)
for i, axis in enumerate(['X', 'Y', 'Z']):
row = col.row(align=True)
split = row.split(factor=0.85, align=True)
split.prop(obj, prop_name, index=i, text=axis)
op = split.operator(OT_ResetProperty.bl_idname, text="", icon='FILE_REFRESH')
op.prop_name = prop_name
def _draw_vector_2d(self, layout, obj, prop_name, label, labels=['U', 'V']):
col = layout.column(align=True)
col.label(text=label)
for i, axis in enumerate(labels):
row = col.row(align=True)
split = row.split(factor=0.85, align=True)
split.prop(obj, prop_name, index=i, text=axis)
op = split.operator(OT_ResetProperty.bl_idname, text="", icon='FILE_REFRESH')
op.prop_name = prop_name
class PT_ViewControlPanel(PG_BasePanel, Panel):
bl_label = "View Control (視座位置)"
bl_idname = f"{OP_PREFIX}_PT_view_control"
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="状態を保持してコードコピー")
layout.separator()
box = layout.box()
box.label(text="Perspective Viewpoint", icon='VIEW_CAMERA')
box.prop(props, "slider_limit", text="Range Limit (+/-)")
col = box.column(align=True)
col.prop(props, "view_pos", text="X", index=0)
col.prop(props, "view_pos", text="Y", index=1)
col.prop(props, "view_pos", text="Z", index=2)
box.separator()
box.operator(OT_View_GetCurrent.bl_idname, icon='RESTRICT_VIEW_OFF')
box.operator(OT_View_Reset.bl_idname, icon='LOOP_BACK')
layout.operator(OT_View_CenterSelected.bl_idname, icon='VIEWZOOM')
layout.separator()
box_info = layout.box()
box_info.label(text="Actual View Status", icon='INFO')
r3d = context.space_data.region_3d if context.space_data else None
if r3d:
target_pos = Vector(r3d.view_location)
actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
vec = target_pos - actual_cam_pos
length = vec.length
col_pos = box_info.column(align=True)
col_pos.label(text="[ Actual Position ]", icon='VIEW_CAMERA')
col_pos.label(text=f" X: {actual_cam_pos.x:.4f}")
col_pos.label(text=f" Y: {actual_cam_pos.y:.4f}")
col_pos.label(text=f" Z: {actual_cam_pos.z:.4f}")
col_pos.label(text=f" Distance: {length:.4f}")
box_info.operator(OT_View_CopyActualPos.bl_idname, icon='COPYDOWN')
box_info.separator()
col_ang = box_info.column(align=True)
if length > 0.0001:
a_x = math.degrees(math.acos(vec.x / length))
a_y = math.degrees(math.acos(vec.y / length))
a_z = math.degrees(math.acos(vec.z / length))
p_x = math.degrees(math.asin(vec.x / length))
p_y = math.degrees(math.asin(vec.y / length))
p_z = math.degrees(math.asin(vec.z / length))
col_ang.label(text="[ Direction Angles (軸との角度) ]", icon='ORIENTATION_GLOBAL')
col_ang.label(text=f" X: {a_x:.2f}°")
col_ang.label(text=f" Y: {a_y:.2f}°")
col_ang.label(text=f" Z: {a_z:.2f}°")
col_ang.separator()
col_ang.label(text="[ Planar Angles (直感的な傾き) ]", icon='DRIVER_ROTATIONAL_DIFFERENCE')
col_ang.label(text=f" X (ズレ角): {p_x:.2f}°")
col_ang.label(text=f" Y (ズレ角): {p_y:.2f}°")
col_ang.label(text=f" Z (仰俯角): {p_z:.2f}°")
else:
col_ang.label(text=" Target is too close")
box_info.separator()
box_info.operator(OT_View_CopyAngles.bl_idname, icon='COPYDOWN')
else:
box_info.label(text="Please use in 3D View")
class PT_MainPanel(PG_BasePanel, Panel):
bl_label = "Primitive Generator [Preview]"
bl_idname = f"{OP_PREFIX}_PT_main"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME)
scene = context.scene
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="状態を保持してコードコピー")
row_view = layout.row()
row_view.operator(OT_ViewCenterFront.bl_idname, icon='VIEWZOOM', text="0,0,0 を正面(Y-)から見る")
layout.separator()
layout.prop(props, "primitive_type", text="図形")
row_sol = layout.row()
row_sol.enabled = (props.primitive_type != 'IMAGE_CYLINDER')
row_sol.prop(props, "use_solidify", text="面に厚みを加える")
if props.use_solidify:
self._draw_prop_with_reset(layout, props, "solidify_thickness", text="厚み")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
row = layout.row()
row.scale_y = 1.5
row.operator(OT_CreatePrimitive.bl_idname, icon='PLUS')
box_naming = layout.box()
box_naming.label(text="Naming & Collection")
self._draw_prop_with_reset(box_naming, props, "collection_name")
self._draw_prop_with_reset(box_naming, props, "sub_collection_name")
self._draw_prop_with_reset(box_naming, props, "object_name_prefix")
row_counter = box_naming.row(align=True)
row_counter.label(text=f"Next Index: {scene.b200_object_counter:03d}")
row_counter.operator(OT_ResetCounter.bl_idname, text="", icon='FILE_REFRESH')
layout.separator()
box_prim = layout.box()
prim_type = props.primitive_type
col = box_prim.column(align=True)
if prim_type == 'CUBE': self._draw_prop_with_reset(col, props, "cube_size")
elif prim_type == 'CUBOID': self._draw_vector_xyz(col, props, "cuboid_dimensions", "Dimensions")
elif prim_type == 'PLANE': self._draw_prop_with_reset(col, props, "plane_size_x"); self._draw_prop_with_reset(col, props, "plane_size_y")
elif prim_type == 'CIRCLE': self._draw_prop_with_reset(col, props, "circle_radius"); self._draw_prop_with_reset(col, props, "circle_vertices")
elif prim_type == 'ELLIPSE': self._draw_prop_with_reset(col, props, "ellipse_radius_x"); self._draw_prop_with_reset(col, props, "ellipse_radius_y"); self._draw_prop_with_reset(col, props, "ellipse_vertices")
elif prim_type == 'UV_SPHERE': self._draw_prop_with_reset(col, props, "uv_sphere_radius"); self._draw_prop_with_reset(col, props, "uv_sphere_segments"); self._draw_prop_with_reset(col, props, "uv_sphere_rings")
elif prim_type == 'ICO_SPHERE': self._draw_prop_with_reset(col, props, "ico_sphere_radius"); self._draw_prop_with_reset(col, props, "ico_sphere_subdivisions")
elif prim_type == 'CYLINDER':
col.prop(props, "cylinder_mode", text="")
col.separator(factor=0.5)
self._draw_prop_with_reset(col, props, "cylinder_radius")
if props.cylinder_mode == 'CENTER':
self._draw_prop_with_reset(col, props, "cylinder_depth")
else:
col.separator()
self._draw_vector_xyz(col, props, "cylinder_point_top", "Top Point:")
col.separator(factor=0.5)
self._draw_vector_xyz(col, props, "cylinder_point_bottom", "Bottom Point:")
col.separator(factor=0.5)
self._draw_prop_with_reset(col, props, "cylinder_vertices")
row_caps = col.row(align=True)
row_caps.prop(props, "cylinder_cap_top")
row_caps.prop(props, "cylinder_cap_bottom")
elif prim_type == 'FRUSTUM':
col.prop(props, "frustum_mode", text="")
col.separator(factor=0.5)
self._draw_prop_with_reset(col, props, "frustum_radius_top")
self._draw_prop_with_reset(col, props, "frustum_radius_bottom")
if props.frustum_mode == 'CENTER':
self._draw_prop_with_reset(col, props, "frustum_height")
else:
col.separator()
self._draw_vector_xyz(col, props, "frustum_point_top", "Top Point:")
col.separator(factor=0.5)
self._draw_vector_xyz(col, props, "frustum_point_bottom", "Bottom Point:")
col.separator(factor=0.5)
self._draw_prop_with_reset(col, props, "frustum_vertices")
row_caps = col.row(align=True); row_caps.prop(props, "frustum_cap_top"); row_caps.prop(props, "frustum_cap_bottom")
elif prim_type == 'IMAGE_CYLINDER':
self._draw_prop_with_reset(col, props, "image_cylinder_radius")
self._draw_prop_with_reset(col, props, "image_cylinder_depth")
self._draw_prop_with_reset(col, props, "image_cylinder_vertices")
col.separator()
box_img = col.box()
box_img.label(text="内側に貼る画像:")
box_img.template_ID(props, "image_cylinder_image", open="image.open")
box_img.separator()
self._draw_vector_2d(box_img, props, "image_cylinder_uv_offset", "UV Offset (位置ずらし):")
box_img.separator()
self._draw_vector_2d(box_img, props, "image_cylinder_uv_scale", "UV Scale (繰り返し/縮尺):")
col.separator()
self._draw_prop_with_reset(col, props, "image_cylinder_alpha_outer")
self._draw_prop_with_reset(col, props, "image_cylinder_alpha_inner")
elif prim_type == 'CUBE_FRAME':
self._draw_prop_with_reset(col, props, "cube_frame_size")
self._draw_prop_with_reset(col, props, "cube_frame_radius")
self._draw_prop_with_reset(col, props, "cube_frame_vertices")
elif prim_type == 'CUBOID_FRAME':
self._draw_vector_xyz(col, props, "cuboid_frame_dimensions", "Dimensions")
self._draw_prop_with_reset(col, props, "cuboid_frame_radius")
self._draw_prop_with_reset(col, props, "cuboid_frame_vertices")
elif prim_type == 'TORUS':
self._draw_prop_with_reset(col, props, "torus_major_radius"); self._draw_prop_with_reset(col, props, "torus_minor_radius")
self._draw_prop_with_reset(col, props, "torus_major_segments"); self._draw_prop_with_reset(col, props, "torus_minor_segments")
elif prim_type == 'MONKEY':
self._draw_prop_with_reset(col, props, "monkey_size")
# 配列図形UI
elif prim_type.startswith('SPHERE_'):
self._draw_prop_with_reset(col, props, "sphere_spacing")
if 'SQUARE' in prim_type or 'CUBE' in prim_type:
self._draw_prop_with_reset(col, props, "sphere_size", text="全体サイズ")
else:
self._draw_prop_with_reset(col, props, "sphere_radius_val", text="全体半径")
col.separator()
box_sph = col.box()
box_sph.label(text="個々の球体設定:")
self._draw_prop_with_reset(box_sph, props, "sphere_elem_radius", text="球の半径")
self._draw_prop_with_reset(box_sph, props, "sphere_segments")
self._draw_prop_with_reset(box_sph, props, "sphere_rings")
elif prim_type == 'TORUS_CONCENTRIC':
self._draw_prop_with_reset(col, props, "torus_count", text="同心円の数")
self._draw_prop_with_reset(col, props, "torus_spacing", text="間隔")
self._draw_prop_with_reset(col, props, "torus_minor_radius_arr", text="トーラスの太さ")
self._draw_prop_with_reset(col, props, "torus_major_segments")
self._draw_prop_with_reset(col, props, "torus_minor_segments")
elif prim_type == 'CYLINDER_GRID':
box_c = col.box()
box_c.label(text="Count (本数):")
row_c = box_c.row(align=True)
row_c.prop(props, "grid_count_x", text="X")
row_c.prop(props, "grid_count_y", text="Y")
row_c.prop(props, "grid_count_z", text="Z")
box_s = col.box()
box_s.label(text="Spacing (間隔):")
row_s = box_s.row(align=True)
row_s.prop(props, "grid_spacing_x", text="X")
row_s.prop(props, "grid_spacing_y", text="Y")
row_s.prop(props, "grid_spacing_z", text="Z")
col.separator()
self._draw_prop_with_reset(col, props, "grid_radius", text="円柱の太さ")
self._draw_prop_with_reset(col, props, "grid_vertices", text="円柱の頂点数")
class PT_TransformPanel(PG_BasePanel, Panel):
bl_label = "Transform (位置・回転・スケール)"
bl_idname = f"{OP_PREFIX}_PT_transform"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
props = getattr(context.scene, PROPS_NAME)
layout = self.layout
disable_transform = False
if props.primitive_type == 'CYLINDER' and props.cylinder_mode != 'CENTER':
disable_transform = True
elif props.primitive_type == 'FRUSTUM' and props.frustum_mode != 'CENTER':
disable_transform = True
main_col = layout.column()
main_col.enabled = not disable_transform
if disable_transform:
main_col.label(text="※ 端点指定モードでは操作できません", icon='INFO')
self._draw_vector_xyz(main_col, props, "location", "Location:")
main_col.separator()
col_scale = main_col.column(align=True)
row_scale_uni = col_scale.row(align=True)
row_scale_uni.prop(props, "scale_uniform", text="Uniform Scale")
if props.scale_uniform:
self._draw_prop_with_reset(row_scale_uni, props, "scale_factor", text="")
else:
self._draw_vector_xyz(col_scale, props, "scale_vector", "Scale:")
main_col.separator()
col_rot = main_col.column(align=True)
col_rot.label(text="Rotation (XYZ Euler):")
self._draw_prop_with_reset(col_rot, props, "additional_rotation_x", text="X")
self._draw_prop_with_reset(col_rot, props, "additional_rotation_y", text="Y")
self._draw_prop_with_reset(col_rot, props, "additional_rotation_z", text="Z")
class PT_MaterialPanel(PG_BasePanel, Panel):
bl_label = "Material & Color"
bl_idname = f"{OP_PREFIX}_PT_material"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
props = getattr(context.scene, PROPS_NAME)
layout = self.layout
if props.primitive_type == 'IMAGE_CYLINDER':
layout.label(text="※ 画像パノラマ筒は上部の専用設定が適用されます", icon='INFO')
return
col_mat = layout.column(align=True)
col_mat.prop(props, "color_mode", text="Mode")
if props.color_mode == 'PRESET':
col_mat.prop(props, "preset_set")
box_presets = col_mat.box()
box_presets.label(text="番号クリックで適用 / 色枠で編集:")
grid = box_presets.grid_flow(row_major=True, columns=5, even_columns=True, align=True)
preset_prefix = f"preset_color_{props.preset_set.lower()}_" if props.preset_set != 'A' else "preset_color_"
for i in range(1, 11):
row = grid.row(align=True)
row.prop_enum(props, "preset_color", str(i), text=str(i))
row.prop(props, f"{preset_prefix}{i}", text="")
elif props.color_mode == 'PICKER':
self._draw_prop_with_reset(col_mat, props, "custom_color", text="")
layout.separator()
self._draw_prop_with_reset(layout, props, "face_alpha")
layout.separator()
layout.operator(OT_ResetAllSettings.bl_idname, icon='LOOP_BACK')
class PT_LinksPanel(PG_BasePanel, Panel):
bl_label = "Links"
bl_idname = f"{OP_PREFIX}_PT_links"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
def draw_section(prop_name, link_list, title):
box = layout.box()
row = box.row()
is_expanded = getattr(props, prop_name)
row.prop(props, prop_name, icon="TRIA_DOWN" if is_expanded else "TRIA_RIGHT", emboss=False, text=title)
if is_expanded:
col = box.column(align=True)
for link in link_list:
op = col.operator(OT_OpenUrl.bl_idname, text=link["label"], icon='URL')
op.url = link["url"]
draw_section("show_main_docs", THIS_LINKS, "This Addon")
draw_section("show_new_docs", NEW_DOC_LINKS, "Documents Index")
draw_section("show_old_docs", DOC_LINKS, "Old Documents")
draw_section("show_social", SOCIAL_LINKS, "Social Links")
class PT_RemovePanel(PG_BasePanel, Panel):
bl_label = "System"
bl_idname = f"{OP_PREFIX}_PT_remove"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
self.layout.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Unregister Addon")
# ==============================================================================
# REGISTER
# ==============================================================================
def initial_setup():
context = bpy.context
if not hasattr(context, "scene") or not context.scene:
return 0.1
update_preview_geometry(context)
return None
def open_sidebar():
if not bpy.context: return None
for window in bpy.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.show_region_ui = True
return None
classes = (
PG_B200Props,
OT_ViewCenterFront, OT_CreatePrimitive, OT_CopyFullScript, OT_ResetProperty, OT_ResetAllSettings, OT_ResetCounter, OT_OpenUrl, OT_RemoveAddon,
OT_View_GetCurrent, OT_View_Reset, OT_View_CenterSelected, OT_View_CopyActualPos, OT_View_CopyAngles,
PT_ViewControlPanel,
PT_MainPanel,
PT_TransformPanel,
PT_MaterialPanel,
PT_LinksPanel,
PT_RemovePanel
)
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_B200Props))
bpy.types.Scene.b200_object_counter = IntProperty(name="Object Counter", default=1, min=1)
if not bpy.app.timers.is_registered(initial_setup):
bpy.app.timers.register(initial_setup, first_interval=0.1)
if not bpy.app.timers.is_registered(open_sidebar):
bpy.app.timers.register(open_sidebar, first_interval=0.1)
if not bpy.app.timers.is_registered(view_sync_timer):
bpy.app.timers.register(view_sync_timer)
def unregister():
if bpy.app.timers.is_registered(initial_setup):
bpy.app.timers.unregister(initial_setup)
if bpy.app.timers.is_registered(view_sync_timer):
bpy.app.timers.unregister(view_sync_timer)
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
if hasattr(bpy.types.Scene, 'b200_object_counter'): delattr(bpy.types.Scene, 'b200_object_counter')
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except RuntimeError: pass
if __name__ == "__main__":
try: unregister()
except Exception: pass
register()'''
# ==============================================================================
# 【 内包する3つ目のスクリプト (3D Viewport Color & Sun & Perspective) 】
# ==============================================================================
VIEWPORT_SCRIPT_CONTENT = r'''# Copied: 20260319 15:00:01
import bpy
import os
import math
from bpy.props import FloatVectorProperty, FloatProperty, EnumProperty, StringProperty, BoolProperty, PointerProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector, Euler
from math import radians
from datetime import datetime
# ★ このスクリプト自身のID (コピー機能で使用)
# ### ZIONAD_SOURCE_ID: VIEWPORT_COLOR_2026_03_16 ###
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: VIEWPORT_COLOR_2026_03_16 ###"
# アドオンのメタデータ
bl_info = {
"name": "zionad 5520[ 3D Viewport Color & Sun & Perspective ] 20260319",
"author": "zionadchat",
"version": (3, 0, 0),
"blender": (4, 4, 0),
"category": " 5520[ 3D Viewport ] ",
"description": "3Dビューポートの色、太陽、透視投影視座位置をリアルタイム制御します",
"location": "3Dビュー > サイドバー",
}
# 定数
ADDON_CATEGORY_NAME = bl_info["category"]
PREFIX = "view2026316"
VIEW_RESET_BTN_TEXT = "Reset View (0, -10, 10)"
VIEW_POS_INIT = (0.0, -10.0, 10.0)
# ==============================================================================
# デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# プリセットによる上書きを防ぐため、プリセット項目を先に読み込む順序にしています
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"sun_control_mode": 'ANGLE',
"grid_preset": 'white',
"wire_preset": 'orange',
"camera_preset": 'Cam',
"background_type": 'LINEAR',
"header_preset": 'Dark Green',
"preset": 'Dark Green',
"render_preset": 'Blue',
"outliner_preset": 'Outliner 4.4.0',
"text_editor_preset": 'Text 4.4.0',
"sun_target_location": (0.0000, 0.0000, 0.0000),
"sun_rotation": (0.7854, 0.0000, 0.7854),
"sun_location": (0.0000, 0.0000, 10.0000),
"sun_strength": 2.5000,
"custom_grid_scale": 1.0000,
"grid_color": (1.0000, 1.0000, 1.0000, 1.0000),
"wire_color": (0.0000, 0.0000, 0.0000),
"camera_color": (0.4700, 0.5500, 1.0000),
"header_color": (0.0000, 0.0300, 0.0000, 1.0000),
"custom_gradient_high": (0.2256, 0.2800, 0.1424),
"custom_gradient_low": (0.1000, 0.1500, 0.0500),
"reverse_gradient": False,
"render_color": (0.1900, 0.6000, 1.0000, 1.0000),
"render_environment_strength": 1.0000,
"outliner_header_color": (0.1900, 0.1900, 0.1900, 0.7000),
"outliner_background_color": (0.1400, 0.1400, 0.1400, 1.0000),
"text_editor_header_color": (0.1900, 0.1900, 0.1900, 0.7000),
"text_editor_background_color": (0.1400, 0.1400, 0.1400, 1.0000),
"view_pos": (0.0000, -10.0000, 10.0000),
}
# <END_DICT>
# パネル定義
COPY_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_copy_panel"
PERSP_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_persp_control"
OVERLAY_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_overlay_panel"
BG_PANEL_IDNAME_1 = f"{PREFIX}_VIEW3D_PT_solid_background_panel"
HEADER_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_header_panel"
RENDER_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_render_panel"
SUN_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_sun_panel"
GRID_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_gridpanel"
WIRE_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_wirepanel"
CAMERA_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_camerapanel"
OUTLINER_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_outliner_panel"
TEXT_EDITOR_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_text_editor_panel"
LINK_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_link_panel"
REMOVE_PANEL_IDNAME = f"{PREFIX}_VIEW3D_PT_remove"
# パネルラベル
PANEL_LABELS = {
"COPY": "コードコピー",
"PERSP": "透視投影 視座位置",
"OVERLAY": "Overlays",
"BACKGROUND": "3D Viewport Color",
"HEADER": "Header Color",
"RENDER": "Render Color",
"SUN": "太陽 設定",
"GRID": "Grid Color",
"WIRE": "Wire Color",
"CAMERA": "Camera Color",
"OUTLINER": "Outliner Color",
"TEXT_EDITOR": "Text Editor Color",
"LINK": "リンク",
"REMOVE": "アドオン削除",
}
# プリセット群
BASE_PRESETS =[
("Dark Green", "Dark Green", "Dark Green background", (0.00, 0.28, 0.02), (0.10, 0.15, 0.05)),
("purple", "purple", "purple background", (0.49, 0.45, 1.00), (0.74, 0.65, 0.88)),
("BlueGreen", "BlueGreen", "BlueGreen background", (0.14, 0.34, 0.83), (0.57, 0.88, 0.63)),
("DARK_BLUE", "Dark Blue", "Dark Blue background", (0.07, 0.13, 0.31), (0.05, 0.05, 0.15)),
("Viewport 4.4.0", "Viewport 4.4.0", "Viewport 4.4.0 background", (0.24, 0.24, 0.24), (0.19, 0.19, 0.19)),
("FOREST_GREEN", "Forest Green", "Forest Green background", (0.50, 0.70, 0.50), (0.10, 0.15, 0.05)),
]
HEADER_PRESETS =[
("Dark Green", "Dark Green", "Dark Green header", (0.00, 0.03, 0.00, 1.00)),
("purple", "purple", "purple header", (0.00, 0.00, 0.00, 1.00)),
("BlueGreen", "BlueGreen", "BlueGreen header", (0.00, 0.00, 0.00, 1.00)),
("DARK_BLUE", "Dark Blue", "Dark Blue header", (0.10, 0.10, 0.30, 1.00)),
("Viewport 4.4.0", "Viewport 4.4.0", "Viewport 4.4.0 header", (0.19, 0.19, 0.19, 0.70)),
("FOREST_GREEN", "Forest Green", "Forest Green header", (0.20, 0.30, 0.10, 1.00)),
]
RENDER_PRESETS =[
("Blue", "Blue", "Blue render color", (0.19, 0.60, 1.00, 1.00)),
("Render 4.4.0", "Render 4.4.0", "Render 4.4.0 color", (0.05, 0.05, 0.05, 1.00)),
("LIGHT_GRAY", "Light Gray", "Light gray render", (0.80, 0.80, 0.80, 1.00)),
]
GRID_PRESETS =[
("white", "white", "white grid color", (1.00, 1.00, 1.00, 1.00)),
("Grid 4.4.0", "Grid 4.4.0", "Grid 4.4.0 color", (0.33, 0.33, 0.33, 0.50)),
("DARK_GRAY", "Dark Gray", "Dark gray grid", (0.10, 0.10, 0.10, 1.00)),
]
WIRE_PRESETS =[
("orange", "orange", "orange wire", (0.71, 0.21, 0.05)),
("Wire 4.4.0", "Wire 4.4.0", "Wire 4.4.0 color", (0.00, 0.00, 0.00)),
("WHITE", "White", "White wire", (1.00, 1.00, 1.00)),
]
CAMERA_PRESETS =[
("Cam", "Cam", "Cam camera color", (0.47, 0.55, 1.00)),
("Cam 4.4.0", "Cam 4.4.0", "Cam 4.4.0 color", (0.00, 0.00, 0.00)),
("YELLOW", "Yellow", "Yellow camera", (1.00, 1.00, 0.00)),
]
OUTLINER_PRESETS =[
("Outliner 4.4.0", "Outliner 4.4.0", "Outliner 4.4.0 colors", (0.19, 0.19, 0.19, 0.70), (0.14, 0.14, 0.14, 1.00)),
("DARK_TEAL", "Dark Teal", "Dark teal outliner", (0.00, 0.20, 0.20, 1.00), (0.00, 0.10, 0.10, 1.00)),
]
TEXT_EDITOR_PRESETS =[
("Text 4.4.0", "Text 4.4.0", "Text Editor 4.4.0 colors", (0.19, 0.19, 0.19, 0.70), (0.14, 0.14, 0.14, 1.00)),
("DARK_GREEN", "Dark Green", "Dark green text editor", (0.00, 0.20, 0.00, 1.00), (0.00, 0.10, 0.00, 1.00)),
]
BACKGROUND_TYPES =[
('SINGLE_COLOR', "Single Color", "Uniform background color"),
('LINEAR', "Linear", "Linear gradient background"),
('RADIAL', "Vignette", "Radial gradient simulating a vignette effect"),
]
# ==============================================================================
# リアルタイム更新用コールバック関数(ビューポート色・太陽)
# ==============================================================================
def format_tuple(t):
return '(' + ', '.join(f"{x:.3f}" for x in t) + ')'
def update_custom_grid_scale(self, context):
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
area.spaces.active.overlay.grid_scale = self.custom_grid_scale
def update_grid_color(self, context):
bpy.context.preferences.themes[0].view_3d.grid = self.grid_color
def update_wire_color(self, context):
bpy.context.preferences.themes[0].view_3d.wire = self.wire_color
def update_camera_color(self, context):
bpy.context.preferences.themes[0].view_3d.camera = self.camera_color
def update_background_color(self, context):
gradients = bpy.context.preferences.themes[0].view_3d.space.gradients
if self.background_type == 'SINGLE_COLOR':
gradients.background_type = 'LINEAR'
gradients.high_gradient = gradients.gradient = self.custom_gradient_low if self.reverse_gradient else self.custom_gradient_high
else:
gradients.background_type = self.background_type
gradients.high_gradient = self.custom_gradient_low if self.reverse_gradient else self.custom_gradient_high
gradients.gradient = self.custom_gradient_high if self.reverse_gradient else self.custom_gradient_low
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D': area.tag_redraw()
def update_header_color(self, context):
bpy.context.preferences.themes[0].view_3d.space.header = self.header_color
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D': area.tag_redraw()
def update_render_color(self, context):
bpy.context.preferences.themes[0].image_editor.space.back = self.render_color[:3]
world = bpy.data.worlds.get('MyWorld') or bpy.data.worlds.new('MyWorld')
context.scene.world = world
world.use_nodes = True
bg_node = world.node_tree.nodes.get('Background') or world.node_tree.nodes.new(type='ShaderNodeBackground')
bg_node.name = 'Background'
bg_node.inputs[0].default_value = self.render_color
bg_node.inputs[1].default_value = self.render_environment_strength
output_node = world.node_tree.nodes.get('World Output') or world.node_tree.nodes.new(type='ShaderNodeOutputWorld')
output_node.name = 'World Output'
world.node_tree.links.new(bg_node.outputs[0], output_node.inputs['Surface'])
def update_outliner_color(self, context):
space = bpy.context.preferences.themes[0].outliner.space
space.header = self.outliner_header_color
space.back = self.outliner_background_color[:3]
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'OUTLINER': area.tag_redraw()
def update_text_editor_color(self, context):
space = bpy.context.preferences.themes[0].text_editor.space
space.header = self.text_editor_header_color
space.back = self.text_editor_background_color[:3]
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'TEXT_EDITOR': area.tag_redraw()
def get_or_create_sun():
sun_obj = bpy.data.objects.get("Sun")
if sun_obj is None or sun_obj.type != 'LIGHT' or sun_obj.data.type != 'SUN':
if sun_obj:
try: bpy.data.objects.remove(sun_obj, do_unlink=True)
except: pass
bpy.ops.object.light_add(type='SUN', align='WORLD', location=(0, 0, 0))
sun_obj = bpy.context.active_object
sun_obj.name = "Sun"; sun_obj.data.name = "Sun"
return sun_obj
def update_sun(self, context):
sun = get_or_create_sun()
sun.location = self.sun_location
if self.sun_control_mode == 'ANGLE':
sun.rotation_euler = self.sun_rotation
else:
target_vec = Vector(self.sun_target_location)
sun_vec = Vector(self.sun_location)
if (target_vec - sun_vec).length_squared < 0.0001: return
direction = target_vec - sun_vec
sun.rotation_euler = direction.to_track_quat('-Z', 'Y').to_euler()
sun.data.energy = self.sun_strength
# ==============================================================================
# リアルタイム更新用コールバック関数(透視投影 視座位置)
# ==============================================================================
_is_updating_view = False
def update_view_position(self, context):
"""スライダーが操作されたときに視点を更新する"""
global _is_updating_view
if _is_updating_view: return
props = getattr(context.scene, "persp_view_props", None)
if not props: return
limit = props.slider_limit
v = list(props.view_pos)
clamped = False
for i in range(3):
if v[i] > limit: v[i] = limit; clamped = True
elif v[i] < -limit: v[i] = -limit; clamped = True
if clamped:
_is_updating_view = True
props.view_pos = v
_is_updating_view = False
_is_updating_view = True
try:
cam_pos = Vector(props.view_pos)
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':
r3d = space.region_3d
r3d.view_perspective = 'PERSP'
target_pos = Vector(r3d.view_location)
rel_pos = cam_pos - target_pos
dist = rel_pos.length
if dist > 0.001:
r3d.view_distance = dist
r3d.view_rotation = rel_pos.to_track_quat('Z', 'Y')
finally:
_is_updating_view = False
def view_sync_timer():
"""マウス操作での視点移動を検知し、スライダー(UI)を同期するタイマー"""
global _is_updating_view
if _is_updating_view: return 0.05
context = bpy.context
if getattr(context, "scene", None) is None: return 0.05
props = getattr(context.scene, "persp_view_props", None)
if not props: return 0.05
r3d = None
target_area = None
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':
r3d = space.region_3d
target_area = area
break
if r3d: break
if r3d: break
if r3d and target_area:
target_pos = Vector(r3d.view_location)
actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
current_pos = Vector(props.view_pos)
if (current_pos - actual_cam_pos).length > 0.001:
_is_updating_view = True
max_val = max(abs(actual_cam_pos.x), abs(actual_cam_pos.y), abs(actual_cam_pos.z))
if max_val > props.slider_limit:
props.slider_limit = max_val + 50.0
props.view_pos = actual_cam_pos
_is_updating_view = False
target_area.tag_redraw()
return 0.05
# ------------------------------------------------------------------------
# Property Groups
# ------------------------------------------------------------------------
class SunSettingsProperties(PropertyGroup):
sun_control_mode: EnumProperty(name="制御モード", items=[('ANGLE', "角度", "太陽の回転を直接指定"), ('TARGET', "ターゲット", "指定位置に太陽を向ける")], default=CURRENT_DEFAULTS["sun_control_mode"], update=update_sun)
sun_target_location: FloatVectorProperty(name="ターゲット位置", subtype='XYZ', default=CURRENT_DEFAULTS["sun_target_location"], update=update_sun)
sun_rotation: FloatVectorProperty(name="角度", subtype='EULER', unit='ROTATION', default=CURRENT_DEFAULTS["sun_rotation"], update=update_sun)
sun_location: FloatVectorProperty(name="位置", subtype='XYZ', default=CURRENT_DEFAULTS["sun_location"], update=update_sun)
sun_strength: FloatProperty(name="強さ", default=CURRENT_DEFAULTS["sun_strength"], min=0.0, update=update_sun)
class ViewportColorProperties(PropertyGroup):
custom_grid_scale: FloatProperty(name="Scale", default=CURRENT_DEFAULTS["custom_grid_scale"], min=0.001, update=update_custom_grid_scale)
grid_color: FloatVectorProperty(name="Grid Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["grid_color"], update=update_grid_color)
grid_preset: EnumProperty(name="Grid Preset", items=[(p[0], p[1], p[2]) for p in GRID_PRESETS], default=CURRENT_DEFAULTS["grid_preset"], update=lambda self, context: self.update_grid_preset(context))
wire_color: FloatVectorProperty(name="Wire Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=CURRENT_DEFAULTS["wire_color"], update=update_wire_color)
wire_preset: EnumProperty(name="Wire Preset", items=[(p[0], p[1], p[2]) for p in WIRE_PRESETS], default=CURRENT_DEFAULTS["wire_preset"], update=lambda self, context: self.update_wire_preset(context))
camera_color: FloatVectorProperty(name="Camera Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=CURRENT_DEFAULTS["camera_color"], update=update_camera_color)
camera_preset: EnumProperty(name="Camera Preset", items=[(p[0], p[1], p[2]) for p in CAMERA_PRESETS], default=CURRENT_DEFAULTS["camera_preset"], update=lambda self, context: self.update_camera_preset(context))
background_type: EnumProperty(name="Background Type", items=BACKGROUND_TYPES, default=CURRENT_DEFAULTS["background_type"], update=update_background_color)
header_color: FloatVectorProperty(name="Header Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["header_color"], update=update_header_color)
header_preset: EnumProperty(name="Header Preset", items=[(p[0], p[1], p[2]) for p in HEADER_PRESETS], default=CURRENT_DEFAULTS["header_preset"], update=lambda self, context: self.update_header_preset(context))
custom_gradient_high: FloatVectorProperty(name="Gradient High Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=CURRENT_DEFAULTS["custom_gradient_high"], update=update_background_color)
custom_gradient_low: FloatVectorProperty(name="Gradient Low Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=CURRENT_DEFAULTS["custom_gradient_low"], update=update_background_color)
reverse_gradient: BoolProperty(name="Reverse Gradient", default=CURRENT_DEFAULTS["reverse_gradient"], update=update_background_color)
preset: EnumProperty(name="Color Preset", items=[(p[0], p[1], p[2]) for p in BASE_PRESETS], default=CURRENT_DEFAULTS["preset"], update=lambda self, context: self.update_preset(context))
render_color: FloatVectorProperty(name="Render Background Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["render_color"], update=update_render_color)
render_preset: EnumProperty(name="Render Preset", items=[(p[0], p[1], p[2]) for p in RENDER_PRESETS], default=CURRENT_DEFAULTS["render_preset"], update=lambda self, context: self.update_render_preset(context))
render_environment_strength: FloatProperty(name="Render Environment Strength", default=CURRENT_DEFAULTS["render_environment_strength"], min=0.0, max=1900.0, update=update_render_color)
outliner_header_color: FloatVectorProperty(name="Outliner Header Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["outliner_header_color"], update=update_outliner_color)
outliner_background_color: FloatVectorProperty(name="Outliner Background Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["outliner_background_color"], update=update_outliner_color)
outliner_preset: EnumProperty(name="Outliner Preset", items=[(p[0], p[1], p[2]) for p in OUTLINER_PRESETS], default=CURRENT_DEFAULTS["outliner_preset"], update=lambda self, context: self.update_outliner_preset(context))
text_editor_header_color: FloatVectorProperty(name="Text Editor Header Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["text_editor_header_color"], update=update_text_editor_color)
text_editor_background_color: FloatVectorProperty(name="Text Editor Background Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=CURRENT_DEFAULTS["text_editor_background_color"], update=update_text_editor_color)
text_editor_preset: EnumProperty(name="Text Editor Preset", items=[(p[0], p[1], p[2]) for p in TEXT_EDITOR_PRESETS], default=CURRENT_DEFAULTS["text_editor_preset"], update=lambda self, context: self.update_text_editor_preset(context))
def update_grid_preset(self, context):
for p in GRID_PRESETS:
if p[0] == self.grid_preset: self.grid_color = p[3]; break
def update_wire_preset(self, context):
for p in WIRE_PRESETS:
if p[0] == self.wire_preset: self.wire_color = p[3]; break
def update_camera_preset(self, context):
for p in CAMERA_PRESETS:
if p[0] == self.camera_preset: self.camera_color = p[3]; break
def update_preset(self, context):
for p in BASE_PRESETS:
if p[0] == self.preset: self.custom_gradient_high = p[3]; self.custom_gradient_low = p[4]; break
def update_header_preset(self, context):
for p in HEADER_PRESETS:
if p[0] == self.header_preset: self.header_color = p[3]; break
def update_render_preset(self, context):
for p in RENDER_PRESETS:
if p[0] == self.render_preset: self.render_color = p[3]; break
def update_outliner_preset(self, context):
for p in OUTLINER_PRESETS:
if p[0] == self.outliner_preset: self.outliner_header_color = p[3]; self.outliner_background_color = p[4]; break
def update_text_editor_preset(self, context):
for p in TEXT_EDITOR_PRESETS:
if p[0] == self.text_editor_preset: self.text_editor_header_color = p[3]; self.text_editor_background_color = p[4]; break
class PerspViewProperties(PropertyGroup):
slider_limit: FloatProperty(name="Range Limit", default=300.0, min=10.0, max=10000.0)
view_pos: FloatVectorProperty(name="View Position", size=3, soft_min=-10000.0, soft_max=10000.0,
default=CURRENT_DEFAULTS.get('view_pos', VIEW_POS_INIT),
update=update_view_position)
# ------------------------------------------------------------------------
# Operators
# ------------------------------------------------------------------------
class OT_ViewCenterFront(Operator):
bl_idname = f"{PREFIX}_wm.view_center_front"
bl_label = "Center 0,0,0 (Front View)"
bl_description = "原点(0,0,0)を画面中央に配置し、Yマイナス方向からの視点(正面)にします"
def execute(self, context):
for area in context.screen.areas:
if area.type == 'VIEW_3D':
rv3d = area.spaces.active.region_3d
if rv3d:
rv3d.view_location = (0.0, 0.0, 0.0)
rv3d.view_rotation = Euler((radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
if rv3d.view_distance < 10.0:
rv3d.view_distance = 60.0
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{PREFIX}_wm.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = context.scene.viewport_color_props
sun_props = context.scene.sun_settings_props
persp_props = context.scene.persp_view_props
code = ""
file_path = globals().get('__file__')
if file_path:
if file_path.endswith('.pyc') or file_path.endswith('.pyo'):
file_path = file_path[:-1]
try:
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
code = f.read()
except Exception:
pass
if not code or SOURCE_ID_TAG not in code:
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string():
code = t.as_string()
break
if not code:
self.report({'ERROR'}, "スクリプトのソースが見つかりません。")
return {'CANCELLED'}
def fmt_vec(v): return "(" + ", ".join(f"{x:.4f}" for x in v) + ")"
def fmt_str(s): return f"'{s}'"
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "sun_control_mode": {fmt_str(sun_props.sun_control_mode)},\n'
new_dict += f' "grid_preset": {fmt_str(props.grid_preset)},\n'
new_dict += f' "wire_preset": {fmt_str(props.wire_preset)},\n'
new_dict += f' "camera_preset": {fmt_str(props.camera_preset)},\n'
new_dict += f' "background_type": {fmt_str(props.background_type)},\n'
new_dict += f' "header_preset": {fmt_str(props.header_preset)},\n'
new_dict += f' "preset": {fmt_str(props.preset)},\n'
new_dict += f' "render_preset": {fmt_str(props.render_preset)},\n'
new_dict += f' "outliner_preset": {fmt_str(props.outliner_preset)},\n'
new_dict += f' "text_editor_preset": {fmt_str(props.text_editor_preset)},\n'
new_dict += f' "sun_target_location": {fmt_vec(sun_props.sun_target_location)},\n'
new_dict += f' "sun_rotation": {fmt_vec(sun_props.sun_rotation)},\n'
new_dict += f' "sun_location": {fmt_vec(sun_props.sun_location)},\n'
new_dict += f' "sun_strength": {sun_props.sun_strength:.4f},\n'
new_dict += f' "custom_grid_scale": {props.custom_grid_scale:.4f},\n'
new_dict += f' "grid_color": {fmt_vec(props.grid_color)},\n'
new_dict += f' "wire_color": {fmt_vec(props.wire_color)},\n'
new_dict += f' "camera_color": {fmt_vec(props.camera_color)},\n'
new_dict += f' "header_color": {fmt_vec(props.header_color)},\n'
new_dict += f' "custom_gradient_high": {fmt_vec(props.custom_gradient_high)},\n'
new_dict += f' "custom_gradient_low": {fmt_vec(props.custom_gradient_low)},\n'
new_dict += f' "reverse_gradient": {props.reverse_gradient},\n'
new_dict += f' "render_color": {fmt_vec(props.render_color)},\n'
new_dict += f' "render_environment_strength": {props.render_environment_strength:.4f},\n'
new_dict += f' "outliner_header_color": {fmt_vec(props.outliner_header_color)},\n'
new_dict += f' "outliner_background_color": {fmt_vec(props.outliner_background_color)},\n'
new_dict += f' "text_editor_header_color": {fmt_vec(props.text_editor_header_color)},\n'
new_dict += f' "text_editor_background_color": {fmt_vec(props.text_editor_background_color)},\n'
new_dict += f' "view_pos": {fmt_vec(persp_props.view_pos)},\n'
new_dict += "}\n"
try:
start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
parts = code.split(start)
if len(parts) < 2: return {'CANCELLED'}
pre = parts[0]
post = code.split(end)[1]
lines = pre.split('\n')
if len(lines) > 0 and lines[0].startswith("# Copied:"): lines.pop(0)
pre = '\n'.join(lines).lstrip('\n')
final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n{pre}{start}\n{new_dict}{end}{post}"
context.window_manager.clipboard = final
self.report({'INFO'}, "現在の数値でコードをコピーしました!")
except Exception as e:
self.report({'ERROR'}, f"Failed to parse code: {str(e)}")
return {'CANCELLED'}
return {'FINISHED'}
class OVERLAY_OT_set_grid_scale(Operator):
bl_idname = f"{PREFIX}_overlay.set_grid_scale"
bl_label = "Set Grid Scale"
scale_value: FloatProperty()
def execute(self, context):
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D': area.spaces.active.overlay.grid_scale = self.scale_value
return {'FINISHED'}
class SUN_OT_Create(Operator):
bl_idname = f"{PREFIX}.create_sun"; bl_label = "太陽を作成"
def execute(self, context):
get_or_create_sun()
self.report({'INFO'}, "太陽を作成しました。"); return {'FINISHED'}
class SUN_OT_Reset(Operator):
bl_idname = f"{PREFIX}.reset_sun"; bl_label = "太陽の設定を初期値にリセット"
def execute(self, context):
props = context.scene.sun_settings_props
props.sun_control_mode, props.sun_target_location = 'ANGLE', (0.0, 0.0, 0.0)
props.sun_rotation = (radians(45.0), radians(0.0), radians(45.0))
props.sun_location, props.sun_strength = (0.0, 0.0, 10.0), 2.5
update_sun(props, context)
self.report({'INFO'}, "太陽の設定をリセットしました。"); return {'FINISHED'}
class RemoveAllPanels(Operator):
bl_idname = f"{PREFIX}_wm.remove_all_panels"; bl_label = PANEL_LABELS["REMOVE"]
def execute(self, context): unregister(); return {'FINISHED'}
# --- 透視投影用のオペレーター ---
class PERSP_OT_GetCurrentView(Operator):
bl_idname = f"{PREFIX}_persp.get_current_view"
bl_label = "Get Current View & Update"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, "persp_view_props", None)
r3d = context.space_data.region_3d if context.space_data else None
if not props or not r3d: return {'CANCELLED'}
target_pos = Vector(r3d.view_location)
actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
max_val = max(abs(actual_cam_pos.x), abs(actual_cam_pos.y), abs(actual_cam_pos.z))
if max_val > props.slider_limit: props.slider_limit = max_val + 50.0
props.view_pos = actual_cam_pos
return {'FINISHED'}
class PERSP_OT_ResetView(Operator):
bl_idname = f"{PREFIX}_persp.reset_view"
bl_label = VIEW_RESET_BTN_TEXT
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, "persp_view_props", None)
if props:
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.region_3d.view_location = (0.0, 0.0, 0.0)
props.view_pos = VIEW_POS_INIT
return {'FINISHED'}
class PERSP_OT_CenterSelected(Operator):
bl_idname = f"{PREFIX}_persp.center_selected"
bl_label = "Center Selected Object"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.view3d.view_selected()
r3d = context.space_data.region_3d if context.space_data else None
props = getattr(context.scene, "persp_view_props", None)
if r3d and props:
target_pos = Vector(r3d.view_location)
props.view_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
return {'FINISHED'}
class PERSP_OT_CopyActualViewPos(Operator):
bl_idname = f"{PREFIX}_persp.copy_actual_pos"
bl_label = "Copy Position Only"
def execute(self, context):
r3d = context.space_data.region_3d if context.space_data else None
if not r3d: return {'CANCELLED'}
target_pos = Vector(r3d.view_location)
p = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
context.window_manager.clipboard = f"Actual View Pos: ({p.x:.4f}, {p.y:.4f}, {p.z:.4f})"
self.report({'INFO'}, "視座位置をコピーしました")
return {'FINISHED'}
class PERSP_OT_CopyAngles(Operator):
bl_idname = f"{PREFIX}_persp.copy_angles"
bl_label = "Copy Full Info (Pos & Angles)"
def execute(self, context):
props = getattr(context.scene, "persp_view_props", None)
r3d = context.space_data.region_3d if context.space_data else None
if not r3d or not props: return {'CANCELLED'}
target_pos = Vector(r3d.view_location)
actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
vec = target_pos - actual_cam_pos
length = vec.length
if length < 0.0001: return {'CANCELLED'}
ang_x = math.degrees(math.acos(vec.x / length))
ang_y = math.degrees(math.acos(vec.y / length))
ang_z = math.degrees(math.acos(vec.z / length))
pl_x = math.degrees(math.asin(vec.x / length))
pl_y = math.degrees(math.asin(vec.y / length))
pl_z = math.degrees(math.asin(vec.z / length))
info_text = (
f"--- View Direction Info ---\n"
f"[ Actual 3D View Status ]\n"
f"Actual View Pos : ({actual_cam_pos.x:.4f}, {actual_cam_pos.y:.4f}, {actual_cam_pos.z:.4f})\n"
f"Target Pos : ({target_pos.x:.4f}, {target_pos.y:.4f}, {target_pos.z:.4f})\n"
f"Distance : {length:.4f}\n\n"
f"[ Direction Angles (軸そのものとの角度 0〜180°) ]\n"
f"Angle from X Axis : {ang_x:.2f} deg\n"
f"Angle from Y Axis : {ang_y:.2f} deg\n"
f"Angle from Z Axis : {ang_z:.2f} deg\n\n"
f"[ Planar Angles (直感的な傾き・ズレ角 -90〜90°) ]\n"
f"X (横のズレ角) : {pl_x:.2f} deg\n"
f"Y (前後の傾き) : {pl_y:.2f} deg\n"
f"Z (仰角・俯角) : {pl_z:.2f} deg\n"
)
context.window_manager.clipboard = info_text
self.report({'INFO'}, "情報全体をクリップボードにコピーしました")
return {'FINISHED'}
# ------------------------------------------------------------------------
# Panels
# ------------------------------------------------------------------------
class BasePanel(Panel):
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME
class VIEW3D_PT_CopyPanel(BasePanel):
bl_label = PANEL_LABELS["COPY"]; bl_idname = COPY_PANEL_IDNAME; bl_order = 0
def draw(self, context):
layout = self.layout
row = layout.row()
row.scale_y = 1.2
row.operator(f"{PREFIX}_wm.copy_script", icon='COPY_ID', text="最新数値付きコードコピー")
row_view = layout.row()
row_view.operator(f"{PREFIX}_wm.view_center_front", icon='VIEWZOOM', text="0,0,0 を正面(Y-)から見る")
class VIEW3D_PT_PerspControlPanel(BasePanel):
bl_label = PANEL_LABELS["PERSP"]; bl_idname = PERSP_PANEL_IDNAME; bl_order = 1
def draw(self, context):
layout = self.layout
props = getattr(context.scene, "persp_view_props", None)
if not props: return
box = layout.box()
box.label(text="Perspective Viewpoint", icon='VIEW_CAMERA')
box.prop(props, "slider_limit", text="Range Limit (+/-)")
col = box.column(align=True)
col.prop(props, "view_pos", text="X", index=0)
col.prop(props, "view_pos", text="Y", index=1)
col.prop(props, "view_pos", text="Z", index=2)
box.separator()
box.operator(f"{PREFIX}_persp.get_current_view", icon='RESTRICT_VIEW_OFF')
box.operator(f"{PREFIX}_persp.reset_view", icon='LOOP_BACK')
layout.operator(f"{PREFIX}_persp.center_selected", icon='VIEWZOOM')
layout.separator()
box_info = layout.box()
box_info.label(text="Actual View Status", icon='INFO')
r3d = context.space_data.region_3d if context.space_data else None
if r3d:
target_pos = Vector(r3d.view_location)
actual_cam_pos = target_pos + r3d.view_rotation @ Vector((0.0, 0.0, r3d.view_distance))
vec = target_pos - actual_cam_pos
length = vec.length
col_pos = box_info.column(align=True)
col_pos.label(text="[ Actual Position ]", icon='VIEW_CAMERA')
col_pos.label(text=f" X: {actual_cam_pos.x:.4f}")
col_pos.label(text=f" Y: {actual_cam_pos.y:.4f}")
col_pos.label(text=f" Z: {actual_cam_pos.z:.4f}")
col_pos.label(text=f" Distance: {length:.4f}")
box_info.operator(f"{PREFIX}_persp.copy_actual_pos", icon='COPYDOWN')
box_info.separator()
col_ang = box_info.column(align=True)
if length > 0.0001:
a_x = math.degrees(math.acos(vec.x / length))
a_y = math.degrees(math.acos(vec.y / length))
a_z = math.degrees(math.acos(vec.z / length))
p_x = math.degrees(math.asin(vec.x / length))
p_y = math.degrees(math.asin(vec.y / length))
p_z = math.degrees(math.asin(vec.z / length))
col_ang.label(text="[ Direction Angles (軸との角度) ]", icon='ORIENTATION_GLOBAL')
col_ang.label(text=f" X: {a_x:.2f}°")
col_ang.label(text=f" Y: {a_y:.2f}°")
col_ang.label(text=f" Z: {a_z:.2f}°")
col_ang.separator()
col_ang.label(text="[ Planar Angles (直感的な傾き) ]", icon='DRIVER_ROTATIONAL_DIFFERENCE')
col_ang.label(text=f" X (ズレ角): {p_x:.2f}°")
col_ang.label(text=f" Y (ズレ角): {p_y:.2f}°")
col_ang.label(text=f" Z (仰俯角): {p_z:.2f}°")
else:
col_ang.label(text=" Target is too close")
box_info.separator()
box_info.operator(f"{PREFIX}_persp.copy_angles", icon='COPYDOWN')
else:
box_info.label(text="Please use in 3D View")
class VIEW3D_PT_OverlayPanel(BasePanel):
bl_label = PANEL_LABELS["OVERLAY"]; bl_idname = OVERLAY_PANEL_IDNAME; bl_order = 2
def draw(self, context):
layout, props = self.layout, context.scene.viewport_color_props
if context.space_data.type == 'VIEW_3D':
layout.prop(context.space_data.overlay, "show_floor", text="Floor")
layout.prop(props, "custom_grid_scale", text="Scale 数値入力")
row = layout.row(align=True)
row.operator(f"{PREFIX}_overlay.set_grid_scale", text="入力値").scale_value = props.custom_grid_scale
row.operator(f"{PREFIX}_overlay.set_grid_scale", text="10.0").scale_value = 10.0
row.operator(f"{PREFIX}_overlay.set_grid_scale", text="100.0").scale_value = 100.0
else:
layout.label(text="3D Viewport is required.")
class VIEW3D_PT_solid_background_panel(BasePanel):
bl_label = PANEL_LABELS["BACKGROUND"]; bl_idname = BG_PANEL_IDNAME_1; bl_order = 3
def draw(self, context):
layout, props = self.layout, context.scene.viewport_color_props
layout.prop(props, "preset", text="Background Preset")
layout.prop(props, "background_type", expand=True)
layout.prop(props, "custom_gradient_high", text="Color High" if props.background_type != 'SINGLE_COLOR' else "Color")
if props.background_type != 'SINGLE_COLOR': layout.prop(props, "custom_gradient_low")
layout.prop(props, "reverse_gradient", text="Reverse Gradient")
class VIEW3D_PT_HeaderPanel(BasePanel):
bl_label = PANEL_LABELS["HEADER"]; bl_idname = HEADER_PANEL_IDNAME; bl_order = 4
def draw(self, context):
layout, props = self.layout, context.scene.viewport_color_props
layout.prop(props, "header_preset", text="Header Preset")
layout.prop(props, "header_color")
class VIEW3D_PT_RenderPanel(BasePanel):
bl_label = PANEL_LABELS["RENDER"]; bl_idname = RENDER_PANEL_IDNAME; bl_order = 5
def draw(self, context):
layout, props = self.layout, context.scene.viewport_color_props
layout.prop(props, "render_preset")
layout.prop(props, "render_color")
layout.prop(props, "render_environment_strength")
class VIEW3D_PT_SunPanel(BasePanel):
bl_label = PANEL_LABELS["SUN"]; bl_idname = SUN_PANEL_IDNAME; bl_order = 6
def draw(self, context):
layout, props = self.layout, context.scene.sun_settings_props
row = layout.row(align=True)
row.operator(f"{PREFIX}.create_sun", text="太陽作成ボタン")
row.operator(f"{PREFIX}.reset_sun", icon='FILE_REFRESH', text="")
layout.separator()
layout.prop(props, "sun_control_mode", expand=True)
if props.sun_control_mode == 'ANGLE': layout.prop(props, "sun_rotation")
else: layout.prop(props, "sun_target_location")
layout.prop(props, "sun_location"); layout.prop(props, "sun_strength")
class VIEW3D_PT_GridPanel(BasePanel):
bl_label = PANEL_LABELS["GRID"]; bl_idname = GRID_PANEL_IDNAME; bl_order = 7
def draw(self, context):
layout, props = self.layout, context.scene.viewport_color_props
layout.prop(props, "grid_preset")
layout.prop(props, "grid_color")
class VIEW3D_PT_WirePanel(BasePanel):
bl_label = PANEL_LABELS["WIRE"]; bl_idname = WIRE_PANEL_IDNAME; bl_order = 8
def draw(self, context):
layout, props = self.layout, context.scene.viewport_color_props
layout.prop(props, "wire_preset")
layout.prop(props, "wire_color")
class VIEW3D_PT_CameraPanel(BasePanel):
bl_label = PANEL_LABELS["CAMERA"]; bl_idname = CAMERA_PANEL_IDNAME; bl_order = 9
def draw(self, context):
layout, props = self.layout, context.scene.viewport_color_props
layout.prop(props, "camera_preset")
layout.prop(props, "camera_color")
class VIEW3D_PT_OutlinerPanel(BasePanel):
bl_label = PANEL_LABELS["OUTLINER"]; bl_idname = OUTLINER_PANEL_IDNAME; bl_order = 10
def draw(self, context):
layout, props = self.layout, context.scene.viewport_color_props
layout.prop(props, "outliner_preset")
layout.prop(props, "outliner_header_color")
layout.prop(props, "outliner_background_color")
class VIEW3D_PT_TextEditorPanel(BasePanel):
bl_label = PANEL_LABELS["TEXT_EDITOR"]; bl_idname = TEXT_EDITOR_PANEL_IDNAME; bl_order = 11
def draw(self, context):
layout, props = self.layout, context.scene.viewport_color_props
layout.prop(props, "text_editor_preset")
layout.prop(props, "text_editor_header_color")
layout.prop(props, "text_editor_background_color")
class VIEW3D_PT_LinkPanel(BasePanel):
bl_label = PANEL_LABELS["LINK"]; bl_idname = LINK_PANEL_IDNAME; bl_order = 12
def draw(self, context):
layout = self.layout
layout.operator("wm.url_open", text="進化版 画面中央 透視投影視座位置 20260319bb", icon='URL').url = "<https://www.notion.so/20260319bb-327f5dacaf43801e8e37ce489dc1d593>"
layout.operator("wm.url_open", text="5520 背景色 変更 20260316版", icon='URL').url = "<https://www.notion.so/5520-20260316-314f5dacaf4380da9be4c05551d40710>"
class VIEW3D_PT_RemovePanel(BasePanel):
bl_label = PANEL_LABELS["REMOVE"]; bl_idname = REMOVE_PANEL_IDNAME; bl_order = 13
def draw(self, context):
self.layout.operator(f"{PREFIX}_wm.remove_all_panels", text=PANEL_LABELS["REMOVE"])
# ------------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------------
classes =[
SunSettingsProperties, ViewportColorProperties, PerspViewProperties,
OT_CopyFullScript, OT_ViewCenterFront,
OVERLAY_OT_set_grid_scale,
SUN_OT_Create, SUN_OT_Reset,
PERSP_OT_GetCurrentView, PERSP_OT_ResetView, PERSP_OT_CenterSelected, PERSP_OT_CopyActualViewPos, PERSP_OT_CopyAngles,
RemoveAllPanels,
VIEW3D_PT_CopyPanel, VIEW3D_PT_PerspControlPanel,
VIEW3D_PT_OverlayPanel, VIEW3D_PT_solid_background_panel,
VIEW3D_PT_HeaderPanel, VIEW3D_PT_RenderPanel, VIEW3D_PT_SunPanel, VIEW3D_PT_GridPanel,
VIEW3D_PT_WirePanel, VIEW3D_PT_CameraPanel, VIEW3D_PT_OutlinerPanel, VIEW3D_PT_TextEditorPanel,
VIEW3D_PT_LinkPanel, VIEW3D_PT_RemovePanel
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.viewport_color_props = PointerProperty(type=ViewportColorProperties)
bpy.types.Scene.sun_settings_props = PointerProperty(type=SunSettingsProperties)
bpy.types.Scene.persp_view_props = PointerProperty(type=PerspViewProperties)
def apply_initial_settings():
if bpy.context.scene and hasattr(bpy.context.scene, 'viewport_color_props'):
props = bpy.context.scene.viewport_color_props
sun_props = bpy.context.scene.sun_settings_props
persp_props = bpy.context.scene.persp_view_props
for key, val in CURRENT_DEFAULTS.items():
if hasattr(props, key): setattr(props, key, val)
elif hasattr(sun_props, key): setattr(sun_props, key, val)
elif hasattr(persp_props, key): setattr(persp_props, key, val)
update_background_color(props, bpy.context)
update_header_color(props, bpy.context)
update_render_color(props, bpy.context)
update_grid_color(props, bpy.context)
update_wire_color(props, bpy.context)
update_camera_color(props, bpy.context)
update_outliner_color(props, bpy.context)
update_text_editor_color(props, bpy.context)
update_sun(sun_props, bpy.context)
bpy.app.timers.register(apply_initial_settings, first_interval=0.1)
if not bpy.app.timers.is_registered(view_sync_timer):
bpy.app.timers.register(view_sync_timer)
def unregister():
if hasattr(bpy.types.Scene, 'viewport_color_props'): del bpy.types.Scene.viewport_color_props
if hasattr(bpy.types.Scene, 'sun_settings_props'): del bpy.types.Scene.sun_settings_props
if hasattr(bpy.types.Scene, 'persp_view_props'): del bpy.types.Scene.persp_view_props
for cls in reversed(classes):
try: bpy.utils.unregister_class(cls)
except RuntimeError: pass
if bpy.app.timers.is_registered(view_sync_timer):
bpy.app.timers.unregister(view_sync_timer)
if __name__ == "__main__":
register()'''
# ==============================================================================
# 【 内包する4つ目のスクリプト (Fixed Camera & World) 】
# ==============================================================================
CAMERA_SCRIPT_CONTENT = r'''import bpy
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
from datetime import datetime
# --- ユニークID生成 ---
START_TIMESTAMP = datetime.now().strftime("%Y%m%d%H%M%S")
PREFIX = f"cam_kotei{START_TIMESTAMP}"
# --- bl_info ---
bl_info = {
"name": "zionad v100 [Fixed Camera & World]",
"author": "zionadchat",
"version": (35, 0, 5), # バージョンアップ
"blender": (4, 1, 0),
"location": "View3D > Sidebar > zionad Control",
"description": "カメラの位置固定、向き(YPR)、レンズ制御に加え、ワールド(HDRI/背景)設定機能を提供します。",
"category": " v100[ 固定 Camera ] ",
}
# ======================================================================
# --- ユーザー設定 / Parameters to Customize ---
# ======================================================================
ADDON_CATEGORY_NAME = bl_info["category"]
# --- HDRI画像ファイルのフルパスリスト ---
# ▼▼▼【変更点】ご指定のHDRIパスをリストの2番目に追加しました ▼▼▼
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",
]
# --- ワイヤーフレームの色プリセット ---
# 形式: ("ID", "ラベル", "説明", (R, G, B))
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)),
]
# --- グリッドの色プリセット ---
# 形式: ("ID", "ラベル", "説明", (R, G, B, A))
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)),
]
# --- 専用カメラのコレクション名とオブジェクト名 ---
CAMERA_COLLECTION_NAME = "Cam"
DEDICATED_CAMERA_NAME = "Fixed_Cam"
# ======================================================================
# --- 定数定義 / Constants ---
# ======================================================================
SENSOR_WIDTH = 36.0
FOV_PRESETS =[1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]
CAMERA_COLOR_PRESETS =[("CYAN", "Cyan", "水色", (0.0, 1.0, 1.0)), ("Cam 4.4.0", "Cam 4.4.0", "Blenderデフォルト色", (0.0, 0.0, 0.0)), ("YELLOW", "Yellow", "黄色", (1.0, 1.0, 0.0)), ("PURPLE", "Purple", "紫色", (0.5, 0.0, 0.5)),]
# --- リンクパネル用データ ---
ADDON_LINKS = ({"label": "カメラ 固定 Git 管理 20250711", "url":"<https://memo2017.hatenablog.com/entry/2025/07/11/131157>"},)
NEW_DOC_LINKS =[
{"label": "完成品 目次", "url": "<https://mokuji000zionad.hatenablog.com/entry/2025/05/30/135936>"},
]
DOC_LINKS =[
{"label": "812 地球儀 経度 緯度でのコントロール 20250302", "url": "<https://sortphotos2025.hatenablog.jp/entry/2025/03/02/211757>"},
{"label": "アドオン目次 from 20250227", "url": "<https://sortphotos2025.hatenablog.jp/entry/2025/02/27/201251>"},
{"label": "addon 目次整理 from 20250116", "url": "<https://blenderzionad.hatenablog.com/entry/2025/01/17/002322>"},
]
SOCIAL_LINKS =[
{"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
{"label": "Posfie zionad2022", "url": "<https://posfie.com/t/zionad2022>"},
{"label": "X (Twitter) zionadchat", "url": "<https://x.com/zionadchat>"},
{"label": "単純トリック 2025 open", "url": "<https://www.notion.so/2025-open-221b3deba7a2809a85a9f5ab5600ab06>"},
]
# --- パネルIDと順序 ---
PANEL_IDS = {
"SETUP": f"{PREFIX}_PT_setup", "POSITION": f"{PREFIX}_PT_position", "AIMING": f"{PREFIX}_PT_aiming",
"LENS": f"{PREFIX}_PT_lens", "CAMERA_DISPLAY": f"{PREFIX}_PT_camera_display", "WORLD_CONTROL": f"{PREFIX}_PT_world_control",
"INFO": f"{PREFIX}_PT_info", "GRID": f"{PREFIX}_PT_grid_panel", "WIRE": f"{PREFIX}_PT_wire_panel",
"LINKS": f"{PREFIX}_PT_links",
"LINKS_NEWDOC": f"{PREFIX}_PT_links_newdoc", "LINKS_DOC": f"{PREFIX}_PT_links_doc", "LINKS_SOCIAL": f"{PREFIX}_PT_links_social",
"REMOVE": f"{PREFIX}_PT_remove",
}
PANEL_ORDER = {PANEL_IDS["SETUP"]: 0, PANEL_IDS["POSITION"]: 1, PANEL_IDS["AIMING"]: 2, PANEL_IDS["LENS"]: 3, PANEL_IDS["CAMERA_DISPLAY"]: 4, PANEL_IDS["WORLD_CONTROL"]: 5, PANEL_IDS["INFO"]: 6, PANEL_IDS["GRID"]: 89, PANEL_IDS["WIRE"]: 90, PANEL_IDS["LINKS"]: 100, PANEL_IDS["REMOVE"]: 200,}
# --- グローバル状態管理 ---
_is_updating_by_addon = False; _update_timer = None
def reset_update_flag(): global _is_updating_by_addon, _update_timer; _is_updating_by_addon = False; _update_timer = None; return None
def schedule_update_flag_reset():
global _update_timer
if _update_timer and bpy.app.timers.is_registered(reset_update_flag): bpy.app.timers.unregister(reset_update_flag)
bpy.app.timers.register(reset_update_flag, first_interval=0.01)
# --- World Tools ヘルパー関数 ---
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 RuntimeError as e: print(f"Error loading image: {e}"); return False
print(f"File not found: {filepath}"); return False
def update_viewport(context):
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D': space.shading.type = 'RENDERED'; 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)
# --- プロパティグループ ---
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))
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))
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()
class SurfaceCameraProperties(PropertyGroup):
camera_obj: PointerProperty(name="操作カメラ", type=bpy.types.Object, poll=lambda self, obj: obj.type == 'CAMERA', update=lambda s,c: update_surface_camera(s,c))
fixed_location: FloatVectorProperty(name="固定位置", default=(0.0, -10.0, 0.0), subtype='XYZ', update=lambda s,c: update_surface_camera(s,c))
target_location: FloatVectorProperty(name="固定注視点", default=(0, 0, 0), subtype='XYZ', update=lambda s,c: update_surface_camera(s,c))
offset_yaw: FloatProperty(name="Yaw", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c)); offset_pitch: FloatProperty(name="Pitch", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c)); offset_roll: FloatProperty(name="Roll", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
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=lambda s,c: update_surface_camera(s,c))
clip_start: FloatProperty(name="クリップ開始", default=0.1, min=0.001, update=lambda s,c: update_surface_camera(s,c)); clip_end: FloatProperty(name="クリップ終了", default=1000.0, min=1.0, update=lambda s,c: update_surface_camera(s,c))
info_precision: EnumProperty(name="桁数", items=[('1', '1', ''), ('2', '2', ''), ('3', '3', '')], default='1', update=lambda s,c: update_info_panel_text(s,c))
info_focal_length: StringProperty(name="焦点距離"); info_horizontal_fov: StringProperty(name="水平視野角"); info_camera_location: StringProperty(name="カメラ位置"); info_target_location: StringProperty(name="注視点位置"); info_distance_to_target: StringProperty(name="注視点までの距離"); info_clip_setting: StringProperty(name="クリップ範囲"); info_viewable_width: StringProperty(name="注視点での横幅")
camera_color: FloatVectorProperty(name="カメラカラー", subtype='COLOR', size=3, min=0.0, max=1.0, default=(0.0, 1.0, 1.0))
camera_preset: EnumProperty(name="カメラプリセット", items=[(p[0], p[1], p[2]) for p in CAMERA_COLOR_PRESETS], default="CYAN", update=lambda self, context: SFC_OT_ApplyCameraColor.update_preset(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 (ZeroDivisionError, ValueError): 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 (ZeroDivisionError, ValueError): return 50.0
def get_target_location(props):
return mathutils.Vector(props.target_location)
def update_object_transform(obj, props):
location = mathutils.Vector(props.fixed_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.location = location; obj.rotation_euler = final_quat.to_euler('XYZ')
def update_surface_camera(self, context):
global _is_updating_by_addon
if _is_updating_by_addon: return
_is_updating_by_addon = True
try:
props, camera_obj = context.scene.surface_camera_properties, context.scene.surface_camera_properties.camera_obj
if props.is_updating_settings or not camera_obj: update_info_panel_text(props, context); return
cam_data = camera_obj.data
if cam_data: cam_data.sensor_fit, cam_data.lens_unit, cam_data.lens, cam_data.clip_start, cam_data.clip_end = 'HORIZONTAL', 'MILLIMETERS', props.lens_focal_length, props.clip_start, props.clip_end
update_object_transform(camera_obj, props); update_info_panel_text(props, context)
finally: schedule_update_flag_reset()
def update_info_panel_text(props, context):
if not hasattr(context, 'scene') or not props: return
precision, fmt = int(props.info_precision), f".{props.info_precision}f"
camera_location, target_location = mathutils.Vector(props.fixed_location), get_target_location(props)
props.info_camera_location = f"({camera_location.x:{fmt}}, {camera_location.y:{fmt}}, {camera_location.z:{fmt}})"; current_fov = calculate_horizontal_fov(props.lens_focal_length); props.info_horizontal_fov = f"{current_fov:{fmt}} °"; props.info_focal_length = f"{props.lens_focal_length:{fmt}} mm"
props.info_target_location = f"({target_location.x:{fmt}}, {target_location.y:{fmt}}, {target_location.z:{fmt}})"; distance = (target_location - camera_location).length; props.info_distance_to_target = f"{distance:{fmt}}"
if distance > 0 and current_fov > 0: props.info_viewable_width = f"{2 * distance * math.tan(math.radians(current_fov) / 2):{fmt}}"
else: props.info_viewable_width = "N/A"
props.info_clip_setting = f"{props.clip_start:{fmt}} - {props.clip_end:{fmt}}"
def sync_ui_from_manual_transform(props, obj, context):
global _is_updating_by_addon
if _is_updating_by_addon: return
_is_updating_by_addon = True
try:
props.fixed_location = obj.location
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, final_quat = direction.to_track_quat('-Z', 'Y'), obj.matrix_world.to_quaternion()
offset_quat, offset_euler = base_track_quat.inverted() @ final_quat, offset_quat.to_euler('XYZ')
props.offset_pitch, props.offset_yaw, props.offset_roll = offset_euler.x, offset_euler.y, offset_euler.z
finally: _is_updating_by_addon = False
update_info_panel_text(props, context)
@bpy.app.handlers.persistent
def on_depsgraph_update(scene, depsgraph):
if _is_updating_by_addon: return
context = bpy.context
if not (hasattr(context, 'scene') and context.scene): return
sfc_props = context.scene.surface_camera_properties
for update in depsgraph.updates:
if not update.is_updated_transform: continue
obj_id = update.id.original
if sfc_props.camera_obj and obj_id == sfc_props.camera_obj: sync_ui_from_manual_transform(sfc_props, sfc_props.camera_obj, context); return
# --- オペレーター ---
class SFC_OT_ApplyCameraColor(Operator):
bl_idname = f"{PREFIX}.apply_camera_color"; bl_label = "カメラカラー適用"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context): context.preferences.themes[0].view_3d.camera = context.scene.surface_camera_properties.camera_color; return {'FINISHED'}
@staticmethod
def update_preset(self, context): props = context.scene.surface_camera_properties; props.camera_color = next((p[3] for p in CAMERA_COLOR_PRESETS if p[0] == props.camera_preset), props.camera_color); getattr(bpy.ops, f"{PREFIX}.apply_camera_color")()
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'}, f"グリッドの色をコピーしました: {context.window_manager.clipboard}"); return {'FINISHED'}
class SFC_OT_CreateDedicatedCamera(Operator):
bl_idname = f"{PREFIX}.create_dedicated_camera"; bl_label = "専用カメラ作成"
def execute(self, context):
if DEDICATED_CAMERA_NAME not in bpy.data.objects:
cam_data = bpy.data.cameras.new(name=DEDICATED_CAMERA_NAME); cam_obj = bpy.data.objects.new(DEDICATED_CAMERA_NAME, cam_data)
cam_collection = bpy.data.collections.get(CAMERA_COLLECTION_NAME) or bpy.data.collections.new(CAMERA_COLLECTION_NAME)
if CAMERA_COLLECTION_NAME not in context.scene.collection.children: context.scene.collection.children.link(cam_collection)
cam_collection.objects.link(cam_obj)
if cam_obj.name in context.scene.collection.objects: context.scene.collection.objects.unlink(cam_obj)
else: cam_obj = bpy.data.objects[DEDICATED_CAMERA_NAME]
props = context.scene.surface_camera_properties; props.camera_obj = cam_obj; props.is_updating_settings = True
for key in props.bl_rna.properties.keys():
if key not in['camera_obj', 'bl_rna', 'is_updating_settings'] and not props.bl_rna.properties[key].is_readonly: props.property_unset(key)
props.is_updating_settings = False; update_surface_camera(props, context); self.report({'INFO'}, f"カメラ '{DEDICATED_CAMERA_NAME}' を作成/選択し、初期化しました。"); return {'FINISHED'}
class SFC_OT_SyncWithCamera(Operator):
bl_idname = f"{PREFIX}.sync_with_camera"; bl_label = "UIを同期"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props, cam_obj = context.scene.surface_camera_properties, context.scene.surface_camera_properties.camera_obj
if not cam_obj or cam_obj.type != 'CAMERA': self.report({'WARNING'}, "有効なカメラが選択されていません。"); return {'CANCELLED'}
context.scene.camera = cam_obj; cam_data = cam_obj.data; props.is_updating_settings = True
props.lens_focal_length, props.clip_start, props.clip_end = cam_data.lens, cam_data.clip_start, cam_data.clip_end
props.is_updating_settings = False; sync_ui_from_manual_transform(props, cam_obj, context); self.report({'INFO'}, f"カメラ '{cam_obj.name}' の設定をUIに読み込みました。"); return {'FINISHED'}
class SFC_OT_UnlinkObject(Operator):
bl_idname = f"{PREFIX}.unlink_object"; bl_label = "解除"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props, update_func, obj_prop = context.scene.surface_camera_properties, update_surface_camera, 'camera_obj'
if getattr(props, obj_prop): self.report({'INFO'}, f"'{getattr(props, obj_prop).name}' との関連付けを解除しました。"); setattr(props, obj_prop, None)
props.is_updating_settings = True
for key in props.bl_rna.properties.keys():
if key not in ['bl_rna', 'is_updating_settings', 'camera_obj'] and not props.bl_rna.properties[key].is_readonly: props.property_unset(key)
props.is_updating_settings = False; update_func(props, context); 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, update_func = context.scene.surface_camera_properties, update_surface_camera
prop_groups = {"location":["fixed_location"],"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 group_props in prop_groups.values(): props_to_reset.update(group_props)
else:
for name in target_names: props_to_reset.update(prop_groups.get(name,[]))
props.is_updating_settings = True
for prop_name in props_to_reset:
if hasattr(props, prop_name): props.property_unset(prop_name)
props.is_updating_settings = False; update_func(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_CopyAllInfo(Operator):
bl_idname = f"{PREFIX}.copy_all_info"; bl_label = "全情報コピー"
def execute(self, context):
props=context.scene.surface_camera_properties; context.window_manager.clipboard = (f"カメラ情報 ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})\n----------------------------------------\n" f"焦点距離: {props.info_focal_length}\n水平視野角: {props.info_horizontal_fov}\nカメラ位置: {props.info_camera_location}\n" f"注視点: {props.info_target_location}\n注視点までの距離: {props.info_distance_to_target}\n注視点での横幅: {props.info_viewable_width}\n" f"クリップ範囲: {props.info_clip_setting}\n----------------------------------------"); self.report({'INFO'}, "全情報をクリップボードにコピーしました。"); 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); self.report({'INFO'}, f"アドオン '{bl_info.get('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}),'; self.report({'INFO'}, f"ワイアの色をコピーしました: {context.window_manager.clipboard}"); return {'FINISHED'}
class SFC_OT_SetFixedLocationFromView(Operator):
bl_idname = f"{PREFIX}.set_fixed_location_from_view"; bl_label = "現在のカメラ位置をセット"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props, cam_obj = context.scene.surface_camera_properties, context.scene.surface_camera_properties.camera_obj
if not cam_obj: self.report({'WARNING'}, "操作対象のカメラが選択されていません。"); return {'CANCELLED'}
props.fixed_location = cam_obj.location; self.report({'INFO'}, f"固定位置を {tuple(round(c, 2) for c in cam_obj.location)} に設定しました。"); 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)
self.report({'INFO'}, f"Loaded: {os.path.basename(HDRI_PATHS[self.hdri_index])}")
else: self.report({'ERROR'}, "Invalid HDRI index")
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; box = layout.box(); col = box.column(); col.prop(props, "camera_obj", text="カメラ")
if props.camera_obj: row = col.row(align=True); row.operator(f"{PREFIX}.sync_with_camera", icon='UV_SYNC_SELECT'); row.operator(f"{PREFIX}.unlink_object", icon='X')
else: col.label(text="カメラを選択してください", icon='ERROR'); col.operator(f"{PREFIX}.create_dedicated_camera", text=f"'{DEDICATED_CAMERA_NAME}' を作成/選択", icon='ADD')
col.separator(); box.prop(props, "camera_preset", text="色プリセット"); box.prop(props, "camera_color", text="カラー"); box.operator(f"{PREFIX}.apply_camera_color", text="ビューポート色を適用")
class SFC_PT_PositionPanel(Panel):
bl_label = "2. カメラ位置 (固定)"; bl_idname = PANEL_IDS["POSITION"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["POSITION"]]
def draw(self, context):
layout = self.layout; props = context.scene.surface_camera_properties; 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 = "location"; op.prop_group_name = "camera"
col.prop(props, "fixed_location", text=""); col.operator(f"{PREFIX}.set_fixed_location_from_view", icon='OBJECT_ORIGIN')
class SFC_PT_AimingPanel(Panel):
bl_label = "3. カメラ視線制御"; 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"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout, props = self.layout, context.scene.surface_camera_properties
box_aim = layout.box(); col_aim = box_aim.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_offset = layout.box(); col_offset = box_offset.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_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; 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:
box_display.label(text="3D Viewport only", icon='INFO')
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:
col = layout.column(align=True)
if not world: col.label(text="No World in Scene", icon='ERROR'); col.operator("world.new", text="Create New World")
else: col.label(text="Enable Nodes in World"); col.prop(world, "use_nodes", text="Use 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); col_list.label(text="HDRI Presets:")
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"); mapping_node = find_node(nodes, 'ShaderNodeMapping', 'Mapping')
if mapping_node:
box_transform = box_env.box(); box_transform.label(text="Transform", icon='OBJECT_DATA'); col = box_transform.column(align=True)
for prop_name in['Location', 'Rotation', 'Scale']:
row = col.row(align=True); split = row.split(factor=0.8, align=True); split.prop(mapping_node.inputs[prop_name], "default_value", text=prop_name)
op = split.operator(f"{PREFIX}.reset_transform", text="", icon='FILE_REFRESH'); op.property_to_reset = prop_name
elif props.background_mode == 'SKY':
box_sky = layout.box(); box_sky.label(text="Sky Texture", icon='WORLD_DATA'); sky_node = find_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture')
if sky_node:
col_sky = box_sky.column(align=True); col_sky.prop(sky_node, "sky_type", text="Sky Type")
if sky_node.sky_type == 'NISHITA':
if hasattr(sky_node, 'sun_elevation'): col_sky.prop(sky_node, "sun_elevation", text="Sun Elevation")
if hasattr(sky_node, 'sun_rotation'): col_sky.prop(sky_node, "sun_rotation", text="Sun Rotation")
if hasattr(sky_node, 'altitude'): col_sky.prop(sky_node, "altitude", text="Altitude")
if hasattr(sky_node, 'air_density'): col_sky.prop(sky_node, "air_density", text="Air Density")
if hasattr(sky_node, 'dust_density'): col_sky.prop(sky_node, "dust_density", text="Dust Density")
if hasattr(sky_node, 'ozone_density'): col_sky.prop(sky_node, "ozone_density", text="Ozone Density")
elif sky_node.sky_type in {'PREETHAM', 'HOSEK_WILKIE'}:
if hasattr(sky_node, 'turbidity'): col_sky.prop(sky_node, "turbidity", text="Turbidity")
if hasattr(sky_node, 'ground_albedo'): col_sky.prop(sky_node, "ground_albedo", text="Ground Albedo")
class SFC_PT_InfoPanel(Panel):
bl_label = "カメラ情報"; bl_idname = PANEL_IDS["INFO"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["INFO"]]; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): layout, props = self.layout, context.scene.surface_camera_properties; col = layout.column(align=True); row = col.row(align=True); row.label(text="焦点距離:"); row.label(text=props.info_focal_length); row = col.row(align=True); row.label(text="水平視野角:"); row.label(text=props.info_horizontal_fov); col.separator(); row = col.row(align=True); row.label(text="カメラ位置:"); row.label(text=props.info_camera_location); row = col.row(align=True); row.label(text="注視点:"); row.label(text=props.info_target_location); row = col.row(align=True); row.label(text="注視点までの距離:"); row.label(text=props.info_distance_to_target); row = col.row(align=True); row.label(text="注視点での横幅:"); row.label(text=props.info_viewable_width); col.separator(); row = col.row(align=True); row.label(text="クリップ範囲:"); row.label(text=props.info_clip_setting); col.separator(); col.prop(props, "info_precision", text="表示桁数"); col.operator(f"{PREFIX}.copy_all_info", text="全情報をコピー", icon='COPY_ID')
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, theme = self.layout, context.scene.theme_grid_properties, bpy.context.preferences.themes[0]; layout.label(text=f"Current: {tuple(round(c, 3) for c in theme.view_3d.grid)}"); layout.operator(f"{PREFIX}.copy_grid_color", text="Copy Grid Color"); layout.separator(); 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, theme = self.layout, context.scene.theme_wire_properties, bpy.context.preferences.themes[0]; layout.label(text=f"Current: {tuple(round(c, 3) for c in theme.view_3d.wire)}"); layout.operator(f"{PREFIX}.copy_wire_color", text="Copy Wire Color"); layout.separator(); 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
for link in ADDON_LINKS:
op = layout.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
op.url = link["url"]
class SFC_PT_NewDocsLinksPanel(Panel):
bl_label = "アドオン管理"; bl_idname = PANEL_IDS["LINKS_NEWDOC"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
def draw(self, context):
layout = self.layout
if not NEW_DOC_LINKS:
layout.label(text="No links available.", icon='INFO')
for link in NEW_DOC_LINKS:
op = layout.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
op.url = link["url"]
class SFC_PT_DocsLinksPanel(Panel):
bl_label = "関連ドキュメント"; bl_idname = PANEL_IDS["LINKS_DOC"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
def draw(self, context):
layout = self.layout
for link in DOC_LINKS:
op = layout.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
op.url = link["url"]
class SFC_PT_SocialLinksPanel(Panel):
bl_label = "ソーシャルリンク"; bl_idname = PANEL_IDS["LINKS_SOCIAL"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_parent_id = PANEL_IDS["LINKS"]
def draw(self, context):
layout = self.layout
for link in SOCIAL_LINKS:
op = layout.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():
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
if source_node.type == 'TEX_SKY': props.background_mode = 'SKY'
else: props.background_mode = '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_WireApplyColor, SFC_OT_WireCopyColor, SFC_OT_ApplyCameraColor,
SFC_OT_CreateDedicatedCamera, SFC_OT_SyncWithCamera, SFC_OT_UnlinkObject, SFC_OT_ResetProperty, SFC_OT_SetFOV,
SFC_OT_CopyAllInfo, SFC_OT_OpenURL, SFC_OT_RemoveAddon, SFC_OT_SetFixedLocationFromView,
ZIONAD_SWT_OT_LoadHdriFromList, ZIONAD_SWT_OT_ResetTransform,
SFC_PT_CameraSetupPanel, SFC_PT_PositionPanel, SFC_PT_AimingPanel, SFC_PT_LensPanel, SFC_PT_CameraDisplayPanel,
ZIONAD_SWT_PT_WorldControlPanel, SFC_PT_InfoPanel, SFC_PT_GridPanel, SFC_PT_WirePanel,
SFC_PT_LinksPanel, SFC_PT_NewDocsLinksPanel, SFC_PT_DocsLinksPanel, SFC_PT_SocialLinksPanel,
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"Error registering class {cls.__name__}: {e}"); unregister(); raise
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 _update_timer and bpy.app.timers.is_registered(reset_update_flag): bpy.app.timers.unregister(reset_update_flag)
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 hasattr(bpy.types.Scene, prop_name):
try: delattr(bpy.types.Scene, prop_name)
except (AttributeError, RuntimeError): pass
for cls in reversed(classes):
if hasattr(bpy.utils, 'unregister_class') and cls in _registered_classes:
try: bpy.utils.unregister_class(cls)
except RuntimeError: pass
_registered_classes.clear()
if __name__ == "__main__":
try: unregister()
except Exception: pass
register()'''
# ==============================================================================
# 【 基本設定エリア 】
# ==============================================================================
PREFIX = "SquareTorus20260324"
ADDON_NAME = "zionad 520[ Sq-Torus ]"
TAB_NAME = "[ Sq Torus copy ] "
PANEL_TITLE = "Square Torus Generator"
AUTHOR = "zionadchat"
# ★ このスクリプト自身のID(絶対に消さないこと)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SQUARE_TORUS_2026_03_24_V7_FINAL ###"
# ==============================================================================
# システム初期化 & ID管理
# ==============================================================================
bl_info = {
"name": f"{ADDON_NAME} {PREFIX}",
"author": AUTHOR,
"version": (7, 0, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": f"Topology-Perfect Square Torus Generator - {PREFIX}",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
ADDON_LINKS = (
{"label": "Prefix トーラス正方形 20260324", "url": "<https://www.notion.so/Prefix-20260324-32df5dacaf4380528980db6a989d6306>"},
)
# ==============================================================================
# デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"show_preview": True,
"show_square_guide": True,
"torus_color": (0.0391, 0.8000, 0.1647, 0.8000),
"torus_loc": (0.0000, 0.0000, 0.0000),
"torus_rot": (0.0000, 30.0000, 0.0000),
"square_size": 10.0000,
"corner_radius": 0.0000,
"minor_radius": 0.5000,
"corner_segments": 8,
"minor_segments": 16,
"torus_plane": "XY",
}
# <END_DICT>
# ==============================================================================
# データ クリーンアップ管理
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_OBJ_NAME = f"[Preview] SqTorus_{PREFIX}"
PREVIEW_GUIDE_NAME = f"[Preview] SqGuide_{PREFIX}"
PREVIEW_MAT_NAME = f"PreviewMat_{PREFIX}"
def cleanup_preview_data():
for name in[PREVIEW_OBJ_NAME, PREVIEW_GUIDE_NAME]:
obj = bpy.data.objects.get(name)
if obj:
mesh = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if mesh and mesh.users == 0:
bpy.data.meshes.remove(mesh)
meshes_to_remove =[m for m in bpy.data.meshes if m.name.startswith(f"PreviewMesh_{PREFIX}")]
for m in meshes_to_remove:
if m.users == 0:
bpy.data.meshes.remove(m)
mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
if mat and mat.users == 0:
bpy.data.materials.remove(mat)
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if col and len(col.objects) == 0:
bpy.data.collections.remove(col)
def cleanup_old_materials(prefix="Mat_UniqueSqTorus", limit=50):
mats =[m for m in bpy.data.materials if m.name.startswith(prefix)]
if len(mats) > limit:
for m in mats[:-limit]:
if m.users == 0:
bpy.data.materials.remove(m)
# ==============================================================================
# 数学的 角丸正方形トーラス生成 & ガイド生成
# ==============================================================================
def create_square_guide_bmesh(bm, square_size):
S = square_size / 2.0
v1 = bm.verts.new((S, S, 0))
v2 = bm.verts.new((-S, S, 0))
v3 = bm.verts.new((-S, -S, 0))
v4 = bm.verts.new((S, -S, 0))
bm.verts.ensure_lookup_table()
bm.edges.new((v1, v2))
bm.edges.new((v2, v3))
bm.edges.new((v3, v4))
bm.edges.new((v4, v1))
return bm
def create_square_torus_bmesh(bm, square_size, corner_radius, minor_radius, corner_segments, minor_segments):
square_size = min(max(square_size, 0.01), 10000.0)
minor_radius = min(max(minor_radius, 0.001), square_size)
minor_segments = max(minor_segments, 3)
half_size = square_size / 2.0
actual_corner_radius = min(max(corner_radius, 0.0), half_size)
rings =[]
EPS = 1e-6
if actual_corner_radius < EPS:
L = half_size
corners =[
(mathutils.Vector((L, L, 0)), mathutils.Vector((1, 1, 0)).normalized()),
(mathutils.Vector((-L, L, 0)), mathutils.Vector((-1, 1, 0)).normalized()),
(mathutils.Vector((-L, -L, 0)), mathutils.Vector((-1, -1, 0)).normalized()),
(mathutils.Vector((L, -L, 0)), mathutils.Vector((1, -1, 0)).normalized())
]
scale_xy = 1.0 / math.cos(math.pi / 4)
for p, n in corners:
b = mathutils.Vector((0, 0, 1))
ring =[]
for j in range(minor_segments):
theta = j * 2.0 * math.pi / minor_segments
offset = n * (minor_radius * math.cos(theta) * scale_xy) + b * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
else:
L = half_size - actual_corner_radius
pts =[]
for q in range(4):
cx = L if q in [0, 3] else -L
cy = L if q in [0, 1] else -L
for i in range(corner_segments + 1):
angle = q * (math.pi / 2) + i * (math.pi / 2) / corner_segments
x = cx + actual_corner_radius * math.cos(angle)
y = cy + actual_corner_radius * math.sin(angle)
pts.append((mathutils.Vector((x, y, 0)), mathutils.Vector((math.cos(angle), math.sin(angle), 0))))
unique_pts =[]
for p, n in pts:
if not unique_pts or (unique_pts[-1][0] - p).length > EPS:
unique_pts.append((p, n))
if len(unique_pts) > 1 and (unique_pts[-1][0] - unique_pts[0][0]).length < EPS:
unique_pts.pop()
for p, n in unique_pts:
b = mathutils.Vector((0, 0, 1))
ring =[]
for j in range(minor_segments):
theta = j * 2.0 * math.pi / minor_segments
offset = n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
bm.verts.ensure_lookup_table()
total_rings = len(rings)
if total_rings < 3: return bm
edge_loops = []
for ring in rings:
edges =[]
for j in range(minor_segments):
v1 = ring[j]
v2 = ring[(j + 1) % minor_segments]
edges.append(bm.edges.new((v1, v2)))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(total_rings):
next_i = (i + 1) % total_rings
try:
bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[next_i])
except Exception:
pass
# ★ 順序保証: remove_doubles -> smooth -> recalc_face_normals
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
for f in bm.faces:
f.smooth = True
if bm.faces:
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
def apply_auto_smooth(mesh):
if bpy.app.version < (4, 1, 0):
try:
if hasattr(mesh, "use_auto_smooth"):
mesh.use_auto_smooth = True
mesh.auto_smooth_angle = math.radians(30)
except AttributeError:
pass
# ==============================================================================
# マテリアル作成ロジック
# ==============================================================================
def create_unique_material(color, name_prefix="Mat_UniqueSqTorus"):
timestamp = datetime.now().strftime('%M%S%f')[:5]
mat_name = f"{name_prefix}_{timestamp}"
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
if mat.use_nodes:
tree = mat.node_tree
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (0, 0)
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (300, 0)
tree.links.new(bsdf.outputs[0], out.inputs[0])
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]
cleanup_old_materials(name_prefix)
return mat
def get_or_create_preview_material():
mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
if not mat:
mat = bpy.data.materials.new(name=PREVIEW_MAT_NAME)
mat.use_nodes = True
mat.blend_method = 'BLEND'
return mat
def update_preview_material(mat, color):
if mat.use_nodes:
bsdf = None
for node in mat.node_tree.nodes:
if node.type == 'BSDF_PRINCIPLED':
bsdf = node
break
if not bsdf:
mat.node_tree.nodes.clear()
bsdf = mat.node_tree.nodes.new("ShaderNodeBsdfPrincipled")
out = mat.node_tree.nodes.new("ShaderNodeOutputMaterial")
mat.node_tree.links.new(bsdf.outputs[0], out.inputs[0])
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]
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
def get_transform_matrix(props):
rot_matrix = mathutils.Matrix.Identity(4)
if props.torus_plane == 'YZ':
rot_matrix = mathutils.Matrix.Rotation(math.radians(90.0), 4, 'Y')
elif props.torus_plane == 'ZX':
rot_matrix = mathutils.Matrix.Rotation(math.radians(-90.0), 4, 'X')
user_rot = mathutils.Euler((
math.radians(props.torus_rot[0]),
math.radians(props.torus_rot[1]),
math.radians(props.torus_rot[2])
), 'XYZ').to_matrix().to_4x4()
loc_matrix = mathutils.Matrix.Translation(mathutils.Vector(props.torus_loc))
return loc_matrix @ user_rot @ rot_matrix
def update_preview_geometry(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_NAME)
if col.name not in context.scene.collection.children:
context.scene.collection.children.link(col)
obj = bpy.data.objects.get(PREVIEW_OBJ_NAME)
guide_obj = bpy.data.objects.get(PREVIEW_GUIDE_NAME)
if not props.show_preview:
if obj: bpy.data.objects.remove(obj, do_unlink=True)
if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)
return
final_matrix = get_transform_matrix(props)
scene_mesh_name = f"PreviewMesh_{PREFIX}_{context.scene.name}"
bm = bmesh.new()
try:
create_square_torus_bmesh(
bm,
square_size=props.square_size,
corner_radius=props.corner_radius,
minor_radius=props.minor_radius,
corner_segments=props.corner_segments,
minor_segments=props.minor_segments
)
bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
mesh = bpy.data.meshes.get(scene_mesh_name)
if not mesh:
mesh = bpy.data.meshes.new(scene_mesh_name)
else:
mesh.clear_geometry()
bm.to_mesh(mesh)
apply_auto_smooth(mesh)
mesh.update()
finally:
bm.free()
if not obj:
obj = bpy.data.objects.new(PREVIEW_OBJ_NAME, mesh)
col.objects.link(obj)
elif obj.data != mesh:
obj.data = mesh
mat = get_or_create_preview_material()
update_preview_material(mat, props.torus_color)
if not obj.data.materials:
obj.data.materials.append(mat)
else:
obj.data.materials[0] = mat
if props.show_square_guide:
bm_g = bmesh.new()
try:
create_square_guide_bmesh(bm_g, props.square_size)
bmesh.ops.transform(bm_g, matrix=final_matrix, verts=bm_g.verts)
guide_mesh_name = scene_mesh_name + "_Guide"
mesh_g = bpy.data.meshes.get(guide_mesh_name)
if not mesh_g:
mesh_g = bpy.data.meshes.new(guide_mesh_name)
else:
mesh_g.clear_geometry()
bm_g.to_mesh(mesh_g)
mesh_g.update()
finally:
bm_g.free()
if not guide_obj:
guide_obj = bpy.data.objects.new(PREVIEW_GUIDE_NAME, mesh_g)
col.objects.link(guide_obj)
elif guide_obj.data != mesh_g:
guide_obj.data = mesh_g
guide_obj.display_type = 'WIRE'
guide_obj.show_in_front = True
else:
if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)
_timer = None
_last_update_time = 0
def delayed_update():
global _timer, _last_update_time
_timer = None
now = time.time()
if now - _last_update_time < 0.05:
if _timer is None:
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
return None
_last_update_time = now
ctx = bpy.context
if not ctx or not ctx.scene:
return None
if ctx.object and ctx.object.mode != 'OBJECT':
return None
update_preview_geometry(ctx)
return None
def on_update(self, context):
global _timer
if _timer is None:
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_TorusProps(PropertyGroup):
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
show_square_guide: BoolProperty(name="Show Square Guide", default=CURRENT_DEFAULTS['show_square_guide'], update=on_update)
torus_color: FloatVectorProperty(name="Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['torus_color'], update=on_update)
torus_plane: EnumProperty(
name="Plane",
items=[('XY', "XY Plane", ""), ('YZ', "YZ Plane", ""), ('ZX', "ZX Plane", "")],
default=CURRENT_DEFAULTS['torus_plane'], update=on_update
)
torus_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['torus_loc'], update=on_update)
torus_rot: FloatVectorProperty(name="Rotation (Deg)", size=3, default=CURRENT_DEFAULTS['torus_rot'], update=on_update)
square_size: FloatProperty(name="Square Size", default=CURRENT_DEFAULTS['square_size'], min=0.1, max=10000.0, update=on_update)
corner_radius: FloatProperty(name="Corner Radius", default=CURRENT_DEFAULTS['corner_radius'], min=0.0, max=5000.0, update=on_update)
minor_radius: FloatProperty(name="Tube Thickness", default=CURRENT_DEFAULTS['minor_radius'], min=0.01, max=5000.0, update=on_update)
corner_segments: IntProperty(name="Corner Segs", default=CURRENT_DEFAULTS['corner_segments'], min=1, soft_max=128, update=on_update)
minor_segments: IntProperty(name="Tube Segs", default=CURRENT_DEFAULTS['minor_segments'], min=3, soft_max=128, update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CreateTorus(Operator):
bl_idname = f"{OP_PREFIX}.create_torus"
bl_label = "Create Square Torus"
bl_options = {'REGISTER', 'UNDO'} # ★ UX重視: UNDOを復活
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
bm = bmesh.new()
create_square_torus_bmesh(
bm,
square_size=props.square_size,
corner_radius=props.corner_radius,
minor_radius=props.minor_radius,
corner_segments=props.corner_segments,
minor_segments=props.minor_segments
)
final_matrix = get_transform_matrix(props)
bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
mesh = bpy.data.meshes.new(f"SquareTorus_Mesh")
bm.to_mesh(mesh)
bm.free()
apply_auto_smooth(mesh)
obj = bpy.data.objects.new(f"SqTorus_{datetime.now().strftime('%H%M%S')}", mesh)
if context.collection: context.collection.objects.link(obj)
else: context.scene.collection.objects.link(obj)
unique_mat = create_unique_material(props.torus_color, "Mat_UniqueSqTorus")
obj.data.materials.append(unique_mat)
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
context.view_layer.objects.active = obj
self.report({'INFO'}, "Created Topology-Perfect Square Torus!")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string(): target_text = t; break
if not target_text:
self.report({'WARNING'}, "Source script not found in Text Editor.")
return {'CANCELLED'}
code = target_text.as_string()
c, l, r = props.torus_color, props.torus_loc, props.torus_rot
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "show_square_guide": {props.show_square_guide},\n'
new_dict += f' "torus_color": ({c[0]:.4f}, {c[1]:.4f}, {c[2]:.4f}, {c[3]:.4f}),\n'
new_dict += f' "torus_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
new_dict += f' "torus_rot": ({r[0]:.4f}, {r[1]:.4f}, {r[2]:.4f}),\n'
new_dict += f' "square_size": {props.square_size:.4f},\n'
new_dict += f' "corner_radius": {props.corner_radius:.4f},\n'
new_dict += f' "minor_radius": {props.minor_radius:.4f},\n'
new_dict += f' "corner_segments": {props.corner_segments},\n'
new_dict += f' "minor_segments": {props.minor_segments},\n'
new_dict += f' "torus_plane": "{props.torus_plane}",\n'
new_dict += "}\n"
try:
tag_start = "# <BEGIN" + "_DICT>"
tag_end = "# <END" + "_DICT>"
if tag_start not in code or tag_end not in code:
self.report({'ERROR'}, "DICT tags missing! Script might be corrupted.")
return {'CANCELLED'}
pre_code, rest = code.split(tag_start, 1)
_, post_code = rest.split(tag_end, 1)
final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
if SOURCE_ID_TAG not in final_code:
self.report({'ERROR'}, "Critical Error: SOURCE_ID_TAG lost during copy.")
return {'CANCELLED'}
lines = final_code.split("\n")
if len(lines) > 0 and lines[0].startswith("# Copied:"):
time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
lines[0] = f"# Copied: {time_str}"
final_code = "\n".join(lines)
context.window_manager.clipboard = final_code
self.report({'INFO'}, "Code copied with absolute safety!")
except Exception as e:
self.report({'ERROR'}, f"Copy failed: {e}")
return {'CANCELLED'}
return {'FINISHED'}
class OT_Reset(Operator):
bl_idname = f"{OP_PREFIX}.reset"
bl_label = "Reset Transform"
def execute(self, context):
p = getattr(context.scene, PROPS_NAME)
p.torus_loc = (0,0,0)
p.torus_rot = (0,0,0)
p.torus_plane = 'XY'
p.square_size = 10.0
p.corner_radius = 0.0
p.minor_radius = 0.5
return {'FINISHED'}
class OT_OpenUrl(Operator):
bl_idname = f"{OP_PREFIX}.open_url"; bl_label = "Open URL"; url: StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class OT_RemoveAddon(Operator):
bl_idname = f"{OP_PREFIX}.remove_addon"; bl_label = "Remove Addon"
def execute(self, context):
bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
return {'FINISHED'}
# ==============================================================================
# ★ 追加したオペレーター: 2つ目のスクリプトをテキストエディタに生成
# ==============================================================================
class OT_AddZukkeiScript(Operator):
bl_idname = f"{OP_PREFIX}.add_zukkei_script"
bl_label = "Load Zukkei Script"
bl_description = "図形&配列ジェネレータースクリプトをテキストエディターに読み込みます"
def execute(self, context):
text_name = "B5200_Zukkei_Array_View_20260319.py"
if text_name in bpy.data.texts:
text_data = bpy.data.texts[text_name]
text_data.clear()
else:
text_data = bpy.data.texts.new(name=text_name)
text_data.write(ZUKKEI_SCRIPT_CONTENT)
found_editor = False
for area in context.screen.areas:
if area.type == 'TEXT_EDITOR':
area.spaces.active.text = text_data
found_editor = True
break
if found_editor:
self.report({'INFO'}, f"テキストエディターに '{text_name}' を読み込みました!")
else:
self.report({'INFO'}, f"'{text_name}' を作成しました。Text Editor を開いて確認してください。")
return {'FINISHED'}
# ==============================================================================
# ★ 追加したオペレーター: 3つ目のスクリプトをテキストエディタに生成
# ==============================================================================
class OT_AddViewportScript(Operator):
bl_idname = f"{OP_PREFIX}.add_viewport_script"
bl_label = "Load Viewport & Sun Script"
bl_description = "3D Viewport Color & Sun スクリプトをテキストエディターに読み込みます"
def execute(self, context):
text_name = "Viewport_Color_Sun_20260316.py"
if text_name in bpy.data.texts:
text_data = bpy.data.texts[text_name]
text_data.clear()
else:
text_data = bpy.data.texts.new(name=text_name)
text_data.write(VIEWPORT_SCRIPT_CONTENT)
found_editor = False
for area in context.screen.areas:
if area.type == 'TEXT_EDITOR':
area.spaces.active.text = text_data
found_editor = True
break
if found_editor:
self.report({'INFO'}, f"テキストエディターに '{text_name}' を読み込みました!")
else:
self.report({'INFO'}, f"'{text_name}' を作成しました。Text Editor を開いて確認してください。")
return {'FINISHED'}
# ==============================================================================
# ★ 追加したオペレーター: 4つ目のスクリプトをテキストエディタに生成
# ==============================================================================
class OT_AddCameraScript(Operator):
bl_idname = f"{OP_PREFIX}.add_camera_script"
bl_label = "Load Fixed Camera Script"
bl_description = "Fixed Camera & World スクリプトをテキストエディターに読み込みます"
def execute(self, context):
text_name = "Fixed_Camera_World_2026.py"
if text_name in bpy.data.texts:
text_data = bpy.data.texts[text_name]
text_data.clear()
else:
text_data = bpy.data.texts.new(name=text_name)
text_data.write(CAMERA_SCRIPT_CONTENT)
found_editor = False
for area in context.screen.areas:
if area.type == 'TEXT_EDITOR':
area.spaces.active.text = text_data
found_editor = True
break
if found_editor:
self.report({'INFO'}, f"テキストエディターに '{text_name}' を読み込みました!")
else:
self.report({'INFO'}, f"'{text_name}' を作成しました。Text Editor を開いて確認してください。")
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_MainPanel(Panel):
bl_label = PANEL_TITLE
bl_idname = f"{PREFIX}_PT_main"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: layout.label(text="Reload Script"); return
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
box = layout.box()
if not props.show_preview:
box.label(text="Preview is Hidden", icon='INFO')
box.prop(props, "torus_color")
col = box.column(align=True)
col.prop(props, "torus_plane")
col.prop(props, "torus_loc")
col.prop(props, "torus_rot")
box.separator()
box.prop(props, "show_square_guide", icon='MESH_PLANE')
col_s = box.column(align=True)
col_s.prop(props, "square_size")
row_cr = col_s.row()
row_cr.prop(props, "corner_radius")
if props.corner_radius <= 0.001:
row_cr.label(text="[90° Mode]", icon='SNAP_VERTEX')
col_s.prop(props, "minor_radius")
row_seg = box.row()
row_seg.prop(props, "corner_segments")
row_seg.prop(props, "minor_segments")
box.operator(OT_Reset.bl_idname, icon='LOOP_BACK')
layout.separator()
col_exec = layout.column()
col_exec.scale_y = 1.5
col_exec.operator(OT_CreateTorus.bl_idname, icon='MESH_TORUS', text="Create Square Torus")
class PT_LinksPanel(Panel):
bl_label = "Links"; bl_idname = f"{PREFIX}_PT_links"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
for l in ADDON_LINKS: self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"]).url = l["url"]
class PT_RemovePanel(Panel):
bl_label = "System"; bl_idname = f"{PREFIX}_PT_remove"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): self.layout.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Remove Addon")
# ==============================================================================
# ★ 追加したパネル: スクリプト出力ボタンを配置 ★
# ==============================================================================
class PT_ScriptPanel(Panel):
bl_label = "Additional Scripts"
bl_idname = f"{PREFIX}_PT_script"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
# 2つ目のスクリプト (図形ジェネレーター)
layout.label(text="B5200 図形ジェネレーターを追加:")
layout.operator(OT_AddZukkeiScript.bl_idname, icon='TEXT', text="Load Zukkei Script")
layout.separator()
# 3つ目のスクリプト (Viewport & Sun)
layout.label(text="5520 Viewport & Sun を追加:")
layout.operator(OT_AddViewportScript.bl_idname, icon='TEXT', text="Load Viewport Script")
layout.separator()
# 4つ目のスクリプト (Fixed Camera & World)
layout.label(text="v100 Fixed Camera を追加:")
layout.operator(OT_AddCameraScript.bl_idname, icon='TEXT', text="Load Camera Script")
# ==============================================================================
# REGISTER
# ==============================================================================
classes = (
PG_TorusProps,
OT_CreateTorus,
OT_CopyFullScript,
OT_Reset,
OT_OpenUrl,
OT_RemoveAddon,
OT_AddZukkeiScript, # ★追加
OT_AddViewportScript, # ★追加
OT_AddCameraScript, # ★追加
PT_MainPanel,
PT_LinksPanel,
PT_RemovePanel,
PT_ScriptPanel # ★追加
)
def auto_open_sidebar():
try:
for window in bpy.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':
if not space.show_region_ui:
space.show_region_ui = True
except: pass
return None
def register():
for c in classes:
try:
bpy.utils.register_class(c)
except ValueError:
pass
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_TorusProps))
bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)
def unregister():
global _timer
if _timer is not None:
try:
bpy.app.timers.unregister(_timer)
except Exception:
pass
_timer = None
cleanup_preview_data()
if hasattr(bpy.types.Scene, PROPS_NAME):
delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes):
try:
bpy.utils.unregister_class(c)
except ValueError:
pass
if __name__ == "__main__":
register()