進化版 平面2つ通過光線
シュウセイチュウ
bl_info = {
"name": "Polygon Torus Generator Pro (Realtime & Detach)",
"author": "Your Name",
"version": (5, 2),
"blender": (4, 2, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション",
"category": "Object",
}
import bpy
import webbrowser
import math
import bmesh
# =========================================================================
# 【基本設定】
# =========================================================================
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_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(n, r, y_pos):
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), y_pos, 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_skirt_mesh(verts, y_c, fill_bottom, mesh_name):
mesh = bpy.data.meshes.new(mesh_name)
if len(verts) < 4: return mesh
bm = bmesh.new()
for v in verts:
bm.verts.new(v)
try: bmesh.ops.convex_hull(bm, input=bm.verts)
except: pass
if not fill_bottom:
faces_to_delete = []
for face in bm.faces:
is_bottom = True
for v in face.verts:
if abs(v.co.y - y_c) > 0.001:
is_bottom = False
break
if is_bottom:
faces_to_delete.append(face)
if faces_to_delete:
bmesh.ops.delete(bm, geom=faces_to_delete, context='FACES')
bm.to_mesh(mesh)
bm.free()
return mesh
def get_torus_mesh_by_ops(n, r, thick, context):
area = next((a for a in context.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
old_active = context.view_layer.objects.active
radius = r * math.sqrt(2) if n == 4 else r
temp_obj = None
override_kwargs = {}
if area: override_kwargs['area'] = area
if region: override_kwargs['region'] = region
with context.temp_override(**override_kwargs):
try:
bpy.ops.mesh.primitive_torus_add(
major_segments=n, minor_segments=12,
major_radius=radius, minor_radius=thick,
location=(0,0,0), rotation=(0,0,0)
)
temp_obj = context.active_object
except: pass
if temp_obj:
new_mesh = temp_obj.data.copy()
bpy.data.objects.remove(temp_obj)
if old_active: context.view_layer.objects.active = old_active
return new_mesh
return bpy.data.meshes.new("FallbackTorus")
def get_sphere_mesh_by_ops(r, context):
area = next((a for a in context.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
old_active = context.view_layer.objects.active
temp_obj = None
override_kwargs = {}
if area: override_kwargs['area'] = area
if region: override_kwargs['region'] = region
with context.temp_override(**override_kwargs):
try:
bpy.ops.mesh.primitive_ico_sphere_add(radius=r, subdivisions=3, location=(0,0,0))
temp_obj = context.active_object
except: pass
if temp_obj:
new_mesh = temp_obj.data.copy()
bpy.data.objects.remove(temp_obj)
if old_active: context.view_layer.objects.active = old_active
return new_mesh
return bpy.data.meshes.new("FallbackSphere")
# =========================================================================
# 【数学・幾何学ロジック (シミュレーション計算)】
# =========================================================================
def get_projected_verts_mode1(verts_a, verts_b, y_a, y_b, y_c):
if abs(y_b - y_a) < 0.0001: return []
t = (y_c - y_a) / (y_b - y_a)
pts = []
for pa in verts_a:
for pb in verts_b:
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 = bpy.data.objects.get("Proj_Skirt")
if not (obj_a and obj_b and obj_c and obj_p and obj_skirt): return
IS_UPDATING = True
try:
n_a, r_a, thick_a, y_a = props.proj_n_a, props.proj_r_a, props.proj_thick_a, props.proj_y_a
n_b, r_b, thick_b, y_b = props.proj_n_b, props.proj_r_b, props.proj_thick_b, props.proj_y_b
y_c = props.proj_y_c
def update_torus(obj, n, r, thick, y_pos, color):
mesh = get_torus_mesh_by_ops(n, r, thick, context)
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)
obj.location = (0, y_pos, 0)
obj.rotation_euler = (math.pi / 2, math.pi / 4 if n == 4 else 0.0, 0.0)
if old_m: bpy.data.meshes.remove(old_m)
update_torus(obj_a, n_a, r_a, thick_a, y_a, props.proj_color_a)
update_torus(obj_b, n_b, r_b, thick_b, y_b, props.proj_color_b)
if props.proj_mode == 'MODE2':
obj_p.hide_viewport = False
mesh_p = get_sphere_mesh_by_ops(props.proj_p_radius, context)
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)
obj_p.location = props.proj_p_loc
else:
obj_p.hide_viewport = True
va = get_polygon_verts(n_a, r_a, y_a)
vb = get_polygon_verts(n_b, r_b, y_b)
if props.proj_mode == 'MODE1':
vc = get_projected_verts_mode1(va, vb, y_a, y_b, 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 and len(vc) >= 3:
obj_skirt.hide_viewport = False
verts_skirt = [tuple(props.proj_p_loc)] + vc
mesh_skirt = create_skirt_mesh(verts_skirt, y_c, props.proj_skirt_fill_bottom, "Mesh_Skirt")
old_m_skirt = obj_skirt.data
mat_skirt = obj_skirt.active_material
obj_skirt.data = mesh_skirt
if mat_skirt:
obj_skirt.data.materials.append(mat_skirt)
update_skirt_material(
mat_skirt,
props.proj_color_skirt, props.proj_alpha_skirt
)
if old_m_skirt: bpy.data.meshes.remove(old_m_skirt)
obj_skirt.location = (0, 0, 0)
obj_skirt.rotation_euler = (0, 0, 0)
else:
obj_skirt.hide_viewport = True
finally:
IS_UPDATING = False
# =========================================================================
# 【連動制御用アップデート関数】
# =========================================================================
def update_proj_link_p_a(self, context):
props = context.scene.poly_torus_props
# 連動開始時の、平面A(Y軸)と球体P(Y座標)の差分をオフセットとして記録
props.proj_p_a_offset = props.proj_y_a - props.proj_p_loc[1]
def update_proj_p_loc(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:
# 球体Pの位置変更に合わせて平面AのY座標も連動
props.proj_y_a = props.proj_p_loc[1] + props.proj_p_a_offset
finally:
IS_UPDATING = False
update_proj(self, context)
def update_proj_y_a(self, context):
global IS_UPDATING
if IS_UPDATING: return
props = context.scene.poly_torus_props
if props.proj_link_p_a:
# 平面Aが手動で直接動かされた場合はオフセットの方を再計算
props.proj_p_a_offset = props.proj_y_a - props.proj_p_loc[1]
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_by_ops(props.segments, props.radius, props.thick, context)
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
proj_p_loc: bpy.props.FloatVectorProperty(name="位置", default=(0.0, -10.0, 0.0), update=update_proj_p_loc)
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のY座標を連動", default=False, update=update_proj_link_p_a)
proj_p_a_offset: bpy.props.FloatProperty(name="連動オフセット", default=0.0)
# スカート機能 (シンプル化: 色と透明度は1つだけ)
proj_show_skirt: bpy.props.BoolProperty(name="光の道筋 (スカート) を表示", default=True, update=update_proj)
proj_skirt_fill_bottom: bpy.props.BoolProperty(name="スカートの底面 (フタ) を作成する", default=True, update=update_proj)
proj_color_skirt: bpy.props.FloatVectorProperty(name="スカート色", subtype='COLOR', default=(1.0, 0.8, 0.2), min=0.0, max=1.0, update=update_proj)
proj_alpha_skirt: bpy.props.FloatProperty(name="透明度", default=0.5, min=0.0, max=1.0, update=update_proj)
# 枠A・B
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_y_a: bpy.props.FloatProperty(name="A Y座標", default=-10.0, update=update_proj_y_a)
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_y_b: bpy.props.FloatProperty(name="B Y座標", default=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_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 = get_or_create_mat("Proj_Mat_Skirt", (1,1,1), 1.0)
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", mat_skirt)
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_by_ops(props.segments, props.radius, props.thick, context)
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")
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")
# 連動オプションのUI表示を追加
col_p.prop(props, "proj_link_p_a", text="球体Pと平面AのY座標を連動", 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.prop(props, "proj_show_skirt", icon='OUTLINER_OB_LIGHT', text="光の道筋 (スカート) 設定")
box_s.prop(props, "proj_skirt_fill_bottom", icon='SNAP_FACE')
row_skirt = box_s.row(align=True)
row_skirt.prop(props, "proj_color_skirt", text="色")
row_skirt.prop(props, "proj_alpha_skirt", 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.prop(props, "proj_y_a")
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.prop(props, "proj_y_b")
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_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()
bl_info = {
"name": "Polygon Torus Generator Pro (Realtime & Detach)",
"author": "Your Name",
"version": (5, 1),
"blender": (4, 2, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション",
"category": "Object",
}
import bpy
import webbrowser
import math
import bmesh
# =========================================================================
# 【基本設定】
# =========================================================================
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_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(n, r, y_pos):
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), y_pos, 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_skirt_mesh(verts, y_c, fill_bottom, mesh_name):
mesh = bpy.data.meshes.new(mesh_name)
if len(verts) < 4: return mesh
bm = bmesh.new()
for v in verts:
bm.verts.new(v)
try: bmesh.ops.convex_hull(bm, input=bm.verts)
except: pass
if not fill_bottom:
faces_to_delete = []
for face in bm.faces:
is_bottom = True
for v in face.verts:
if abs(v.co.y - y_c) > 0.001:
is_bottom = False
break
if is_bottom:
faces_to_delete.append(face)
if faces_to_delete:
bmesh.ops.delete(bm, geom=faces_to_delete, context='FACES')
bm.to_mesh(mesh)
bm.free()
return mesh
def get_torus_mesh_by_ops(n, r, thick, context):
area = next((a for a in context.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
old_active = context.view_layer.objects.active
radius = r * math.sqrt(2) if n == 4 else r
temp_obj = None
override_kwargs = {}
if area: override_kwargs['area'] = area
if region: override_kwargs['region'] = region
with context.temp_override(**override_kwargs):
try:
bpy.ops.mesh.primitive_torus_add(
major_segments=n, minor_segments=12,
major_radius=radius, minor_radius=thick,
location=(0,0,0), rotation=(0,0,0)
)
temp_obj = context.active_object
except: pass
if temp_obj:
new_mesh = temp_obj.data.copy()
bpy.data.objects.remove(temp_obj)
if old_active: context.view_layer.objects.active = old_active
return new_mesh
return bpy.data.meshes.new("FallbackTorus")
def get_sphere_mesh_by_ops(r, context):
area = next((a for a in context.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
old_active = context.view_layer.objects.active
temp_obj = None
override_kwargs = {}
if area: override_kwargs['area'] = area
if region: override_kwargs['region'] = region
with context.temp_override(**override_kwargs):
try:
bpy.ops.mesh.primitive_ico_sphere_add(radius=r, subdivisions=3, location=(0,0,0))
temp_obj = context.active_object
except: pass
if temp_obj:
new_mesh = temp_obj.data.copy()
bpy.data.objects.remove(temp_obj)
if old_active: context.view_layer.objects.active = old_active
return new_mesh
return bpy.data.meshes.new("FallbackSphere")
# =========================================================================
# 【数学・幾何学ロジック (シミュレーション計算)】
# =========================================================================
def get_projected_verts_mode1(verts_a, verts_b, y_a, y_b, y_c):
if abs(y_b - y_a) < 0.0001: return []
t = (y_c - y_a) / (y_b - y_a)
pts = []
for pa in verts_a:
for pb in verts_b:
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 = bpy.data.objects.get("Proj_Skirt")
if not (obj_a and obj_b and obj_c and obj_p and obj_skirt): return
IS_UPDATING = True
try:
n_a, r_a, thick_a, y_a = props.proj_n_a, props.proj_r_a, props.proj_thick_a, props.proj_y_a
n_b, r_b, thick_b, y_b = props.proj_n_b, props.proj_r_b, props.proj_thick_b, props.proj_y_b
y_c = props.proj_y_c
def update_torus(obj, n, r, thick, y_pos, color):
mesh = get_torus_mesh_by_ops(n, r, thick, context)
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)
obj.location = (0, y_pos, 0)
obj.rotation_euler = (math.pi / 2, math.pi / 4 if n == 4 else 0.0, 0.0)
if old_m: bpy.data.meshes.remove(old_m)
update_torus(obj_a, n_a, r_a, thick_a, y_a, props.proj_color_a)
update_torus(obj_b, n_b, r_b, thick_b, y_b, props.proj_color_b)
if props.proj_mode == 'MODE2':
obj_p.hide_viewport = False
mesh_p = get_sphere_mesh_by_ops(props.proj_p_radius, context)
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)
obj_p.location = props.proj_p_loc
else:
obj_p.hide_viewport = True
va = get_polygon_verts(n_a, r_a, y_a)
vb = get_polygon_verts(n_b, r_b, y_b)
if props.proj_mode == 'MODE1':
vc = get_projected_verts_mode1(va, vb, y_a, y_b, 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 and len(vc) >= 3:
obj_skirt.hide_viewport = False
verts_skirt = [tuple(props.proj_p_loc)] + vc
mesh_skirt = create_skirt_mesh(verts_skirt, y_c, props.proj_skirt_fill_bottom, "Mesh_Skirt")
old_m_skirt = obj_skirt.data
mat_skirt = obj_skirt.active_material
obj_skirt.data = mesh_skirt
if mat_skirt:
obj_skirt.data.materials.append(mat_skirt)
update_skirt_material(
mat_skirt,
props.proj_color_skirt, props.proj_alpha_skirt
)
if old_m_skirt: bpy.data.meshes.remove(old_m_skirt)
obj_skirt.location = (0, 0, 0)
obj_skirt.rotation_euler = (0, 0, 0)
else:
obj_skirt.hide_viewport = True
finally:
IS_UPDATING = False
# =========================================================================
# 【リアルタイム更新関数 (単体多角形トーラス用)】
# =========================================================================
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_by_ops(props.segments, props.radius, props.thick, context)
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
proj_p_loc: bpy.props.FloatVectorProperty(name="位置", default=(0.0, -10.0, 0.0), update=update_proj)
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)
# スカート機能 (シンプル化: 色と透明度は1つだけ)
proj_show_skirt: bpy.props.BoolProperty(name="光の道筋 (スカート) を表示", default=True, update=update_proj)
proj_skirt_fill_bottom: bpy.props.BoolProperty(name="スカートの底面 (フタ) を作成する", default=True, update=update_proj)
proj_color_skirt: bpy.props.FloatVectorProperty(name="スカート色", subtype='COLOR', default=(1.0, 0.8, 0.2), min=0.0, max=1.0, update=update_proj)
proj_alpha_skirt: bpy.props.FloatProperty(name="透明度", default=0.5, min=0.0, max=1.0, update=update_proj)
# 枠A・B
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_y_a: bpy.props.FloatProperty(name="A Y座標", default=-10.0, update=update_proj)
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_y_b: bpy.props.FloatProperty(name="B Y座標", default=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_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 = get_or_create_mat("Proj_Mat_Skirt", (1,1,1), 1.0)
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", mat_skirt)
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_by_ops(props.segments, props.radius, props.thick, context)
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")
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")
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.prop(props, "proj_show_skirt", icon='OUTLINER_OB_LIGHT', text="光の道筋 (スカート) 設定")
box_s.prop(props, "proj_skirt_fill_bottom", icon='SNAP_FACE')
row_skirt = box_s.row(align=True)
row_skirt.prop(props, "proj_color_skirt", text="色")
row_skirt.prop(props, "proj_alpha_skirt", 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.prop(props, "proj_y_a")
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.prop(props, "proj_y_b")
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_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()
bl_info = {
"name": "Polygon Torus Generator Pro (Realtime & Detach)",
"author": "Your Name",
"version": (3, 5),
"blender": (3, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション",
"category": "Object",
}
import bpy
import webbrowser
import math
import bmesh
# =========================================================================
# 【基本設定】
# =========================================================================
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_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.blend_method = 'BLEND' if alpha < 1.0 else 'OPAQUE'
mat.shadow_method = 'NONE'
except:
pass
return mat
# 【修正】スカート専用:Mix Shaderを使って表裏のBSDFを完全に分離
def update_skirt_material(mat, cf, af, cb, ab):
if not mat.use_nodes:
mat.use_nodes = True
# 透明レンダリング用の設定
try: mat.blend_method = 'BLEND'
except: pass
try: mat.show_transparent_back = True
except: pass
try: mat.shadow_method = 'NONE'
except: pass
try: mat.use_backface_culling = False
except: pass
tree = mat.node_tree
# ノード取得・作成ヘルパー
def get_node(name, idname):
n = tree.nodes.get(name)
if not n:
n = tree.nodes.new(idname)
n.name = name
return n
# 2つのBSDFとMixShaderを用意する
bsdf_front = get_node("Skirt_Front_BSDF", 'ShaderNodeBsdfPrincipled')
bsdf_back = get_node("Skirt_Back_BSDF", 'ShaderNodeBsdfPrincipled')
mix_shader = get_node("Skirt_MixShader", 'ShaderNodeMixShader')
geom = get_node("Skirt_Geom", 'ShaderNodeNewGeometry')
out = get_node("Skirt_Out", 'ShaderNodeOutputMaterial')
links = tree.links
def ensure_link(out_sock, in_sock):
if not in_sock.is_linked or in_sock.links[0].from_socket != out_sock:
links.new(out_sock, in_sock)
# Front設定 (表面)
bsdf_front.inputs["Base Color"].default_value = (*cf, 1.0)
bsdf_front.inputs["Alpha"].default_value = af
# Back設定 (裏面)
bsdf_back.inputs["Base Color"].default_value = (*cb, 1.0)
bsdf_back.inputs["Alpha"].default_value = ab
# ノード接続: Geometry(Backfacing) -> MixShader(Fac)
ensure_link(geom.outputs["Backfacing"], mix_shader.inputs["Fac"])
ensure_link(bsdf_front.outputs["BSDF"], mix_shader.inputs[1])
ensure_link(bsdf_back.outputs["BSDF"], mix_shader.inputs[2])
ensure_link(mix_shader.outputs["Shader"], out.inputs["Surface"])
mat.diffuse_color = (*cf, af)
def get_polygon_verts(n, r, y_pos):
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), y_pos, 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_skirt_mesh(verts, y_c, fill_bottom, mesh_name):
mesh = bpy.data.meshes.new(mesh_name)
if len(verts) < 4: return mesh
bm = bmesh.new()
for v in verts:
bm.verts.new(v)
try:
bmesh.ops.convex_hull(bm, input=bm.verts)
except:
pass
if not fill_bottom:
faces_to_delete = []
for face in bm.faces:
is_bottom = True
for v in face.verts:
if abs(v.co.y - y_c) > 0.001:
is_bottom = False
break
if is_bottom:
faces_to_delete.append(face)
if faces_to_delete:
bmesh.ops.delete(bm, geom=faces_to_delete, context='FACES')
bm.to_mesh(mesh)
bm.free()
return mesh
def get_torus_mesh_by_ops(n, r, thick, context):
area = next((a for a in context.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
old_active = context.view_layer.objects.active
radius = r * math.sqrt(2) if n == 4 else r
temp_obj = None
override_kwargs = {}
if area: override_kwargs['area'] = area
if region: override_kwargs['region'] = region
with context.temp_override(**override_kwargs):
try:
bpy.ops.mesh.primitive_torus_add(
major_segments=n, minor_segments=12,
major_radius=radius, minor_radius=thick,
location=(0,0,0), rotation=(0,0,0)
)
temp_obj = context.active_object
except: pass
if temp_obj:
new_mesh = temp_obj.data.copy()
bpy.data.objects.remove(temp_obj)
if old_active: context.view_layer.objects.active = old_active
return new_mesh
return bpy.data.meshes.new("FallbackTorus")
def get_sphere_mesh_by_ops(r, context):
area = next((a for a in context.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
old_active = context.view_layer.objects.active
temp_obj = None
override_kwargs = {}
if area: override_kwargs['area'] = area
if region: override_kwargs['region'] = region
with context.temp_override(**override_kwargs):
try:
bpy.ops.mesh.primitive_ico_sphere_add(radius=r, subdivisions=3, location=(0,0,0))
temp_obj = context.active_object
except: pass
if temp_obj:
new_mesh = temp_obj.data.copy()
bpy.data.objects.remove(temp_obj)
if old_active: context.view_layer.objects.active = old_active
return new_mesh
return bpy.data.meshes.new("FallbackSphere")
# =========================================================================
# 【数学・幾何学ロジック (シミュレーション計算)】
# =========================================================================
def get_projected_verts_mode1(verts_a, verts_b, y_a, y_b, y_c):
if abs(y_b - y_a) < 0.0001: return []
t = (y_c - y_a) / (y_b - y_a)
pts = []
for pa in verts_a:
for pb in verts_b:
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 = bpy.data.objects.get("Proj_Skirt")
if not (obj_a and obj_b and obj_c and obj_p and obj_skirt): return
IS_UPDATING = True
try:
n_a, r_a, thick_a, y_a = props.proj_n_a, props.proj_r_a, props.proj_thick_a, props.proj_y_a
n_b, r_b, thick_b, y_b = props.proj_n_b, props.proj_r_b, props.proj_thick_b, props.proj_y_b
y_c = props.proj_y_c
# 1. 枠A、枠Bの更新
def update_torus(obj, n, r, thick, y_pos, color):
mesh = get_torus_mesh_by_ops(n, r, thick, context)
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)
obj.location = (0, y_pos, 0)
obj.rotation_euler = (math.pi / 2, math.pi / 4 if n == 4 else 0.0, 0.0)
if old_m: bpy.data.meshes.remove(old_m)
update_torus(obj_a, n_a, r_a, thick_a, y_a, props.proj_color_a)
update_torus(obj_b, n_b, r_b, thick_b, y_b, props.proj_color_b)
# ---------------------------------------------------
# 2. P (球体) の更新
if props.proj_mode == 'MODE2':
obj_p.hide_viewport = False
mesh_p = get_sphere_mesh_by_ops(props.proj_p_radius, context)
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.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)
obj_p.location = props.proj_p_loc
else:
obj_p.hide_viewport = True
# ---------------------------------------------------
# 3. C (到達範囲) の計算
va = get_polygon_verts(n_a, r_a, y_a)
vb = get_polygon_verts(n_b, r_b, y_b)
if props.proj_mode == 'MODE1':
vc = get_projected_verts_mode1(va, vb, y_a, y_b, 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
# 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)
# ---------------------------------------------------
# 4. スカート (光の道筋) の更新
if props.proj_mode == 'MODE2' and props.proj_show_skirt and len(vc) >= 3:
obj_skirt.hide_viewport = False
verts_skirt = [tuple(props.proj_p_loc)] + vc
mesh_skirt = create_skirt_mesh(verts_skirt, y_c, props.proj_skirt_fill_bottom, "Mesh_Skirt")
old_m_skirt = obj_skirt.data
mat_skirt = obj_skirt.active_material
obj_skirt.data = mesh_skirt
if mat_skirt:
obj_skirt.data.materials.append(mat_skirt)
update_skirt_material(
mat_skirt,
props.proj_color_skirt_front, props.proj_alpha_skirt_front,
props.proj_color_skirt_back, props.proj_alpha_skirt_back
)
if old_m_skirt: bpy.data.meshes.remove(old_m_skirt)
obj_skirt.location = (0, 0, 0)
obj_skirt.rotation_euler = (0, 0, 0)
else:
obj_skirt.hide_viewport = True
finally:
IS_UPDATING = False
# =========================================================================
# 【リアルタイム更新関数 (単体多角形トーラス用)】
# =========================================================================
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.blend_method = 'BLEND' if props.alpha < 1.0 else 'OPAQUE'
except: pass
mesh_new = get_torus_mesh_by_ops(props.segments, props.radius, props.thick, context)
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
proj_p_loc: bpy.props.FloatVectorProperty(name="位置", default=(0.0, -100.0, 0.0), update=update_proj)
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_show_skirt: bpy.props.BoolProperty(name="光の道筋 (スカート) を表示", default=True, update=update_proj)
proj_skirt_fill_bottom: bpy.props.BoolProperty(name="スカートの底面 (フタ) を作成する", default=True, update=update_proj)
proj_color_skirt_front: bpy.props.FloatVectorProperty(name="表面色", subtype='COLOR', default=(1.0, 0.8, 0.2), min=0.0, max=1.0, update=update_proj)
proj_alpha_skirt_front: bpy.props.FloatProperty(name="表面 透明度", default=1.0, min=0.0, max=1.0, update=update_proj) # 1.0 に変更
proj_color_skirt_back: bpy.props.FloatVectorProperty(name="裏面色", subtype='COLOR', default=(0.2, 0.8, 1.0), min=0.0, max=1.0, update=update_proj)
proj_alpha_skirt_back: bpy.props.FloatProperty(name="裏面 透明度", default=0.5, min=0.0, max=1.0, update=update_proj) # 0.5 に変更
# 枠A・B
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_y_a: bpy.props.FloatProperty(name="A Y座標", default=-10.0, update=update_proj)
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_y_b: bpy.props.FloatProperty(name="B Y座標", default=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_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 = get_or_create_mat("Proj_Mat_Skirt", (1,1,1), 1.0)
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", mat_skirt)
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_by_ops(props.segments, props.radius, props.thick, context)
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")
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")
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.prop(props, "proj_show_skirt", icon='OUTLINER_OB_LIGHT', text="光の道筋 (スカート) 設定")
box_s.prop(props, "proj_skirt_fill_bottom", icon='SNAP_FACE')
row_front = box_s.row(align=True)
row_front.prop(props, "proj_color_skirt_front", text="表面色")
row_front.prop(props, "proj_alpha_skirt_front", text="表 透明度")
row_back = box_s.row(align=True)
row_back.prop(props, "proj_color_skirt_back", text="裏面色")
row_back.prop(props, "proj_alpha_skirt_back", 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.prop(props, "proj_y_a")
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.prop(props, "proj_y_b")
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_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()
bl_info = {
"name": "Polygon Torus Generator Pro (Realtime & Detach)",
"author": "Your Name",
"version": (3, 2),
"blender": (3, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション",
"category": "Object",
}
import bpy
import webbrowser
import math
import bmesh
# =========================================================================
# 【基本設定】
# =========================================================================
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_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.blend_method = 'BLEND' if alpha < 1.0 else 'OPAQUE'
except:
pass
return mat
def get_polygon_verts(n, r, y_pos):
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), y_pos, 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 get_torus_mesh_by_ops(n, r, thick, context):
area = next((a for a in context.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
old_active = context.view_layer.objects.active
radius = r * math.sqrt(2) if n == 4 else r
temp_obj = None
override_kwargs = {}
if area: override_kwargs['area'] = area
if region: override_kwargs['region'] = region
with context.temp_override(**override_kwargs):
try:
bpy.ops.mesh.primitive_torus_add(
major_segments=n, minor_segments=12,
major_radius=radius, minor_radius=thick,
location=(0,0,0), rotation=(0,0,0)
)
temp_obj = context.active_object
except: pass
if temp_obj:
new_mesh = temp_obj.data.copy()
bpy.data.objects.remove(temp_obj)
if old_active: context.view_layer.objects.active = old_active
return new_mesh
return bpy.data.meshes.new("FallbackTorus")
def get_sphere_mesh_by_ops(r, context):
area = next((a for a in context.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
old_active = context.view_layer.objects.active
temp_obj = None
override_kwargs = {}
if area: override_kwargs['area'] = area
if region: override_kwargs['region'] = region
with context.temp_override(**override_kwargs):
try:
bpy.ops.mesh.primitive_ico_sphere_add(radius=r, subdivisions=3, location=(0,0,0))
temp_obj = context.active_object
except: pass
if temp_obj:
new_mesh = temp_obj.data.copy()
bpy.data.objects.remove(temp_obj)
if old_active: context.view_layer.objects.active = old_active
return new_mesh
return bpy.data.meshes.new("FallbackSphere")
# =========================================================================
# 【数学・幾何学ロジック (シミュレーション計算)】
# =========================================================================
def get_projected_verts_mode1(verts_a, verts_b, y_a, y_b, y_c):
if abs(y_b - y_a) < 0.0001: return []
t = (y_c - y_a) / (y_b - y_a)
pts = []
for pa in verts_a:
for pb in verts_b:
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")
if not (obj_a and obj_b and obj_c and obj_p): return
IS_UPDATING = True
try:
n_a, r_a, thick_a, y_a = props.proj_n_a, props.proj_r_a, props.proj_thick_a, props.proj_y_a
n_b, r_b, thick_b, y_b = props.proj_n_b, props.proj_r_b, props.proj_thick_b, props.proj_y_b
y_c = props.proj_y_c
# 1. 枠A、枠Bの更新
def update_torus(obj, n, r, thick, y_pos, color):
mesh = get_torus_mesh_by_ops(n, r, thick, context)
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)
obj.location = (0, y_pos, 0)
obj.rotation_euler = (math.pi / 2, math.pi / 4 if n == 4 else 0.0, 0.0)
if old_m: bpy.data.meshes.remove(old_m)
update_torus(obj_a, n_a, r_a, thick_a, y_a, props.proj_color_a)
update_torus(obj_b, n_b, r_b, thick_b, y_b, props.proj_color_b)
# ---------------------------------------------------
# 2. P (球体) の更新とマテリアル変更(※ここを修正しました)
if props.proj_mode == 'MODE2':
obj_p.hide_viewport = False
mesh_p = get_sphere_mesh_by_ops(props.proj_p_radius, context)
# 先に現在のマテリアルを確保しておく
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.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)
obj_p.location = props.proj_p_loc
else:
obj_p.hide_viewport = True
# ---------------------------------------------------
# 3. C (到達範囲) の計算と更新
va = get_polygon_verts(n_a, r_a, y_a)
vb = get_polygon_verts(n_b, r_b, y_b)
if props.proj_mode == 'MODE1':
vc = get_projected_verts_mode1(va, vb, y_a, y_b, 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
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)
finally:
IS_UPDATING = False
# =========================================================================
# 【リアルタイム更新関数 (単体多角形トーラス用)】
# =========================================================================
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.blend_method = 'BLEND' if props.alpha < 1.0 else 'OPAQUE'
except: pass
mesh_new = get_torus_mesh_by_ops(props.segments, props.radius, props.thick, context)
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
)
proj_p_loc: bpy.props.FloatVectorProperty(name="位置", default=(0.0, -100.0, 0.0), update=update_proj)
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_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_y_a: bpy.props.FloatProperty(name="A Y座標", default=-10.0, update=update_proj)
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_y_b: bpy.props.FloatProperty(name="B Y座標", default=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_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_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)
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)
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_by_ops(props.segments, props.radius, props.thick, context)
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")
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")
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()
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.prop(props, "proj_y_a")
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.prop(props, "proj_y_b")
col_b.prop(props, "proj_color_b", text="B 色")
box_c = layout.box()
box_c.label(text="平面C (壁・到達範囲)", icon='MESH_PLANE')
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_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()
bl_info = {
"name": "Polygon Torus Generator Pro (Realtime & Detach)",
"author": "Your Name",
"version": (3, 0),
"blender": (3, 0, 0),
"location": "View3D > Sidebar (Nパネル)",
"description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション",
"category": "Object",
}
import bpy
import webbrowser
import math
import bmesh
# =========================================================================
# 【基本設定】
# =========================================================================
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_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.blend_method = 'BLEND' if alpha < 1.0 else 'OPAQUE'
except:
pass
return mat
def get_polygon_verts(n, r, y_pos):
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), y_pos, 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 get_torus_mesh_by_ops(n, r, thick, context):
area = next((a for a in context.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
old_active = context.view_layer.objects.active
radius = r * math.sqrt(2) if n == 4 else r
temp_obj = None
override_kwargs = {}
if area: override_kwargs['area'] = area
if region: override_kwargs['region'] = region
with context.temp_override(**override_kwargs):
try:
bpy.ops.mesh.primitive_torus_add(
major_segments=n, minor_segments=12,
major_radius=radius, minor_radius=thick,
location=(0,0,0), rotation=(0,0,0)
)
temp_obj = context.active_object
except: pass
if temp_obj:
new_mesh = temp_obj.data.copy()
bpy.data.objects.remove(temp_obj)
if old_active: context.view_layer.objects.active = old_active
return new_mesh
return bpy.data.meshes.new("FallbackTorus")
def get_sphere_mesh_by_ops(r, context):
area = next((a for a in context.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
old_active = context.view_layer.objects.active
temp_obj = None
override_kwargs = {}
if area: override_kwargs['area'] = area
if region: override_kwargs['region'] = region
with context.temp_override(**override_kwargs):
try:
bpy.ops.mesh.primitive_ico_sphere_add(radius=r, subdivisions=3, location=(0,0,0))
temp_obj = context.active_object
except: pass
if temp_obj:
new_mesh = temp_obj.data.copy()
bpy.data.objects.remove(temp_obj)
if old_active: context.view_layer.objects.active = old_active
return new_mesh
return bpy.data.meshes.new("FallbackSphere")
# =========================================================================
# 【数学・幾何学ロジック (シミュレーション計算)】
# =========================================================================
# モード1:2つの多角形領域間の全直線の束 (従来型)
def get_projected_verts_mode1(verts_a, verts_b, y_a, y_b, y_c):
if abs(y_b - y_a) < 0.0001: return []
t = (y_c - y_a) / (y_b - y_a)
pts = []
for pa in verts_a:
for pb in verts_b:
x = pa[0] + t * (pb[0] - pa[0])
z = pa[2] + t * (pb[2] - pa[2])
pts.append((x, y_c, z))
return pts
# モード2:1点から発射され、2つの多角形領域をすり抜けた範囲 (新型)
def get_projected_verts_mode2(p_loc, verts_a, verts_b, y_c):
px, py, pz = p_loc
# 1. 任意の多角形を点Pから平面y_cに投影する関数
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 [] # Pから見て後ろにある場合は到達しない
x = px + t * (vx - px)
z = pz + t * (vz - pz)
poly2d.append((x, z))
return poly2d
# 2. 頂点群を反時計回り(CCW)に整列する関数
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
# 3. 2つの凸多角形の交差(共通部分)を求める Sutherland-Hodgman アルゴリズム
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
# AとBそれぞれの投影形状(2D)を取得
poly_a = project_polygon(verts_a)
poly_b = project_polygon(verts_b)
if not poly_a or not poly_b: return []
# 共通部分(Intersection)を計算
intersect_poly2d = sutherland_hodgman(make_ccw(poly_a), make_ccw(poly_b))
# 3D空間(y = y_c)の座標に戻す
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")
if not (obj_a and obj_b and obj_c and obj_p): return
IS_UPDATING = True
try:
# Aの設定
n_a, r_a, thick_a, y_a = props.proj_n_a, props.proj_r_a, props.proj_thick_a, props.proj_y_a
# Bの設定
n_b, r_b, thick_b, y_b = props.proj_n_b, props.proj_r_b, props.proj_thick_b, props.proj_y_b
# Cの設定
y_c = props.proj_y_c
# ---------------------------------------------------
# 1. 枠A、枠Bのメッシュ更新
def update_torus(obj, n, r, thick, y_pos, color):
mesh = get_torus_mesh_by_ops(n, r, thick, context)
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)
obj.location = (0, y_pos, 0)
obj.rotation_euler = (math.pi / 2, math.pi / 4 if n == 4 else 0.0, 0.0)
if old_m: bpy.data.meshes.remove(old_m)
update_torus(obj_a, n_a, r_a, thick_a, y_a, props.proj_color_a)
update_torus(obj_b, n_b, r_b, thick_b, y_b, props.proj_color_b)
# ---------------------------------------------------
# 2. P (指定点・赤い球体) の更新と表示制御
if props.proj_mode == 'MODE2':
obj_p.hide_viewport = False
mesh_p = get_sphere_mesh_by_ops(props.proj_p_radius, context)
old_m_p = obj_p.data
obj_p.data = mesh_p
if old_m_p: bpy.data.meshes.remove(old_m_p)
# マテリアルは初期化時に赤色固定でセット済み
if obj_p.active_material:
obj_p.data.materials.append(obj_p.active_material)
obj_p.location = props.proj_p_loc
else:
obj_p.hide_viewport = True # モード1の時は隠す
# ---------------------------------------------------
# 3. C (到達範囲) の計算と更新
va = get_polygon_verts(n_a, r_a, y_a)
vb = get_polygon_verts(n_b, r_b, y_b)
if props.proj_mode == 'MODE1':
vc = get_projected_verts_mode1(va, vb, y_a, y_b, 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
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)
# Cの色更新
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)
finally:
IS_UPDATING = False
# =========================================================================
# 【リアルタイム更新関数 (単体多角形トーラス用)】
# =========================================================================
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.blend_method = 'BLEND' if props.alpha < 1.0 else 'OPAQUE'
except: pass
mesh_new = get_torus_mesh_by_ops(props.segments, props.radius, props.thick, context)
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
proj_p_loc: bpy.props.FloatVectorProperty(name="位置", default=(0.0, -100.0, 0.0), update=update_proj)
proj_p_radius: bpy.props.FloatProperty(name="球の半径", default=1.0, min=0.1, update=update_proj)
# 枠A
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_y_a: bpy.props.FloatProperty(name="A Y座標", default=-10.0, update=update_proj)
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
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_y_b: bpy.props.FloatProperty(name="B Y座標", default=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_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_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", (1.0, 0.0, 0.0), 1.0) # Pは赤固定
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) # 球体P
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_by_ops(props.segments, props.radius, props.thick, context)
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")
layout.operator(f"mesh.{PREFIX_NAME.lower()}_create_proj", text="シミュレーションを生成", icon='LIGHT_SUN')
layout.separator()
# モード選択
layout.prop(props, "proj_mode", expand=False)
# モード2のみ表示する「指定点P」の設定
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")
col_p.prop(props, "proj_p_radius")
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.prop(props, "proj_y_a")
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.prop(props, "proj_y_b")
col_b.prop(props, "proj_color_b", text="B 色")
box_c = layout.box()
box_c.label(text="平面C (壁・到達範囲)", icon='MESH_PLANE')
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_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()