Sidebar (Nパネル)", "description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション", "category": "Object", } import bpy import webbrowser import math import bmesh import mathutils # ========================================================================= # 【基本設定】 # ========================================================================= TAB_NAME = "多角形ツール" PREFIX_NAME = "poly_torus" ADDON_LINKS = [ { "label": "トーラス作成", "url": "https://app.notion.com/p/390f5dacaf43801d8f08c695979e60e1", "icon": "URL" } ] INIT_COLOR = (0.0, 1.0, 0.5) IS_UPDATING = False # ========================================================================= # 【マトリクス・座標変換ヘルパー】 # ========================================================================= def get_matrix(loc, rot): """位置とオイラー回転(ラジアン)から4x4"> Sidebar (Nパネル)", "description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション", "category": "Object", } import bpy import webbrowser import math import bmesh import mathutils # ========================================================================= # 【基本設定】 # ========================================================================= TAB_NAME = "多角形ツール" PREFIX_NAME = "poly_torus" ADDON_LINKS = [ { "label": "トーラス作成", "url": "https://app.notion.com/p/390f5dacaf43801d8f08c695979e60e1", "icon": "URL" } ] INIT_COLOR = (0.0, 1.0, 0.5) IS_UPDATING = False # ========================================================================= # 【マトリクス・座標変換ヘルパー】 # ========================================================================= def get_matrix(loc, rot): """位置とオイラー回転(ラジアン)から4x4"> Sidebar (Nパネル)", "description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション", "category": "Object", } import bpy import webbrowser import math import bmesh import mathutils # ========================================================================= # 【基本設定】 # ========================================================================= TAB_NAME = "多角形ツール" PREFIX_NAME = "poly_torus" ADDON_LINKS = [ { "label": "トーラス作成", "url": "https://app.notion.com/p/390f5dacaf43801d8f08c695979e60e1", "icon": "URL" } ] INIT_COLOR = (0.0, 1.0, 0.5) IS_UPDATING = False # ========================================================================= # 【マトリクス・座標変換ヘルパー】 # ========================================================================= def get_matrix(loc, rot): """位置とオイラー回転(ラジアン)から4x4">

bl_info = {
"name": "Polygon Torus Generator Pro (Realtime & Detach)",
"author": "Your Name",
"version": (5, 8),
"blender": (4, 2, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション",
"category": "Object",
}
import bpy
import webbrowser
import math
import bmesh
import mathutils
# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME = "多角形ツール"
PREFIX_NAME = "poly_torus"
ADDON_LINKS = [
{
"label": "トーラス作成",
"url": "<https://app.notion.com/p/390f5dacaf43801d8f08c695979e60e1>",
"icon": "URL"
}
]
INIT_COLOR = (0.0, 1.0, 0.5)
IS_UPDATING = False
# =========================================================================
# 【マトリクス・座標変換ヘルパー】
# =========================================================================
def get_matrix(loc, rot):
"""位置とオイラー回転(ラジアン)から4x4行列を作成"""
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):
"""4x4行列から位置とオイラー回転を取得"""
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):
"""数学的なパラメータから多角形トーラスを直接BMeshで生成"""
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):
"""球体(光源P)をBMeshでオペレータ不使用で安全生成"""
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):
"""平面上(Y=0)のローカルポリゴン頂点座標を取得"""
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):
"""Pから平面Aへのスカート (底面なしコーン)"""
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):
"""平面Aから平面Cへのスカート (底面・上面なし錐台 / 円錐台)"""
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):
"""任意の3D点集合A、Bを結ぶ直線をY=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
# 平面Aのワールド法線
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()
# 1. Pから平面Aへのスカート (コーン)
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)
# 2. 平面Aから平面Cへのスカート (錐台)
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_poly_torus(self, context):
global IS_UPDATING
if IS_UPDATING: return
props = context.scene.poly_torus_props
if not props.active_obj_name: return
obj = bpy.data.objects.get(props.active_obj_name)
if not obj or not obj.get("pt_live"): return
IS_UPDATING = True
try:
obj.location = (props.loc_x, props.loc_y, props.loc_z)
obj.rotation_euler = (props.rot_x, props.rot_y, props.rot_z)
mat = obj.active_material
if mat and mat.use_nodes:
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = (*props.color, 1.0)
bsdf.inputs["Alpha"].default_value = props.alpha
mat.diffuse_color = (*props.color, props.alpha)
try: mat.surface_render_method = 'BLENDED' if props.alpha < 1.0 else 'DITHERED'
except: pass
try: mat.blend_method = 'BLEND' if props.alpha < 1.0 else 'OPAQUE'
except: pass
mesh_new = get_torus_mesh_pure(props.segments, props.radius, props.thick)
old_mesh = obj.data
obj.data = mesh_new
if mat: obj.data.materials.append(mat)
if old_mesh: bpy.data.meshes.remove(old_mesh)
finally:
IS_UPDATING = False
# =========================================================================
# 【プロパティ定義】
# =========================================================================
class PolyTorusProperties(bpy.types.PropertyGroup):
active_obj_name: bpy.props.StringProperty(name="編集中のオブジェクト", default="")
segments: bpy.props.IntProperty(name="角数", default=3, min=3, max=256, update=update_poly_torus)
radius: bpy.props.FloatProperty(name="半径 (R)", default=1.0, min=0.0001, update=update_poly_torus)
thick: bpy.props.FloatProperty(name="太さ (断面)", default=0.05, min=0.001, update=update_poly_torus)
multiplier_n: bpy.props.IntProperty(name="n =", default=2, min=1)
color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=INIT_COLOR, min=0.0, max=1.0, update=update_poly_torus)
alpha: bpy.props.FloatProperty(name="透明度", default=1.0, min=0.0, max=1.0, update=update_poly_torus)
loc_x: bpy.props.FloatProperty(name="位置 X", default=0.0, update=update_poly_torus)
loc_y: bpy.props.FloatProperty(name="位置 Y", default=0.0, update=update_poly_torus)
loc_z: bpy.props.FloatProperty(name="位置 Z", default=0.0, update=update_poly_torus)
rot_x: bpy.props.FloatProperty(name="X軸", default=0.0, subtype='ANGLE', update=update_poly_torus)
rot_y: bpy.props.FloatProperty(name="Y軸", default=0.0, subtype='ANGLE', update=update_poly_torus)
rot_z: bpy.props.FloatProperty(name="Z軸", default=0.0, subtype='ANGLE', update=update_poly_torus)
proj_mode: bpy.props.EnumProperty(
name="シミュレーションの種類",
items=[
('MODE1', "モード1 (広域・青)", "枠Aと枠Bを繋ぐすべての直線の範囲"),
('MODE2', "モード2 (点光源・ピンク)", "指定点PからA・Bを通り抜ける視界・範囲")
],
default='MODE1',
update=update_proj
)
# 指定点P (位置と回転 - 初期位置 Y=-50.0 に変更)
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)
# スカート1 (P-A 軌跡/コーン) 設定
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)
# スカート2 (A-C 軌跡/錐台) 設定
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)
# 枠A (位置・回転を3D化)
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)
# 枠B (位置・回転を3D化)
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)
# 平面C (到達範囲)
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)
# =========================================================================
# 【オペレーター】
# =========================================================================
class POLY_OT_reset_view_and_p(bpy.types.Operator):
"""球体Pを現在の画面注視点にリセットし、フロント・透視投影ビューにする"""
bl_idname = f"view3d.{PREFIX_NAME.lower()}_reset_view_and_p"
bl_label = "視線と球体Pを初期化"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.poly_torus_props
# 3Dビューポートの領域情報(region_3d)を取得
rv3d = None
for area in context.screen.areas:
if area.type == 'VIEW_3D':
rv3d = area.spaces.active.region_3d
break
if not rv3d:
self.report({'WARNING'}, "3Dビューポートが見つかりません。")
return {'CANCELLED'}
# 1. 球体Pを画面の真ん中(現在の注視点の座標)に移動
props.proj_p_loc = rv3d.view_location
# 2. 視線角度を「Y軸プラスが奥行き」のフロント方向に切り替え (オイラー角: 90, 0, 0 の回転)
rv3d.view_rotation = mathutils.Quaternion((0.7071068, 0.7071068, 0.0, 0.0))
# 3. 投影モードを透視投影(Perspective)に設定
rv3d.view_perspective = 'PERSP'
# 4. スカート等の表示シミュレーションを更新
update_proj(self, context)
self.report({'INFO'}, "ビューと球体Pをリセットしました。")
return {'FINISHED'}
class POLY_OT_create_projection(bpy.types.Operator):
bl_idname = f"mesh.{PREFIX_NAME.lower()}_create_proj"
bl_label = "投影平面を生成"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.poly_torus_props
col = context.collection
mat_a = get_or_create_mat("Proj_Mat_A", props.proj_color_a, 1.0)
mat_b = get_or_create_mat("Proj_Mat_B", props.proj_color_b, 1.0)
mat_c = get_or_create_mat("Proj_Mat_C", props.proj_color_c_m1, 0.8)
mat_p = get_or_create_mat("Proj_Mat_P", props.proj_color_p, props.proj_alpha_p)
mat_skirt_a = get_or_create_mat("Proj_Mat_Skirt_A", props.proj_color_skirt_a, props.proj_alpha_skirt_a)
mat_skirt_c = get_or_create_mat("Proj_Mat_Skirt_C", props.proj_color_skirt_c, props.proj_alpha_skirt_c)
def create_or_get(name, mat):
obj = bpy.data.objects.get(name)
if not obj:
obj = bpy.data.objects.new(name, bpy.data.meshes.new(f"{name}_Mesh"))
col.objects.link(obj)
if len(obj.data.materials) == 0:
obj.data.materials.append(mat)
return obj
create_or_get("Proj_Plane_A", mat_a)
create_or_get("Proj_Plane_B", mat_b)
create_or_get("Proj_Plane_C", mat_c)
create_or_get("Proj_Point_P", mat_p)
create_or_get("Proj_Skirt_A", mat_skirt_a)
create_or_get("Proj_Skirt_C", mat_skirt_c)
update_proj(self, context)
return {'FINISHED'}
class POLY_OT_create_torus(bpy.types.Operator):
bl_idname = f"mesh.{PREFIX_NAME.lower()}_create"
bl_label = "単体多角形を生成"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
global IS_UPDATING
props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
if props.active_obj_name:
old_obj = bpy.data.objects.get(props.active_obj_name)
if old_obj and "pt_live" in old_obj: del old_obj["pt_live"]
mat = get_or_create_mat(f"{PREFIX_NAME}_Mat", props.color, props.alpha)
IS_UPDATING = True
try:
mesh = get_torus_mesh_pure(props.segments, props.radius, props.thick)
obj = bpy.data.objects.new("PolyTorus", mesh)
context.collection.objects.link(obj)
obj.data.materials.append(mat)
obj["pt_live"] = True
obj.location = (props.loc_x, props.loc_y, props.loc_z)
obj.rotation_euler = (props.rot_x, props.rot_y, props.rot_z)
props.active_obj_name = obj.name
context.view_layer.objects.active = obj
obj.select_set(True)
finally: IS_UPDATING = False
return {'FINISHED'}
class POLY_OT_detach_torus(bpy.types.Operator):
bl_idname = f"object.{PREFIX_NAME.lower()}_detach"
bl_label = "アドオンから切り離し"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
global IS_UPDATING
props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
obj = bpy.data.objects.get(props.active_obj_name)
if obj and "pt_live" in obj: del obj["pt_live"]
IS_UPDATING = True; props.active_obj_name = ""; IS_UPDATING = False
return {'FINISHED'}
class POLY_OT_modify_radius(bpy.types.Operator):
bl_idname = f"object.{PREFIX_NAME.lower()}_modify_radius"
bl_label = "半径を数値修飾"
bl_options = {'REGISTER', 'UNDO'}
action: bpy.props.EnumProperty(items=[('MULTIPLY', "Multiply", ""), ('DIVIDE', "Divide", "")])
def execute(self, context):
props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
if props.multiplier_n <= 0: return {'CANCELLED'}
factor = math.sqrt(props.multiplier_n)
if self.action == 'MULTIPLY': props.radius *= factor
elif self.action == 'DIVIDE': props.radius /= factor
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_projection_panel(bpy.types.Panel):
bl_idname = f"{PREFIX_NAME.upper()}_PT_projection_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")
# 一番上に、ビューリセット&球体Pの初期化ボタンを配置
layout.operator(f"view3d.{PREFIX_NAME.lower()}_reset_view_and_p", text="視点と球体Pの位置をリセット", icon='VIEW_CAMERA')
layout.separator()
layout.operator(f"mesh.{PREFIX_NAME.lower()}_create_proj", text="シミュレーションを生成", icon='LIGHT_SUN')
layout.separator()
layout.prop(props, "proj_mode", expand=False)
if props.proj_mode == 'MODE2':
box_p = layout.box()
box_p.label(text="指定点 P (球体・光源)", icon='LIGHT')
col_p = box_p.column(align=True)
col_p.prop(props, "proj_p_loc", text="位置")
col_p.prop(props, "proj_p_rot", text="回転")
col_p.prop(props, "proj_link_p_a", text="球体Pと平面Aを完全連動 (位置・回転)", icon='LINKED')
col_p.prop(props, "proj_p_radius")
row_p = col_p.row(align=True)
row_p.prop(props, "proj_color_p", text="")
row_p.prop(props, "proj_alpha_p", text="透明度")
layout.separator()
if props.proj_show_skirt:
box_s = layout.box()
box_s.label(text="光の道筋 (スカート) 設定", icon='OUTLINER_OB_LIGHT')
box_s.prop(props, "proj_show_skirt", text="スカートを表示する")
# スカートAの設定項目
col_sa = box_s.column(align=True)
col_sa.label(text="P-A 軌跡 (コーン):")
row_sa = col_sa.row(align=True)
row_sa.prop(props, "proj_color_skirt_a", text="")
row_sa.prop(props, "proj_alpha_skirt_a", text="透明度")
# スカートCの設定項目
col_sc = box_s.column(align=True)
col_sc.label(text="A-C 軌跡 (円錐台):")
row_sc = col_sc.row(align=True)
row_sc.prop(props, "proj_color_skirt_c", text="")
row_sc.prop(props, "proj_alpha_skirt_c", text="透明度")
else:
layout.prop(props, "proj_show_skirt", icon='OUTLINER_OB_LIGHT')
layout.separator()
box_a = layout.box()
box_a.label(text="平面A (窓枠1)", icon='MESH_TORUS')
col_a = box_a.column(align=True)
col_a.prop(props, "proj_n_a")
col_a.prop(props, "proj_r_a")
col_a.prop(props, "proj_thick_a")
col_a.separator()
col_a.prop(props, "proj_a_loc", text="位置")
col_a.prop(props, "proj_a_rot", text="回転")
col_a.separator()
col_a.prop(props, "proj_color_a", text="A 色")
box_b = layout.box()
box_b.label(text="平面B (窓枠2)", icon='MESH_TORUS')
col_b = box_b.column(align=True)
col_b.prop(props, "proj_n_b")
col_b.prop(props, "proj_r_b")
col_b.prop(props, "proj_thick_b")
col_b.separator()
col_b.prop(props, "proj_b_loc", text="位置")
col_b.prop(props, "proj_b_rot", text="回転")
col_b.separator()
col_b.prop(props, "proj_color_b", text="B 色")
box_c = layout.box()
box_c.prop(props, "proj_show_c", icon='RESTRICT_VIEW_OFF' if props.proj_show_c else 'RESTRICT_VIEW_ON')
if props.proj_show_c:
col = box_c.column(align=True)
col.prop(props, "proj_y_c", text="Cの Y座標")
if props.proj_mode == 'MODE1':
col.prop(props, "proj_color_c_m1", text="C 色 (青系)")
else:
col.prop(props, "proj_color_c_m2", text="C 色 (ピンク系)")
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
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
if props.active_obj_name and bpy.data.objects.get(props.active_obj_name):
box_live = layout.box()
box_live.label(text=f"🔄 編集中: {props.active_obj_name}", icon='EDITMODE_HLT')
box_live.scale_y = 1.3
box_live.operator(f"object.{PREFIX_NAME.lower()}_detach", text="アドオンから切り離し", icon='UNLINKED')
layout.separator()
else:
layout.scale_y = 1.3
layout.operator(f"mesh.{PREFIX_NAME.lower()}_create", text="新しく単体多角形を生成", icon='ADD')
layout.scale_y = 1.0
layout.separator()
box_s = layout.box()
col_s = box_s.column(align=True)
col_s.prop(props, "segments")
col_s.separator()
col_s.prop(props, "radius")
row_n = col_s.row(align=True)
row_n.prop(props, "multiplier_n")
op_mult = row_n.operator(f"object.{PREFIX_NAME.lower()}_modify_radius", text="× √n")
op_mult.action = 'MULTIPLY'
op_div = row_n.operator(f"object.{PREFIX_NAME.lower()}_modify_radius", text="× 1/√n")
op_div.action = 'DIVIDE'
col_s.separator()
col_s.prop(props, "thick")
col_s.separator()
row_c = col_s.row(align=True)
row_c.prop(props, "color", text="")
row_c.prop(props, "alpha", text="透明")
class POLY_PT_footer_panel(bpy.types.Panel):
bl_idname = f"{PREFIX_NAME.upper()}_PT_footer_panel"
bl_label = "ドキュメント / 管理"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_order = 100
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
box_links = layout.box()
box_links.label(text="関連リンク:", icon='BOOKMARKS')
for link in ADDON_LINKS:
box_links.operator(f"wm.{PREFIX_NAME.lower()}_open_url", text=link["label"], icon='URL').url = link["url"]
layout.separator(factor=2.0)
layout.operator(f"wm.{PREFIX_NAME.lower()}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')
# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
PolyTorusProperties,
POLY_OT_reset_view_and_p,
POLY_OT_create_torus,
POLY_OT_create_projection,
POLY_OT_detach_torus,
POLY_OT_modify_radius,
POLY_OT_open_url,
POLY_OT_remove_addon,
POLY_PT_projection_panel,
POLY_PT_main_panel,
POLY_PT_footer_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()