Sidebar (Nパネル)", "description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション", "category": "Object", } import bpy import webbrowser import math import bmesh import mathutils # ========================================================================= # 【基本設定】 # ========================================================================= TAB_NAME = "Cam 20260706" PREFIX_NAME = "Can001_addon6" INIT_COLOR = (0.0, 1.0, 0.5) IS_UPDATING = False # ========================================================================= # 【マトリクス・座標変換ヘルパー】 # ========================================================================= def get_matrix(loc, rot): euler = mathutils.Euler(rot, 'XYZ') mat_rot = euler.to_matrix().to_4x4() mat_loc = mathutils.Matrix.Translation(loc) return mat_loc @ mat_rot def decompose"> Sidebar (Nパネル)", "description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション", "category": "Object", } import bpy import webbrowser import math import bmesh import mathutils # ========================================================================= # 【基本設定】 # ========================================================================= TAB_NAME = "Cam 20260706" PREFIX_NAME = "Can001_addon6" INIT_COLOR = (0.0, 1.0, 0.5) IS_UPDATING = False # ========================================================================= # 【マトリクス・座標変換ヘルパー】 # ========================================================================= def get_matrix(loc, rot): euler = mathutils.Euler(rot, 'XYZ') mat_rot = euler.to_matrix().to_4x4() mat_loc = mathutils.Matrix.Translation(loc) return mat_loc @ mat_rot def decompose"> Sidebar (Nパネル)", "description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション", "category": "Object", } import bpy import webbrowser import math import bmesh import mathutils # ========================================================================= # 【基本設定】 # ========================================================================= TAB_NAME = "Cam 20260706" PREFIX_NAME = "Can001_addon6" INIT_COLOR = (0.0, 1.0, 0.5) IS_UPDATING = False # ========================================================================= # 【マトリクス・座標変換ヘルパー】 # ========================================================================= def get_matrix(loc, rot): euler = mathutils.Euler(rot, 'XYZ') mat_rot = euler.to_matrix().to_4x4() mat_loc = mathutils.Matrix.Translation(loc) return mat_loc @ mat_rot def decompose">

bl_info = {
"name": "Polygon Torus Generator Pro (Realtime & Detach)",
"author": "Your Name",
"version": (7, 1),
"blender": (4, 2, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション",
"category": "Object",
}
import bpy
import webbrowser
import math
import bmesh
import mathutils
# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME = "Cam 20260706"
PREFIX_NAME = "Can001_addon6"
INIT_COLOR = (0.0, 1.0, 0.5)
IS_UPDATING = False
# =========================================================================
# 【マトリクス・座標変換ヘルパー】
# =========================================================================
def get_matrix(loc, rot):
euler = mathutils.Euler(rot, 'XYZ')
mat_rot = euler.to_matrix().to_4x4()
mat_loc = mathutils.Matrix.Translation(loc)
return mat_loc @ mat_rot
def decompose_matrix(matrix):
loc = matrix.to_translation()
rot = matrix.to_euler('XYZ')
return loc, rot
def get_world_verts(verts_local, loc, rot):
mat = get_matrix(loc, rot)
verts_world = []
for v in verts_local:
v_4d = mathutils.Vector((v[0], v[1], v[2], 1.0))
v_world = (mat @ v_4d).to_3d()
verts_world.append(tuple(v_world))
return verts_world
# =========================================================================
# 【純粋数学メッシュ更新:既存オブジェクトの再描画用ヘルパー】
# =========================================================================
def get_torus_mesh_pure(n, r, thick):
radius = r * math.sqrt(2) if n == 4 else r
mesh = bpy.data.meshes.new("TorusMesh")
bm = bmesh.new()
major_segments = n
minor_segments = 12
verts = []
for i in range(major_segments):
theta = 2.0 * math.pi * i / major_segments
cos_theta = math.cos(theta)
sin_theta = math.sin(theta)
ring = []
for j in range(minor_segments):
phi = 2.0 * math.pi * j / minor_segments
cos_phi = math.cos(phi)
sin_phi = math.sin(phi)
x = (radius + thick * cos_phi) * cos_theta
y = (radius + thick * cos_phi) * sin_theta
z = thick * sin_phi
ring.append(bm.verts.new((x, y, z)))
verts.append(ring)
for i in range(major_segments):
next_i = (i + 1) % major_segments
for j in range(minor_segments):
next_j = (j + 1) % minor_segments
v1 = verts[i][j]
v2 = verts[next_i][j]
v3 = verts[next_i][next_j]
v4 = verts[i][next_j]
try:
bm.faces.new((v1, v2, v3, v4))
except ValueError:
pass
bm.to_mesh(mesh)
bm.free()
return mesh
def get_sphere_mesh_pure(r):
mesh = bpy.data.meshes.new("SphereMesh")
bm = bmesh.new()
bmesh.ops.create_icosphere(bm, subdivisions=3, radius=r)
bm.to_mesh(mesh)
bm.free()
return mesh
# =========================================================================
# 【ヘルパー関数:マテリアル・メッシュ再生成】
# =========================================================================
def get_or_create_mat(name, color, alpha=1.0):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = (*color, 1.0)
bsdf.inputs["Alpha"].default_value = alpha
mat.diffuse_color = (*color, alpha)
try: mat.surface_render_method = 'BLENDED' if alpha < 1.0 else 'DITHERED'
except: pass
try: mat.blend_method = 'BLEND' if alpha < 1.0 else 'OPAQUE'
except: pass
try: mat.shadow_method = 'NONE'
except: pass
return mat
def update_skirt_material(mat, color, alpha):
if not mat.use_nodes:
mat.use_nodes = True
try: mat.blend_method = 'BLEND'
except: pass
try: mat.surface_render_method = 'BLENDED'
except: pass
try: mat.show_transparent_back = True
except: pass
try: mat.use_transparency_overlap = True
except: pass
try: mat.shadow_method = 'NONE'
except: pass
try: mat.use_backface_culling = False
except: pass
try: mat.use_backface_culling_shadow = False
except: pass
tree = mat.node_tree
tree.nodes.clear()
bsdf = tree.nodes.new('ShaderNodeBsdfPrincipled')
out = tree.nodes.new('ShaderNodeOutputMaterial')
bsdf.inputs["Base Color"].default_value = (*color, 1.0)
bsdf.inputs["Alpha"].default_value = alpha
tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
mat.diffuse_color = (*color, alpha)
def get_polygon_verts_local(n, r):
verts = []
radius = r * math.sqrt(2) if n == 4 else r
phase = math.pi / 4 if n == 4 else 0.0
for i in range(n):
angle = phase + 2 * math.pi * i / n
verts.append((radius * math.cos(angle), 0.0, radius * math.sin(angle)))
return verts
def create_convex_mesh(verts, mesh_name):
mesh = bpy.data.meshes.new(mesh_name)
if len(verts) < 3: return mesh
bm = bmesh.new()
for v in verts:
bm.verts.new(v)
try: bmesh.ops.convex_hull(bm, input=bm.verts)
except: pass
bm.to_mesh(mesh)
bm.free()
return mesh
def create_cone_skirt_mesh(p_loc, va, mesh_name):
mesh = bpy.data.meshes.new(mesh_name)
if len(va) < 3: return mesh
bm = bmesh.new()
v_p = bm.verts.new(p_loc)
v_as = [bm.verts.new(v) for v in va]
for k in range(len(v_as)):
v1 = v_as[k]
v2 = v_as[(k + 1) % len(v_as)]
try: bm.faces.new((v_p, v2, v1))
except ValueError: pass
bm.to_mesh(mesh)
bm.free()
return mesh
def create_frustum_skirt_mesh(p_loc, vc, normal_a, a_loc, mesh_name):
mesh = bpy.data.meshes.new(mesh_name)
if len(vc) < 3: return mesh
bm = bmesh.new()
p = mathutils.Vector(p_loc)
a_l = mathutils.Vector(a_loc)
va_projected = []
for v in vc:
c_k = mathutils.Vector(v)
dir_v = c_k - p
denom = dir_v.dot(normal_a)
if abs(denom) > 1e-6:
t = (a_l - p).dot(normal_a) / denom
a_k = p + t * dir_v
else:
a_k = c_k
va_projected.append(tuple(a_k))
v_cs = [bm.verts.new(v) for v in vc]
v_aps = [bm.verts.new(v) for v in va_projected]
for k in range(len(vc)):
next_k = (k + 1) % len(vc)
try: bm.faces.new((v_aps[k], v_cs[k], v_cs[next_k], v_aps[next_k]))
except ValueError: pass
bm.to_mesh(mesh)
bm.free()
return mesh
# =========================================================================
# 【数学・幾何学ロジック (シミュレーション計算)】
# =========================================================================
def get_projected_verts_mode1(verts_a, verts_b, y_c):
pts = []
for pa in verts_a:
for pb in verts_b:
dy = pb[1] - pa[1]
if abs(dy) < 0.0001: continue
t = (y_c - pa[1]) / dy
x = pa[0] + t * (pb[0] - pa[0])
z = pa[2] + t * (pb[2] - pa[2])
pts.append((x, y_c, z))
return pts
def get_projected_verts_mode2(p_loc, verts_a, verts_b, y_c):
px, py, pz = p_loc
def project_polygon(verts):
poly2d = []
for vx, vy, vz in verts:
if abs(vy - py) < 1e-6: return []
t = (y_c - py) / (vy - py)
if t < 0: return []
x = px + t * (vx - px)
z = pz + t * (vz - pz)
poly2d.append((x, z))
return poly2d
def make_ccw(poly):
if len(poly) < 3: return poly
area = sum(poly[i][0]*poly[(i+1)%len(poly)][1] - poly[(i+1)%len(poly)][0]*poly[i][1] for i in range(len(poly)))
return poly[::-1] if area < 0 else poly
def sutherland_hodgman(subject, clip):
def inside(p, cp1, cp2):
return (cp2[0] - cp1[0]) * (p[1] - cp1[1]) - (cp2[1] - cp1[1]) * (p[0] - cp1[0]) >= -1e-8
def compute_intersection(cp1, cp2, s, e):
dc, dp = [cp1[0] - cp2[0], cp1[1] - cp2[1]], [s[0] - e[0], s[1] - e[1]]
n1, n2 = cp1[0] * cp2[1] - cp1[1] * cp2[0], s[0] * e[1] - s[1] * e[0]
det = dc[0] * dp[1] - dc[1] * dp[0]
if abs(det) < 1e-8: return s
n3 = 1.0 / det
return ((n1*dp[0] - n2*dc[0]) * n3, (n1*dp[1] - n2*dc[1]) * n3)
out_poly = subject
if not out_poly or not clip: return []
cp1 = clip[-1]
for cp2 in clip:
in_poly = out_poly
out_poly = []
if not in_poly: break
s = in_poly[-1]
for e in in_poly:
if inside(e, cp1, cp2):
if not inside(s, cp1, cp2):
out_poly.append(compute_intersection(cp1, cp2, s, e))
out_poly.append(e)
elif inside(s, cp1, cp2):
out_poly.append(compute_intersection(cp1, cp2, s, e))
s = e
cp1 = cp2
return out_poly
poly_a = project_polygon(verts_a)
poly_b = project_polygon(verts_b)
if not poly_a or not poly_b: return []
intersect_poly2d = sutherland_hodgman(make_ccw(poly_a), make_ccw(poly_b))
return [(x, y_c, z) for (x, z) in intersect_poly2d]
# =========================================================================
# 【リアルタイム更新関数 (投影シミュレーション用)】
# =========================================================================
def update_proj(self, context):
global IS_UPDATING
if IS_UPDATING: return
props = context.scene.poly_torus_props
obj_a = bpy.data.objects.get("Proj_Plane_A")
obj_b = bpy.data.objects.get("Proj_Plane_B")
obj_c = bpy.data.objects.get("Proj_Plane_C")
obj_p = bpy.data.objects.get("Proj_Point_P")
obj_skirt_a = bpy.data.objects.get("Proj_Skirt_A")
obj_skirt_c = bpy.data.objects.get("Proj_Skirt_C")
if not (obj_a and obj_b and obj_c and obj_p and obj_skirt_a and obj_skirt_c): return
IS_UPDATING = True
try:
n_a, r_a, thick_a = props.proj_n_a, props.proj_r_a, props.proj_thick_a
n_b, r_b, thick_b = props.proj_n_b, props.proj_r_b, props.proj_thick_b
y_c = props.proj_y_c
def update_torus(obj, n, r, thick, loc, rot, color):
mesh = get_torus_mesh_pure(n, r, thick)
old_m, old_mat = obj.data, obj.active_material
obj.data = mesh
if old_mat:
obj.data.materials.append(old_mat)
if old_mat.use_nodes:
bsdf = old_mat.node_tree.nodes.get("Principled BSDF")
if bsdf: bsdf.inputs["Base Color"].default_value = (*color, 1.0)
old_mat.diffuse_color = (*color, 1.0)
mat_base = mathutils.Euler((math.pi / 2, math.pi / 4 if n == 4 else 0.0, 0.0)).to_matrix().to_4x4()
mat_user = get_matrix(loc, rot)
mat_final = mat_user @ mat_base
obj.location = mat_final.to_translation()
obj.rotation_euler = mat_final.to_euler('XYZ')
if old_m: bpy.data.meshes.remove(old_m)
update_torus(obj_a, n_a, r_a, thick_a, props.proj_a_loc, props.proj_a_rot, props.proj_color_a)
update_torus(obj_b, n_b, r_b, thick_b, props.proj_b_loc, props.proj_b_rot, props.proj_color_b)
if props.proj_mode == 'MODE2':
obj_p.hide_viewport = False
mesh_p = get_sphere_mesh_pure(props.proj_p_radius)
old_m_p = obj_p.data
mat_p = obj_p.active_material
obj_p.data = mesh_p
if mat_p:
obj_p.data.materials.append(mat_p)
if mat_p.use_nodes:
bsdf = mat_p.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = (*props.proj_color_p, 1.0)
bsdf.inputs["Alpha"].default_value = props.proj_alpha_p
mat_p.diffuse_color = (*props.proj_color_p, props.proj_alpha_p)
try: mat_p.surface_render_method = 'BLENDED' if props.proj_alpha_p < 1.0 else 'DITHERED'
except: pass
try: mat_p.blend_method = 'BLEND' if props.proj_alpha_p < 1.0 else 'OPAQUE'
except: pass
if old_m_p: bpy.data.meshes.remove(old_m_p)
mat_p_user = get_matrix(props.proj_p_loc, props.proj_p_rot)
obj_p.location = mat_p_user.to_translation()
obj_p.rotation_euler = mat_p_user.to_euler('XYZ')
else:
obj_p.hide_viewport = True
va = get_world_verts(get_polygon_verts_local(n_a, r_a), props.proj_a_loc, props.proj_a_rot)
vb = get_world_verts(get_polygon_verts_local(n_b, r_b), props.proj_b_loc, props.proj_b_rot)
if props.proj_mode == 'MODE1':
vc = get_projected_verts_mode1(va, vb, y_c)
color_c = props.proj_color_c_m1
else:
vc = get_projected_verts_mode2(props.proj_p_loc, va, vb, y_c)
color_c = props.proj_color_c_m2
mesh_c = create_convex_mesh(vc, "Mesh_C")
old_mc, old_mat_c = obj_c.data, obj_c.active_material
obj_c.data = mesh_c
obj_c.hide_viewport = not props.proj_show_c
obj_c.hide_render = not props.proj_show_c
if old_mat_c: obj_c.data.materials.append(old_mat_c)
obj_c.location = (0, 0, 0)
obj_c.rotation_euler = (0, 0, 0)
if old_mc: bpy.data.meshes.remove(old_mc)
if old_mat_c and old_mat_c.use_nodes:
bsdf = old_mat_c.node_tree.nodes.get("Principled BSDF")
if bsdf: bsdf.inputs["Base Color"].default_value = (*color_c, 1.0)
old_mat_c.diffuse_color = (*color_c, 0.8)
if props.proj_mode == 'MODE2' and props.proj_show_skirt:
obj_skirt_a.hide_viewport = False
obj_skirt_c.hide_viewport = False
mat_a_rot = mathutils.Euler(props.proj_a_rot, 'XYZ').to_matrix()
normal_a = mat_a_rot @ mathutils.Vector((0.0, 1.0, 0.0))
normal_a.normalize()
mesh_skirt_a = create_cone_skirt_mesh(props.proj_p_loc, va, "Mesh_Skirt_A")
old_m_skirt_a = obj_skirt_a.data
mat_skirt_a = obj_skirt_a.active_material
obj_skirt_a.data = mesh_skirt_a
if mat_skirt_a:
obj_skirt_a.data.materials.append(mat_skirt_a)
update_skirt_material(mat_skirt_a, props.proj_color_skirt_a, props.proj_alpha_skirt_a)
if old_m_skirt_a: bpy.data.meshes.remove(old_m_skirt_a)
obj_skirt_a.location = (0, 0, 0)
obj_skirt_a.rotation_euler = (0, 0, 0)
mesh_skirt_c = create_frustum_skirt_mesh(props.proj_p_loc, vc, normal_a, props.proj_a_loc, "Mesh_Skirt_C")
old_m_skirt_c = obj_skirt_c.data
mat_skirt_c = obj_skirt_c.active_material
obj_skirt_c.data = mesh_skirt_c
if mat_skirt_c:
obj_skirt_c.data.materials.append(mat_skirt_c)
update_skirt_material(mat_skirt_c, props.proj_color_skirt_c, props.proj_alpha_skirt_c)
if old_m_skirt_c: bpy.data.meshes.remove(old_m_skirt_c)
obj_skirt_c.location = (0, 0, 0)
obj_skirt_c.rotation_euler = (0, 0, 0)
else:
obj_skirt_a.hide_viewport = True
obj_skirt_c.hide_viewport = True
finally:
IS_UPDATING = False
# =========================================================================
# 【連動制御用アップデート関数】
# =========================================================================
def update_proj_link_p_a(self, context):
props = context.scene.poly_torus_props
if props.proj_link_p_a:
mat_p = get_matrix(props.proj_p_loc, props.proj_p_rot)
mat_a = get_matrix(props.proj_a_loc, props.proj_a_rot)
mat_rel = mat_p.inverted() @ mat_a
props.proj_p_a_rel_matrix = [val for row in mat_rel for val in row]
def update_proj_p_transform(self, context):
global IS_UPDATING
if IS_UPDATING: return
props = context.scene.poly_torus_props
if props.proj_link_p_a:
IS_UPDATING = True
try:
mat_rel = mathutils.Matrix([props.proj_p_a_rel_matrix[i:i+4] for i in range(0, 16, 4)])
mat_p = get_matrix(props.proj_p_loc, props.proj_p_rot)
mat_a_new = mat_p @ mat_rel
loc_a, rot_a = decompose_matrix(mat_a_new)
props.proj_a_loc = loc_a
props.proj_a_rot = rot_a
finally:
IS_UPDATING = False
update_proj(self, context)
def update_proj_a_transform(self, context):
global IS_UPDATING
if IS_UPDATING: return
props = context.scene.poly_torus_props
if props.proj_link_p_a:
mat_p = get_matrix(props.proj_p_loc, props.proj_p_rot)
mat_a = get_matrix(props.proj_a_loc, props.proj_a_rot)
mat_rel = mat_p.inverted() @ mat_a
props.proj_p_a_rel_matrix = [val for row in mat_rel for val in row]
update_proj(self, context)
# =========================================================================
# 【カメラ軌道制御用アップデート関数】
# =========================================================================
def update_cam_circle(self, context):
cam = context.scene.camera
if not cam: return
curve_obj = None
for const in cam.constraints:
if const.type == 'FOLLOW_PATH':
curve_obj = const.target
break
if curve_obj and "CamTrack_Circle" in curve_obj.name:
curve_obj.location = self.cam_circle_center
curve_obj.scale = (self.cam_circle_radius, self.cam_circle_radius, self.cam_circle_radius)
curve_obj.rotation_euler = self.cam_circle_rotation # 傾きの反映
def update_cam_line(self, context):
cam = context.scene.camera
if not cam: return
curve_obj = None
for const in cam.constraints:
if const.type == 'FOLLOW_PATH':
curve_obj = const.target
break
if curve_obj and "CamTrack_Line" in curve_obj.name:
curve_obj.location = (0, 0, 0)
curve_obj.scale = (1, 1, 1)
curve_obj.rotation_euler = (0, 0, 0)
if len(curve_obj.data.splines) > 0:
spline = curve_obj.data.splines[0]
if spline.type == 'POLY' and len(spline.points) >= 2:
spline.points[0].co = (self.cam_line_start[0], self.cam_line_start[1], self.cam_line_start[2], 1.0)
spline.points[1].co = (self.cam_line_end[0], self.cam_line_end[1], self.cam_line_end[2], 1.0)
def update_cam_target(self, context):
"""注視点(ターゲットEmpty)の位置や追従設定を更新する"""
cam = context.scene.camera
if not cam: return
target_obj = None
for const in cam.constraints:
if const.type == 'TRACK_TO':
target_obj = const.target
break
if not target_obj: return
# 既存の Copy Location 制約を一旦すべてクリア
for c in target_obj.constraints:
if c.type == 'COPY_LOCATION':
target_obj.constraints.remove(c)
mode = self.cam_target_mode
if mode == 'OBJECT':
if self.cam_target_obj1:
c1 = target_obj.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
elif mode == 'POINT':
# どのオブジェクトにも追従せず、指定座標に移動
target_obj.location = self.cam_target_loc
elif mode == 'MIDPOINT':
if self.cam_target_obj1:
c1 = target_obj.constraints.new(type='COPY_LOCATION')
c1.target = self.cam_target_obj1
c1.influence = 1.0
if self.cam_target_obj2:
c2 = target_obj.constraints.new(type='COPY_LOCATION')
c2.target = self.cam_target_obj2
# 2つ目の影響度を0.5にすることで、1つ目と2つ目のちょうど中間になる
c2.influence = 0.5
def cam_fov_get(self):
cam = bpy.context.scene.camera
if cam and cam.type == 'CAMERA':
return math.degrees(cam.data.angle)
return 50.0
def cam_fov_set(self, value):
cam = bpy.context.scene.camera
if cam and cam.type == 'CAMERA':
cam.data.lens_unit = 'FOV'
try:
cam.data.angle = math.radians(value)
except:
pass
# =========================================================================
# 【ビュー(画面)の回転制御用コールバック】
# =========================================================================
def get_rv3d(context):
if context.area and context.area.type == 'VIEW_3D':
return context.area.spaces.active.region_3d
for a in context.window.screen.areas:
if a.type == 'VIEW_3D':
return a.spaces.active.region_3d
return None
def view_rotation_euler_get(self):
rv3d = get_rv3d(bpy.context)
if rv3d:
return rv3d.view_rotation.to_euler('XYZ')
return (0.0, 0.0, 0.0)
def view_rotation_euler_set(self, value):
rv3d = get_rv3d(bpy.context)
if rv3d:
target_obj = bpy.context.active_object
if target_obj:
rv3d.view_location = target_obj.matrix_world.to_translation()
euler = mathutils.Euler((value[0], value[1], value[2]), 'XYZ')
rv3d.view_rotation = euler.to_quaternion()
# =========================================================================
# 【プロパティ定義】
# =========================================================================
class PolyTorusProperties(bpy.types.PropertyGroup):
proj_mode: bpy.props.EnumProperty(
name="シミュレーションの種類",
items=[
('MODE1', "モード1 (広域・青)", "枠Aと枠Bを繋ぐすべての直線の範囲"),
('MODE2', "モード2 (点光源・ピンク)", "指定点PからA・Bを通り抜ける視界・範囲")
],
default='MODE2',
update=update_proj
)
proj_p_loc: bpy.props.FloatVectorProperty(name="P 位置", default=(0.0, -50.0, 0.0), update=update_proj_p_transform)
proj_p_rot: bpy.props.FloatVectorProperty(name="P 回転", subtype='EULER', default=(0.0, 0.0, 0.0), update=update_proj_p_transform)
proj_p_radius: bpy.props.FloatProperty(name="球の半径", default=1.0, min=0.1, update=update_proj)
proj_color_p: bpy.props.FloatVectorProperty(name="P 色", subtype='COLOR', default=(1.0, 0.0, 0.0), min=0.0, max=1.0, update=update_proj)
proj_alpha_p: bpy.props.FloatProperty(name="透明度", default=1.0, min=0.0, max=1.0, update=update_proj)
proj_link_p_a: bpy.props.BoolProperty(name="球体Pと平面Aを完全連動 (位置・回転)", default=False, update=update_proj_link_p_a)
proj_p_a_rel_matrix: bpy.props.FloatVectorProperty(name="相対トランスフォーム行列", size=16, default=[0.0]*16)
proj_show_skirt: bpy.props.BoolProperty(name="光の道筋 (スカート) を表示", default=True, update=update_proj)
proj_color_skirt_a: bpy.props.FloatVectorProperty(name="P-A色", subtype='COLOR', default=(1.0, 0.4, 0.1), min=0.0, max=1.0, update=update_proj)
proj_alpha_skirt_a: bpy.props.FloatProperty(name="P-A透明度", default=0.4, min=0.0, max=1.0, update=update_proj)
proj_color_skirt_c: bpy.props.FloatVectorProperty(name="A-C色", subtype='COLOR', default=(1.0, 0.8, 0.2), min=0.0, max=1.0, update=update_proj)
proj_alpha_skirt_c: bpy.props.FloatProperty(name="A-C透明度", default=0.5, min=0.0, max=1.0, update=update_proj)
proj_n_a: bpy.props.IntProperty(name="A 角数 (4=正)", default=4, min=3, max=256, update=update_proj)
proj_r_a: bpy.props.FloatProperty(name="A サイズ", default=10.0, min=0.1, update=update_proj)
proj_thick_a: bpy.props.FloatProperty(name="A 枠の太さ", default=0.2, min=0.01, update=update_proj)
proj_a_loc: bpy.props.FloatVectorProperty(name="A 位置", default=(0.0, -5.0, 0.0), update=update_proj_a_transform)
proj_a_rot: bpy.props.FloatVectorProperty(name="A 回転", subtype='EULER', default=(0.0, 0.0, 0.0), update=update_proj_a_transform)
proj_color_a: bpy.props.FloatVectorProperty(name="A 色", subtype='COLOR', default=(0.0, 0.5, 1.0), min=0.0, max=1.0, update=update_proj)
proj_n_b: bpy.props.IntProperty(name="B 角数 (4=正)", default=4, min=3, max=256, update=update_proj)
proj_r_b: bpy.props.FloatProperty(name="B サイズ", default=10.0, min=0.1, update=update_proj)
proj_thick_b: bpy.props.FloatProperty(name="B 枠の太さ", default=0.2, min=0.01, update=update_proj)
proj_b_loc: bpy.props.FloatVectorProperty(name="B 位置", default=(0.0, 0.0, 0.0), update=update_proj)
proj_b_rot: bpy.props.FloatVectorProperty(name="B 回転", subtype='EULER', default=(0.0, 0.0, 0.0), update=update_proj)
proj_color_b: bpy.props.FloatVectorProperty(name="B 色", subtype='COLOR', default=(0.0, 1.0, 0.5), min=0.0, max=1.0, update=update_proj)
proj_y_c: bpy.props.FloatProperty(name="C (投影先) Y座標", default=10.0, update=update_proj)
proj_show_c: bpy.props.BoolProperty(name="平面C (壁・到達範囲) を表示する", default=True, update=update_proj)
proj_color_c_m1: bpy.props.FloatVectorProperty(name="C 色(モード1)", subtype='COLOR', default=(0.0, 0.5, 1.0), min=0.0, max=1.0, update=update_proj)
proj_color_c_m2: bpy.props.FloatVectorProperty(name="C 色(モード2)", subtype='COLOR', default=(1.0, 0.2, 0.8), min=0.0, max=1.0, update=update_proj)
obj_rot_axis: bpy.props.EnumProperty(
name="回転軸",
items=[
('X', "X軸 (X)", "X軸を回転"),
('Y', "Y軸 (Y)", "Y軸を回転"),
('Z', "Z軸 (Z)", "Z軸を回転")
],
default='Z'
)
view_rot_axis: bpy.props.EnumProperty(
name="画面の回転軸",
items=[
('X', "X軸 (X)", "ワールドX軸周りに回転"),
('Y', "Y軸 (Y)", "ワールドY軸周りに回転"),
('Z', "Z軸 (Z)", "ワールドZ軸周りに回転")
],
default='Z'
)
view_rotation_euler: bpy.props.FloatVectorProperty(
name="画面回転",
subtype='EULER',
unit='ROTATION',
size=3,
get=view_rotation_euler_get,
set=view_rotation_euler_set
)
cam_circle_center: bpy.props.FloatVectorProperty(name="円の中心", default=(0,0,5), update=update_cam_circle)
cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=15.0, min=0.1, update=update_cam_circle)
cam_circle_rotation: bpy.props.FloatVectorProperty(name="円の傾き", subtype='EULER', default=(0,0,0), update=update_cam_circle)
cam_line_start: bpy.props.FloatVectorProperty(name="始点", default=(-15,-15,5), update=update_cam_line)
cam_line_end: bpy.props.FloatVectorProperty(name="終点", default=(15,-15,5), update=update_cam_line)
cam_fov: bpy.props.FloatProperty(
name="水平視野角 (度)",
min=1.0,
max=359.0,
default=50.0,
get=cam_fov_get,
set=cam_fov_set
)
# --- [NEW] 注視点コントロール用プロパティ ---
cam_target_mode: bpy.props.EnumProperty(
name="注視点の指定方法",
items=[
('OBJECT', "単一オブジェクト", "1つのオブジェクトを追従"),
('POINT', "指定座標 (手動)", "任意の座標を注視"),
('MIDPOINT', "2オブジェクトの中間", "2つのオブジェクトの中間を注視")
],
default='OBJECT',
update=update_cam_target
)
cam_target_obj1: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット1", update=update_cam_target)
cam_target_obj2: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット2", update=update_cam_target)
cam_target_loc: bpy.props.FloatVectorProperty(name="注視点 座標", default=(0,0,0), update=update_cam_target)
# =========================================================================
# 【オペレーター】
# =========================================================================
class POLY_OT_create_camera_rig(bpy.types.Operator):
bl_idname = f"object.{PREFIX_NAME.lower()}_create_camera_rig"
bl_label = "カメラ軌道セットアップ"
bl_options = {'REGISTER', 'UNDO'}
rig_type: bpy.props.StringProperty(default='CIRCLE')
def execute(self, context):
target = context.active_object
# ターゲット用のEmptyを生成 (既存のものを再利用せず新規で作成し紐付ける)
bpy.ops.object.empty_add(type='PLAIN_AXES', location=(0,0,0))
target_empty = context.active_object
target_empty.name = "CameraTarget"
# 基準となる位置 (選択オブジェクトがあればその位置、なければ原点)
loc = target.matrix_world.to_translation() if target else mathutils.Vector((0,0,0))
target_empty.location = loc
curve_obj = None
if self.rig_type == 'CIRCLE':
bpy.ops.curve.primitive_nurbs_circle_add(radius=1.0, location=(0,0,0))
curve_obj = context.active_object
curve_obj.name = "CamTrack_Circle"
elif self.rig_type == 'LINE':
curve_data = bpy.data.curves.new('CamTrack_Line', type='CURVE')
curve_data.dimensions = '3D'
spline = curve_data.splines.new('POLY')
spline.points.add(1)
curve_obj = bpy.data.objects.new("CamTrack_Line", curve_data)
context.collection.objects.link(curve_obj)
else: # CURVE
bpy.ops.curve.primitive_bezier_curve_add(radius=15.0, location=loc)
curve_obj = context.active_object
curve_obj.location.y -= 10.0
curve_obj.location.z += 5.0
curve_obj.scale = (2.0, 2.0, 2.0)
curve_obj.name = "CamTrack_Curve"
bpy.ops.object.camera_add(location=(0,0,0))
cam_obj = context.active_object
cam_obj.name = f"TrackingCamera_{self.rig_type}"
cam_obj.rotation_mode = 'XYZ'
const_path = cam_obj.constraints.new(type='FOLLOW_PATH')
const_path.target = curve_obj
const_path.use_curve_follow = False
const_path.use_fixed_location = True
const_path.offset_factor = 0.0
const_track = cam_obj.constraints.new(type='TRACK_TO')
const_track.target = target_empty
const_track.track_axis = 'TRACK_NEGATIVE_Z'
const_track.up_axis = 'UP_Y'
context.scene.camera = cam_obj
cam_obj.data.dof.use_dof = True
cam_obj.data.dof.focus_object = target_empty
cam_obj.data.dof.aperture_fstop = 1.8
# --- プロパティの初期化 ---
props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
# ターゲット設定の初期化
props.cam_target_mode = 'OBJECT'
if target and target.type not in ['CAMERA', 'LIGHT'] and target != target_empty:
props.cam_target_obj1 = target
else:
props.cam_target_obj1 = None
update_cam_target(props, context)
# 軌道設定の初期化
if self.rig_type == 'CIRCLE':
props.cam_circle_center = (loc.x, loc.y, loc.z + 5.0)
props.cam_circle_radius = 15.0
props.cam_circle_rotation = (0, 0, 0)
elif self.rig_type == 'LINE':
props.cam_line_start = (loc.x - 15.0, loc.y - 15.0, loc.z + 5.0)
props.cam_line_end = (loc.x + 15.0, loc.y - 15.0, loc.z + 5.0)
for a in context.window.screen.areas:
if a.type == 'VIEW_3D':
a.spaces.active.region_3d.view_perspective = 'CAMERA'
break
self.report({'INFO'}, f"{self.rig_type}軌道のカメラセットアップが完了しました。")
return {'FINISHED'}
class POLY_OT_reset_view_and_p(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_NAME.lower()}_reset_view_and_p"
bl_label = "選択したオブジェクトを中心にPを配置 & ビュー初期化"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
global IS_UPDATING
props = context.scene.poly_torus_props
target_area = None
target_region = None
for a in context.window.screen.areas:
if a.type == 'VIEW_3D':
target_area = a
for r in a.regions:
if r.type == 'WINDOW':
target_region = r
break
break
if target_area is None or target_region is None: return {'CANCELLED'}
rv3d = target_area.spaces.active.region_3d
target_obj = context.active_object
if target_obj:
center_loc = target_obj.matrix_world.to_translation()
radius = target_obj.dimensions.length
new_distance = max(radius * 1.5, 10.0)
else:
center_loc = rv3d.view_location.copy()
new_distance = 30.0
IS_UPDATING = True
try:
props.proj_p_loc = center_loc
if props.proj_link_p_a:
mat_rel = mathutils.Matrix([props.proj_p_a_rel_matrix[i:i+4] for i in range(0, 16, 4)])
mat_p = get_matrix(props.proj_p_loc, props.proj_p_rot)
mat_a_new = mat_p @ mat_rel
loc_a, rot_a = decompose_matrix(mat_a_new)
props.proj_a_loc = loc_a
props.proj_a_rot = rot_a
rv3d.view_location = center_loc
rv3d.view_rotation = mathutils.Euler((math.radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
rv3d.view_distance = new_distance
rv3d.view_perspective = 'PERSP'
finally:
IS_UPDATING = False
context.view_layer.update()
update_proj(self, context)
context.evaluated_depsgraph_get().update()
return {'FINISHED'}
class POLY_OT_rotate_selected(bpy.types.Operator):
bl_idname = f"object.{PREFIX_NAME.lower()}_rotate_selected"
bl_label = "選択オブジェクトを指定軸で回転"
bl_options = {'REGISTER', 'UNDO'}
angle: bpy.props.FloatProperty(name="Angle (Deg)", default=90.0)
def execute(self, context):
props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
axis = props.obj_rot_axis
rad = math.radians(self.angle)
for obj in context.selected_objects:
if obj.rotation_mode != 'XYZ':
obj.rotation_mode = 'XYZ'
if axis == 'X': obj.rotation_euler.x += rad
elif axis == 'Y': obj.rotation_euler.y += rad
elif axis == 'Z': obj.rotation_euler.z += rad
return {'FINISHED'}
class POLY_OT_rotate_view(bpy.types.Operator):
bl_idname = f"view3d.{PREFIX_NAME.lower()}_rotate_view"
bl_label = "画面を指定軸で回転"
bl_options = {'REGISTER', 'UNDO'}
angle: bpy.props.FloatProperty(name="Angle (Deg)", default=15.0)
def execute(self, context):
props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
axis = props.view_rot_axis
rad = math.radians(self.angle)
rv3d = get_rv3d(context)
if not rv3d: return {'CANCELLED'}
target_obj = context.active_object
if target_obj:
rv3d.view_location = target_obj.matrix_world.to_translation()
if axis == 'X': vec = (1.0, 0.0, 0.0)
elif axis == 'Y': vec = (0.0, 1.0, 0.0)
else: vec = (0.0, 0.0, 1.0)
q_rot = mathutils.Quaternion(vec, rad)
rv3d.view_rotation = q_rot @ rv3d.view_rotation
return {'FINISHED'}
class POLY_OT_open_url(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_NAME.lower()}_open_url"
bl_label = "URL"
url: bpy.props.StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class POLY_OT_remove_addon(bpy.types.Operator):
bl_idname = f"wm.{PREFIX_NAME.lower()}_remove_addon"
bl_label = "アドオン削除"
def execute(self, context): unregister(); return {'FINISHED'}
# =========================================================================
# 【UIパネル】
# =========================================================================
class POLY_PT_main_panel(bpy.types.Panel):
bl_idname = f"{PREFIX_NAME.upper()}_PT_main_panel"
bl_label = "多角形ツール"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
# 1. 画面中央戻りボタン
box_action = layout.box()
box_action.label(text="ビューとオブジェクト操作:", icon='VIEW_CAMERA')
col_action = box_action.column()
col_action.scale_y = 1.3
col_action.operator(
f"view3d.{PREFIX_NAME.lower()}_reset_view_and_p",
text="選択中心にPを配置 & ビュー初期化",
icon='ZOOM_ALL'
)
layout.separator(factor=1.5)
# 2. 選択オブジェクトの回転操作
box_rot = layout.box()
box_rot.label(text="選択オブジェクトの回転:", icon='ORIENTATION_GIMBAL')
box_rot.prop(props, "obj_rot_axis", expand=True)
target_obj = context.active_object
if target_obj:
axis = props.obj_rot_axis
col_rot = box_rot.column(align=True)
if axis == 'X': col_rot.prop(target_obj, "rotation_euler", index=0, text="X 回転角度")
elif axis == 'Y': col_rot.prop(target_obj, "rotation_euler", index=1, text="Y 回転角度")
elif axis == 'Z': col_rot.prop(target_obj, "rotation_euler", index=2, text="Z 回転角度")
row_rot = box_rot.row(align=True)
row_rot.operator(f"object.{PREFIX_NAME.lower()}_rotate_selected", text="-90°").angle = -90.0
row_rot.operator(f"object.{PREFIX_NAME.lower()}_rotate_selected", text="-15°").angle = -15.0
row_rot.operator(f"object.{PREFIX_NAME.lower()}_rotate_selected", text="+15°").angle = 15.0
row_rot.operator(f"object.{PREFIX_NAME.lower()}_rotate_selected", text="+90°").angle = 90.0
else:
box_rot.label(text="※オブジェクトを選択してください", icon='INFO')
layout.separator(factor=1.5)
# 3. 画面(ビュー)の回転操作
box_vrot = layout.box()
box_vrot.label(text="画面(ビュー)自体の回転:", icon='VIEW3D')
box_vrot.prop(props, "view_rot_axis", expand=True)
axis_v = props.view_rot_axis
col_vrot = box_vrot.column(align=True)
if axis_v == 'X': col_vrot.prop(props, "view_rotation_euler", index=0, text="画面 X 回転")
elif axis_v == 'Y': col_vrot.prop(props, "view_rotation_euler", index=1, text="画面 Y 回転")
elif axis_v == 'Z': col_vrot.prop(props, "view_rotation_euler", index=2, text="画面 Z 回転")
row_vrot = box_vrot.row(align=True)
row_vrot.operator(f"view3d.{PREFIX_NAME.lower()}_rotate_view", text="-90°").angle = -90.0
row_vrot.operator(f"view3d.{PREFIX_NAME.lower()}_rotate_view", text="-15°").angle = -15.0
row_vrot.operator(f"view3d.{PREFIX_NAME.lower()}_rotate_view", text="+15°").angle = 15.0
row_vrot.operator(f"view3d.{PREFIX_NAME.lower()}_rotate_view", text="+90°").angle = 90.0
layout.separator(factor=1.5)
# 4. カメラ軌道&てんこ盛り設定
box_cam = layout.box()
box_cam.label(text="カメラ軌道・てんこもり設定:", icon='CAMERA_DATA')
row_cam_btn = box_cam.row(align=True)
row_cam_btn.operator(f"object.{PREFIX_NAME.lower()}_create_camera_rig", text="円周軌道", icon='MESH_CIRCLE').rig_type = 'CIRCLE'
row_cam_btn.operator(f"object.{PREFIX_NAME.lower()}_create_camera_rig", text="線分軌道", icon='CURVE_PATH').rig_type = 'LINE'
row_cam_btn.operator(f"object.{PREFIX_NAME.lower()}_create_camera_rig", text="曲線軌道", icon='CURVE_BEZCURVE').rig_type = 'CURVE'
cam = context.scene.camera
if cam and cam.type == 'CAMERA':
box_cam.separator()
box_cam.label(text=f"操作中のカメラ: {cam.name}", icon='VIEW_CAMERA')
curve_obj = None
target_obj = None
for const in cam.constraints:
if const.type == 'FOLLOW_PATH':
curve_obj = const.target
box_cam.prop(const, "offset_factor", text="軌道上の移動 (0~1)", slider=True)
elif const.type == 'TRACK_TO':
target_obj = const.target
box_cam.separator()
# --- 軌道(円/線分)の詳細設定 ---
if curve_obj:
box_curve = box_cam.box()
box_curve.label(text=f"軌道({curve_obj.name})の調整:", icon='CURVE_DATA')
col_curve = box_curve.column(align=True)
if "CamTrack_Circle" in curve_obj.name:
col_curve.prop(props, "cam_circle_center", text="円の中心")
col_curve.prop(props, "cam_circle_radius", text="円の半径")
col_curve.prop(props, "cam_circle_rotation", text="円の傾き (XYZ)")
elif "CamTrack_Line" in curve_obj.name:
col_curve.prop(props, "cam_line_start", text="始点")
col_curve.prop(props, "cam_line_end", text="終点")
else:
col_curve.prop(curve_obj, "scale", text="軌道の広さ(スケール)")
col_curve.prop(curve_obj, "location", text="軌道の位置(移動)")
col_curve.prop(curve_obj, "rotation_euler", text="軌道の傾き(回転)")
# --- [NEW] 注視点の詳細設定 ---
if target_obj:
box_target = box_cam.box()
box_target.label(text=f"注視点({target_obj.name})の設定:", icon='EMPTY_DATA')
# モード切り替え (ラジオボタン)
box_target.prop(props, "cam_target_mode", expand=True)
col_target = box_target.column(align=True)
mode = props.cam_target_mode
if mode == 'OBJECT':
col_target.prop(props, "cam_target_obj1", text="追従オブジェクト")
elif mode == 'POINT':
col_target.prop(props, "cam_target_loc", text="指定座標(XYZ)")
elif mode == 'MIDPOINT':
col_target.prop(props, "cam_target_obj1", text="オブジェクト 1")
col_target.prop(props, "cam_target_obj2", text="オブジェクト 2")
box_cam.separator()
# --- カメラのズームと視野角 (度) ---
col_lens = box_cam.column(align=True)
col_lens.prop(cam.data, "lens", text="ズーム (焦点距離 mm)")
col_lens.prop(props, "cam_fov", text="水平視野角 (度)")
box_cam.separator()
# --- カメラの視線方向 (Pitch/Roll/Yaw) ---
box_sight = box_cam.box()
box_sight.label(text="視線方向 (Pitch/Roll/Yaw):", icon='ORIENTATION_GIMBAL')
track_const = None
for const in cam.constraints:
if const.type == 'TRACK_TO':
track_const = const
break
if track_const:
box_sight.prop(track_const, "mute", text="ターゲット注視を解除 (手動で回転)", toggle=True, icon='UNLINKED')
col_sight = box_sight.column(align=True)
col_sight.enabled = track_const.mute
col_sight.prop(cam, "rotation_euler", index=0, text="Pitch (上下・X)")
col_sight.prop(cam, "rotation_euler", index=1, text="Roll (傾き・Y)")
col_sight.prop(cam, "rotation_euler", index=2, text="Yaw (左右・Z)")
else:
col_sight = box_sight.column(align=True)
col_sight.prop(cam, "rotation_euler", index=0, text="Pitch (上下・X)")
col_sight.prop(cam, "rotation_euler", index=1, text="Roll (傾き・Y)")
col_sight.prop(cam, "rotation_euler", index=2, text="Yaw (左右・Z)")
box_cam.separator()
# --- カメラの被写界深度 (ボケ) ---
box_cam.prop(cam.data.dof, "use_dof", text="被写界深度 (ボケ) を有効化", toggle=True, icon='STYLUS_PRESSURE')
if cam.data.dof.use_dof:
col_dof = box_cam.column(align=True)
# ピント合わせの対象は自動的に target_obj になっている
col_dof.prop(cam.data.dof, "focus_object", text="ピント対象")
if not cam.data.dof.focus_object:
col_dof.prop(cam.data.dof, "focus_distance", text="ピント距離")
col_dof.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
col_dof.prop(cam.data.dof, "aperture_blades", text="絞り羽根数")
else:
box_cam.label(text="※アクティブなカメラがありません", icon='INFO')
layout.separator(factor=1.5)
# 5. システム / リンク設定
box_sys = layout.box()
box_sys.label(text="システム / リンク:", icon='PREFERENCES')
box_sys.operator(f"wm.{PREFIX_NAME.lower()}_open_url", text="アドオン削除パネル 20260704", icon='URL').url = "<https://app.notion.com/p/20260704-390f5dacaf4380e6939dd28e6e2ff91d>"
box_sys.operator(f"wm.{PREFIX_NAME.lower()}_open_url", text="ドキュメント (トーラス作成)", icon='URL').url = "<https://app.notion.com/p/390f5dacaf43801d8f08c695979e60e1>"
box_sys.separator()
box_sys.operator(
f"wm.{PREFIX_NAME.lower()}_remove_addon",
text="アドオンを無効化して閉じる",
icon='CANCEL'
)
# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
PolyTorusProperties,
POLY_OT_create_camera_rig,
POLY_OT_reset_view_and_p,
POLY_OT_rotate_selected,
POLY_OT_rotate_view,
POLY_OT_open_url,
POLY_OT_remove_addon,
POLY_PT_main_panel,
]
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, f"{PREFIX_NAME.lower()}_props", bpy.props.PointerProperty(type=PolyTorusProperties))
def unregister():
if hasattr(bpy.types.Scene, f"{PREFIX_NAME.lower()}_props"): delattr(bpy.types.Scene, f"{PREFIX_NAME.lower()}_props")
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except: pass
if __name__ == "__main__":
try: unregister()
except: pass
register()