blender Million 2026
Prefix トーラス正方形 20260324
# Copied: 10:35:00
import bpy
import bmesh
import webbrowser
import math
import mathutils
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime
# ==============================================================================
# 【 基本設定エリア 】 先頭で Prefix や 表示名 を変更できます
# ==============================================================================
PREFIX = "SquareTorus20260324" # 内部ID用プレフィックス (大文字は自動で小文字化されます)
ADDON_NAME = "zionad 520[ Sq-Torus ]" # アドオンの表示名 (環境設定などに表示)
TAB_NAME = " [ Sq Torus copy ] " # 3Dビューサイドバー(Nパネル)のタブ名
PANEL_TITLE = "Square Torus Generator" # メインパネルのタイトル名
AUTHOR = "zionadchat" # 作者名
# ★ このスクリプト自身のID (コピー機能でテキスト検索に使用されます)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SQUARE_TORUS_2026_03_24_V3 ###"
# ==============================================================================
# システム初期化 & ID管理
# ==============================================================================
bl_info = {
"name": f"{ADDON_NAME} {PREFIX}",
"author": AUTHOR,
"version": (4, 1, 1),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": f"Unique Material Square Torus Generator - {PREFIX}",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
# ★ リンクを新しいものに差し替え(既存リンクは削除)
ADDON_LINKS = (
{"label": "Prefix トーラス正方形 20260324", "url": "<https://www.notion.so/Prefix-20260324-32df5dacaf4380528980db6a989d6306>"},
)
# ==============================================================================
# デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"show_preview": True,
"show_square_guide": True,
"torus_color": (0.0391, 0.8000, 0.1647, 0.8000),
"torus_loc": (0.0000, 0.0000, 0.0000),
"torus_rot": (0.0000, 0.0000, 0.0000),
"square_size": 10.0000,
"corner_radius": 0.0000,
"minor_radius": 0.5000,
"corner_segments": 8,
"minor_segments": 16,
"torus_plane": "XY",
}
# <END_DICT>
# ==============================================================================
# 数学的 角丸正方形トーラス生成 & ガイド生成
# ==============================================================================
def create_square_guide_bmesh(bm, square_size):
""" プレビュー用の正方形ガイド(線のみ)を生成 """
S = square_size / 2.0
v1 = bm.verts.new((S, S, 0))
v2 = bm.verts.new((-S, S, 0))
v3 = bm.verts.new((-S, -S, 0))
v4 = bm.verts.new((S, -S, 0))
bm.verts.ensure_lookup_table()
bm.edges.new((v1, v2))
bm.edges.new((v2, v3))
bm.edges.new((v3, v4))
bm.edges.new((v4, v1))
return bm
def create_square_torus_bmesh(bm, square_size, corner_radius, minor_radius, corner_segments, minor_segments):
""" 正方形の枠に沿ったトーラス(チューブ)を生成 """
half_size = square_size / 2.0
actual_corner_radius = min(max(corner_radius, 0.0), half_size)
rings =[]
# ★ Corner Radius が 0 の場合、完全な直角(90度)で斜めに繋がるよう生成
if actual_corner_radius <= 0.001:
L = half_size
corners =[
(mathutils.Vector((L, L, 0)), mathutils.Vector((1, 1, 0)).normalized()),
(mathutils.Vector((-L, L, 0)), mathutils.Vector((-1, 1, 0)).normalized()),
(mathutils.Vector((-L, -L, 0)), mathutils.Vector((-1, -1, 0)).normalized()),
(mathutils.Vector((L, -L, 0)), mathutils.Vector((1, -1, 0)).normalized())
]
scale_xy = 1.0 / math.cos(math.pi / 4) # 直角接合のためのスケール(約1.414倍)
for p, n in corners:
b = mathutils.Vector((0, 0, 1)) # Z方向
ring =[]
for j in range(minor_segments):
theta = j * 2.0 * math.pi / minor_segments
# 法線方向を √2 倍に伸ばして斜め接合(マイタージョイント)を実現
offset = n * (minor_radius * math.cos(theta) * scale_xy) + b * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
# ★ それ以外は角丸で生成
else:
L = half_size - actual_corner_radius
pts =[]
# 0: 右上, 1: 左上, 2: 左下, 3: 右下 の4つの角を順に生成
for q in range(4):
cx = L if q in [0, 3] else -L
cy = L if q in [0, 1] else -L
for i in range(corner_segments + 1):
angle = q * (math.pi / 2) + i * (math.pi / 2) / corner_segments
x = cx + actual_corner_radius * math.cos(angle)
y = cy + actual_corner_radius * math.sin(angle)
nx = math.cos(angle)
ny = math.sin(angle)
pts.append((mathutils.Vector((x, y, 0)), mathutils.Vector((nx, ny, 0))))
for p, n in pts:
b = mathutils.Vector((0, 0, 1))
ring =[]
for j in range(minor_segments):
theta = j * 2.0 * math.pi / minor_segments
offset = n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
bm.verts.ensure_lookup_table()
# 断面同士を繋いで面(ポリゴン)を張る
total_rings = len(rings)
for i in range(total_rings):
next_i = (i + 1) % total_rings
ring1 = rings[i]
ring2 = rings[next_i]
for j in range(minor_segments):
next_j = (j + 1) % minor_segments
bm.faces.new((ring1[j], ring2[j], ring2[next_j], ring1[next_j]))
# 滑らかに表示
for f in bm.faces:
f.smooth = True
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
def apply_auto_smooth(mesh):
""" 直角部分の影を綺麗にするための自動スムース設定 """
try:
if hasattr(mesh, "use_auto_smooth"):
mesh.use_auto_smooth = True
mesh.auto_smooth_angle = math.radians(30)
except: pass
# ==============================================================================
# マテリアル作成ロジック (実体化用)
# ==============================================================================
def create_unique_material(color, name_prefix="Mat"):
timestamp = datetime.now().strftime('%M%S%f')[:5]
mat_name = f"{name_prefix}_{timestamp}"
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
if mat.use_nodes:
tree = mat.node_tree
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (0, 0)
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (300, 0)
tree.links.new(bsdf.outputs[0], out.inputs[0])
if "Base Color" in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = color[3]
return mat
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_OBJ_NAME = f"[Preview] SqTorus_{PREFIX}"
PREVIEW_GUIDE_NAME = f"[Preview] SqGuide_{PREFIX}"
PREVIEW_MESH_NAME = f"PreviewMesh_{PREFIX}"
PREVIEW_MAT_NAME = f"PreviewMat_{PREFIX}"
def get_or_create_preview_material():
mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
if not mat:
mat = bpy.data.materials.new(name=PREVIEW_MAT_NAME)
mat.use_nodes = True
mat.blend_method = 'BLEND'
return mat
def update_preview_material(mat, color):
if mat.use_nodes:
bsdf = None
for node in mat.node_tree.nodes:
if node.type == 'BSDF_PRINCIPLED':
bsdf = node
break
if not bsdf:
mat.node_tree.nodes.clear()
bsdf = mat.node_tree.nodes.new("ShaderNodeBsdfPrincipled")
out = mat.node_tree.nodes.new("ShaderNodeOutputMaterial")
mat.node_tree.links.new(bsdf.outputs[0], out.inputs[0])
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
def get_transform_matrix(props):
rot_matrix = mathutils.Matrix.Identity(4)
if props.torus_plane == 'YZ':
rot_matrix = mathutils.Matrix.Rotation(math.radians(90.0), 4, 'Y')
elif props.torus_plane == 'ZX':
rot_matrix = mathutils.Matrix.Rotation(math.radians(-90.0), 4, 'X')
user_rot = mathutils.Euler((
math.radians(props.torus_rot[0]),
math.radians(props.torus_rot[1]),
math.radians(props.torus_rot[2])
), 'XYZ').to_matrix().to_4x4()
loc_matrix = mathutils.Matrix.Translation(mathutils.Vector(props.torus_loc))
return loc_matrix @ user_rot @ rot_matrix
def update_preview_geometry(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_NAME)
context.scene.collection.children.link(col)
obj = bpy.data.objects.get(PREVIEW_OBJ_NAME)
guide_obj = bpy.data.objects.get(PREVIEW_GUIDE_NAME)
if not props.show_preview:
if obj: bpy.data.objects.remove(obj, do_unlink=True)
if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)
return
final_matrix = get_transform_matrix(props)
# --- 四角いトーラス本体の更新 ---
bm = bmesh.new()
try:
create_square_torus_bmesh(
bm,
square_size=props.square_size,
corner_radius=props.corner_radius,
minor_radius=props.minor_radius,
corner_segments=props.corner_segments,
minor_segments=props.minor_segments
)
bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
mesh = bpy.data.meshes.get(PREVIEW_MESH_NAME)
if not mesh: mesh = bpy.data.meshes.new(PREVIEW_MESH_NAME)
else: mesh.clear_geometry()
bm.to_mesh(mesh)
apply_auto_smooth(mesh)
mesh.update()
finally: bm.free()
if not obj:
obj = bpy.data.objects.new(PREVIEW_OBJ_NAME, mesh)
col.objects.link(obj)
elif obj.data != mesh: obj.data = mesh
mat = get_or_create_preview_material()
update_preview_material(mat, props.torus_color)
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
# --- 正方形ガイドの更新 ---
if props.show_square_guide:
bm_g = bmesh.new()
try:
create_square_guide_bmesh(bm_g, props.square_size)
bmesh.ops.transform(bm_g, matrix=final_matrix, verts=bm_g.verts)
mesh_g = bpy.data.meshes.get(PREVIEW_MESH_NAME + "_Guide")
if not mesh_g: mesh_g = bpy.data.meshes.new(PREVIEW_MESH_NAME + "_Guide")
else: mesh_g.clear_geometry()
bm_g.to_mesh(mesh_g)
mesh_g.update()
finally: bm_g.free()
if not guide_obj:
guide_obj = bpy.data.objects.new(PREVIEW_GUIDE_NAME, mesh_g)
col.objects.link(guide_obj)
elif guide_obj.data != mesh_g: guide_obj.data = mesh_g
guide_obj.display_type = 'WIRE'
guide_obj.show_in_front = True
else:
if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)
_timer = None
def delayed_update():
global _timer
_timer = None
if bpy.context and bpy.context.scene: update_preview_geometry(bpy.context)
return None
def on_update(self, context):
global _timer
if _timer:
try: bpy.app.timers.unregister(_timer)
except: pass
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_TorusProps(PropertyGroup):
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
show_square_guide: BoolProperty(name="Show Square Guide", default=CURRENT_DEFAULTS['show_square_guide'], update=on_update)
torus_color: FloatVectorProperty(name="Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['torus_color'], update=on_update)
torus_plane: EnumProperty(
name="Plane",
items=[('XY', "XY Plane", ""), ('YZ', "YZ Plane", ""), ('ZX', "ZX Plane", "")],
default=CURRENT_DEFAULTS['torus_plane'], update=on_update
)
torus_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['torus_loc'], update=on_update)
torus_rot: FloatVectorProperty(name="Rotation (Deg)", size=3, default=CURRENT_DEFAULTS['torus_rot'], update=on_update)
square_size: FloatProperty(name="Square Size", default=CURRENT_DEFAULTS['square_size'], min=0.1, update=on_update)
corner_radius: FloatProperty(name="Corner Radius", default=CURRENT_DEFAULTS['corner_radius'], min=0.0, update=on_update)
minor_radius: FloatProperty(name="Tube Thickness", default=CURRENT_DEFAULTS['minor_radius'], min=0.01, update=on_update)
corner_segments: IntProperty(name="Corner Segs", default=CURRENT_DEFAULTS['corner_segments'], min=1, update=on_update)
minor_segments: IntProperty(name="Tube Segs", default=CURRENT_DEFAULTS['minor_segments'], min=3, update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CreateTorus(Operator):
bl_idname = f"{OP_PREFIX}.create_torus"
bl_label = "Create Square Torus"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
bm = bmesh.new()
create_square_torus_bmesh(
bm,
square_size=props.square_size,
corner_radius=props.corner_radius,
minor_radius=props.minor_radius,
corner_segments=props.corner_segments,
minor_segments=props.minor_segments
)
final_matrix = get_transform_matrix(props)
bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
mesh = bpy.data.meshes.new(f"SquareTorus_Mesh")
bm.to_mesh(mesh)
bm.free()
apply_auto_smooth(mesh)
obj = bpy.data.objects.new(f"SqTorus_{datetime.now().strftime('%H%M%S')}", mesh)
if context.collection: context.collection.objects.link(obj)
else: context.scene.collection.objects.link(obj)
unique_mat = create_unique_material(props.torus_color, "Mat_UniqueSqTorus")
obj.data.materials.append(unique_mat)
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
context.view_layer.objects.active = obj
self.report({'INFO'}, "Created Square Torus!")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string(): target_text = t; break
if not target_text:
self.report({'ERROR'}, "Script source not found.")
return {'CANCELLED'}
code = target_text.as_string()
c, l, r = props.torus_color, props.torus_loc, props.torus_rot
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "show_square_guide": {props.show_square_guide},\n'
new_dict += f' "torus_color": ({c[0]:.4f}, {c[1]:.4f}, {c[2]:.4f}, {c[3]:.4f}),\n'
new_dict += f' "torus_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
new_dict += f' "torus_rot": ({r[0]:.4f}, {r[1]:.4f}, {r[2]:.4f}),\n'
new_dict += f' "square_size": {props.square_size:.4f},\n'
new_dict += f' "corner_radius": {props.corner_radius:.4f},\n'
new_dict += f' "minor_radius": {props.minor_radius:.4f},\n'
new_dict += f' "corner_segments": {props.corner_segments},\n'
new_dict += f' "minor_segments": {props.minor_segments},\n'
new_dict += f' "torus_plane": "{props.torus_plane}",\n'
new_dict += "}\n"
try:
start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
pre, post = code.split(start)[0], code.split(end)[1]
final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
context.window_manager.clipboard = final
self.report({'INFO'}, "Code copied!")
except: return {'CANCELLED'}
return {'FINISHED'}
class OT_Reset(Operator):
bl_idname = f"{OP_PREFIX}.reset"
bl_label = "Reset Transform"
def execute(self, context):
p = getattr(context.scene, PROPS_NAME)
p.torus_loc = (0,0,0)
p.torus_rot = (0,0,0)
p.torus_plane = 'XY'
p.square_size = 10.0
p.corner_radius = 0.0
p.minor_radius = 0.5
return {'FINISHED'}
class OT_OpenUrl(Operator):
bl_idname = f"{OP_PREFIX}.open_url"; bl_label = "Open URL"; url: StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class OT_RemoveAddon(Operator):
bl_idname = f"{OP_PREFIX}.remove_addon"; bl_label = "Remove Addon"
def execute(self, context):
bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_MainPanel(Panel):
bl_label = PANEL_TITLE
bl_idname = f"{PREFIX}_PT_main"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: layout.label(text="Reload Script"); return
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
box = layout.box()
if not props.show_preview:
box.label(text="Preview is Hidden", icon='INFO')
box.prop(props, "torus_color")
col = box.column(align=True)
col.prop(props, "torus_plane")
col.prop(props, "torus_loc")
col.prop(props, "torus_rot")
box.separator()
# ガイドトグルとサイズ設定
box.prop(props, "show_square_guide", icon='MESH_PLANE')
col_s = box.column(align=True)
col_s.prop(props, "square_size")
# 角の丸み (0で直角になることを明示)
row_cr = col_s.row()
row_cr.prop(props, "corner_radius")
if props.corner_radius <= 0.001:
row_cr.label(text="[90° Mode]", icon='SNAP_VERTEX')
col_s.prop(props, "minor_radius")
row_seg = box.row()
row_seg.prop(props, "corner_segments")
row_seg.prop(props, "minor_segments")
box.operator(OT_Reset.bl_idname, icon='LOOP_BACK')
layout.separator()
col_exec = layout.column()
col_exec.scale_y = 1.5
col_exec.operator(OT_CreateTorus.bl_idname, icon='MESH_TORUS', text="Create Square Torus")
class PT_LinksPanel(Panel):
bl_label = "Links"; bl_idname = f"{PREFIX}_PT_links"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
for l in ADDON_LINKS: self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"]).url = l["url"]
class PT_RemovePanel(Panel):
bl_label = "System"; bl_idname = f"{PREFIX}_PT_remove"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): self.layout.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Remove Addon")
# ==============================================================================
# REGISTER
# ==============================================================================
classes = (PG_TorusProps, OT_CreateTorus, OT_CopyFullScript, OT_Reset, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, PT_LinksPanel, PT_RemovePanel)
def auto_open_sidebar():
try:
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
if not space.show_region_ui:
space.show_region_ui = True
except: pass
return None
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_TorusProps))
bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)
def unregister():
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes): bpy.utils.unregister_class(c)
if __name__ == "__main__": register()
# Copied: 15:00:01
import bpy
import bmesh
import webbrowser
import math
import mathutils
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime
# ==============================================================================
# 【 基本設定エリア 】 先頭で Prefix や 表示名 を変更できます
# ==============================================================================
PREFIX = "SquareTorus20260227" # 内部ID用プレフィックス (大文字は自動で小文字化されます)
ADDON_NAME = "zionad 520[ Sq-Torus ]" # アドオンの表示名 (環境設定などに表示)
TAB_NAME = " [ Sq Torus copy ] " # 3Dビューサイドバー(Nパネル)のタブ名
PANEL_TITLE = "Square Torus Generator" # メインパネルのタイトル名
AUTHOR = "zionadchat" # 作者名
# ★ このスクリプト自身のID (コピー機能でテキスト検索に使用されます)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SQUARE_TORUS_2026_02_27_V2 ###"
# ==============================================================================
# システム初期化 & ID管理
# ==============================================================================
bl_info = {
"name": f"{ADDON_NAME} {PREFIX}",
"author": AUTHOR,
"version": (4, 1, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": f"Unique Material Square Torus Generator - {PREFIX}",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
ADDON_LINKS = (
{"label": "Code Copy Template", "url": "<https://www.notion.so/Code-copy-20260221>"},
{"label": "Theory Background", "url": "<https://www.notion.so/Einstein-from-20260119>"},
)
# ==============================================================================
# デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"show_preview": True,
"show_square_guide": True,
"torus_color": (0.0391, 0.8000, 0.1647, 0.8000),
"torus_loc": (0.0000, 0.0000, 0.0000),
"torus_rot": (0.0000, 0.0000, 0.0000),
"square_size": 10.0000,
"corner_radius": 0.0000,
"minor_radius": 0.5000,
"corner_segments": 8,
"minor_segments": 16,
"torus_plane": "XY",
}
# <END_DICT>
# ==============================================================================
# 数学的 角丸正方形トーラス生成 & ガイド生成
# ==============================================================================
def create_square_guide_bmesh(bm, square_size):
""" プレビュー用の正方形ガイド(線のみ)を生成 """
S = square_size / 2.0
v1 = bm.verts.new((S, S, 0))
v2 = bm.verts.new((-S, S, 0))
v3 = bm.verts.new((-S, -S, 0))
v4 = bm.verts.new((S, -S, 0))
bm.verts.ensure_lookup_table()
bm.edges.new((v1, v2))
bm.edges.new((v2, v3))
bm.edges.new((v3, v4))
bm.edges.new((v4, v1))
return bm
def create_square_torus_bmesh(bm, square_size, corner_radius, minor_radius, corner_segments, minor_segments):
""" 正方形の枠に沿ったトーラス(チューブ)を生成 """
half_size = square_size / 2.0
actual_corner_radius = min(max(corner_radius, 0.0), half_size)
rings =[]
# ★ Corner Radius が 0 の場合、完全な直角(90度)で斜めに繋がるよう生成
if actual_corner_radius <= 0.001:
L = half_size
corners =[
(mathutils.Vector((L, L, 0)), mathutils.Vector((1, 1, 0)).normalized()),
(mathutils.Vector((-L, L, 0)), mathutils.Vector((-1, 1, 0)).normalized()),
(mathutils.Vector((-L, -L, 0)), mathutils.Vector((-1, -1, 0)).normalized()),
(mathutils.Vector((L, -L, 0)), mathutils.Vector((1, -1, 0)).normalized())
]
scale_xy = 1.0 / math.cos(math.pi / 4) # 直角接合のためのスケール(約1.414倍)
for p, n in corners:
b = mathutils.Vector((0, 0, 1)) # Z方向
ring =[]
for j in range(minor_segments):
theta = j * 2.0 * math.pi / minor_segments
# 法線方向を √2 倍に伸ばして斜め接合(マイタージョイント)を実現
offset = n * (minor_radius * math.cos(theta) * scale_xy) + b * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
# ★ それ以外は角丸で生成
else:
L = half_size - actual_corner_radius
pts =[]
# 0: 右上, 1: 左上, 2: 左下, 3: 右下 の4つの角を順に生成
for q in range(4):
cx = L if q in [0, 3] else -L
cy = L if q in [0, 1] else -L
for i in range(corner_segments + 1):
angle = q * (math.pi / 2) + i * (math.pi / 2) / corner_segments
x = cx + actual_corner_radius * math.cos(angle)
y = cy + actual_corner_radius * math.sin(angle)
nx = math.cos(angle)
ny = math.sin(angle)
pts.append((mathutils.Vector((x, y, 0)), mathutils.Vector((nx, ny, 0))))
for p, n in pts:
b = mathutils.Vector((0, 0, 1))
ring =[]
for j in range(minor_segments):
theta = j * 2.0 * math.pi / minor_segments
offset = n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
bm.verts.ensure_lookup_table()
# 断面同士を繋いで面(ポリゴン)を張る
total_rings = len(rings)
for i in range(total_rings):
next_i = (i + 1) % total_rings
ring1 = rings[i]
ring2 = rings[next_i]
for j in range(minor_segments):
next_j = (j + 1) % minor_segments
bm.faces.new((ring1[j], ring2[j], ring2[next_j], ring1[next_j]))
# 滑らかに表示
for f in bm.faces:
f.smooth = True
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
def apply_auto_smooth(mesh):
""" 直角部分の影を綺麗にするための自動スムース設定 """
try:
if hasattr(mesh, "use_auto_smooth"):
mesh.use_auto_smooth = True
mesh.auto_smooth_angle = math.radians(30)
except: pass
# ==============================================================================
# マテリアル作成ロジック (実体化用)
# ==============================================================================
def create_unique_material(color, name_prefix="Mat"):
timestamp = datetime.now().strftime('%M%S%f')[:5]
mat_name = f"{name_prefix}_{timestamp}"
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
if mat.use_nodes:
tree = mat.node_tree
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (0, 0)
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (300, 0)
tree.links.new(bsdf.outputs[0], out.inputs[0])
if "Base Color" in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = color[3]
return mat
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_OBJ_NAME = f"[Preview] SqTorus_{PREFIX}"
PREVIEW_GUIDE_NAME = f"[Preview] SqGuide_{PREFIX}"
PREVIEW_MESH_NAME = f"PreviewMesh_{PREFIX}"
PREVIEW_MAT_NAME = f"PreviewMat_{PREFIX}"
def get_or_create_preview_material():
mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
if not mat:
mat = bpy.data.materials.new(name=PREVIEW_MAT_NAME)
mat.use_nodes = True
mat.blend_method = 'BLEND'
return mat
def update_preview_material(mat, color):
if mat.use_nodes:
bsdf = None
for node in mat.node_tree.nodes:
if node.type == 'BSDF_PRINCIPLED':
bsdf = node
break
if not bsdf:
mat.node_tree.nodes.clear()
bsdf = mat.node_tree.nodes.new("ShaderNodeBsdfPrincipled")
out = mat.node_tree.nodes.new("ShaderNodeOutputMaterial")
mat.node_tree.links.new(bsdf.outputs[0], out.inputs[0])
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
def get_transform_matrix(props):
rot_matrix = mathutils.Matrix.Identity(4)
if props.torus_plane == 'YZ':
rot_matrix = mathutils.Matrix.Rotation(math.radians(90.0), 4, 'Y')
elif props.torus_plane == 'ZX':
rot_matrix = mathutils.Matrix.Rotation(math.radians(-90.0), 4, 'X')
user_rot = mathutils.Euler((
math.radians(props.torus_rot[0]),
math.radians(props.torus_rot[1]),
math.radians(props.torus_rot[2])
), 'XYZ').to_matrix().to_4x4()
loc_matrix = mathutils.Matrix.Translation(mathutils.Vector(props.torus_loc))
return loc_matrix @ user_rot @ rot_matrix
def update_preview_geometry(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_NAME)
context.scene.collection.children.link(col)
obj = bpy.data.objects.get(PREVIEW_OBJ_NAME)
guide_obj = bpy.data.objects.get(PREVIEW_GUIDE_NAME)
if not props.show_preview:
if obj: bpy.data.objects.remove(obj, do_unlink=True)
if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)
return
final_matrix = get_transform_matrix(props)
# --- 四角いトーラス本体の更新 ---
bm = bmesh.new()
try:
create_square_torus_bmesh(
bm,
square_size=props.square_size,
corner_radius=props.corner_radius,
minor_radius=props.minor_radius,
corner_segments=props.corner_segments,
minor_segments=props.minor_segments
)
bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
mesh = bpy.data.meshes.get(PREVIEW_MESH_NAME)
if not mesh: mesh = bpy.data.meshes.new(PREVIEW_MESH_NAME)
else: mesh.clear_geometry()
bm.to_mesh(mesh)
apply_auto_smooth(mesh)
mesh.update()
finally: bm.free()
if not obj:
obj = bpy.data.objects.new(PREVIEW_OBJ_NAME, mesh)
col.objects.link(obj)
elif obj.data != mesh: obj.data = mesh
mat = get_or_create_preview_material()
update_preview_material(mat, props.torus_color)
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
# --- 正方形ガイドの更新 ---
if props.show_square_guide:
bm_g = bmesh.new()
try:
create_square_guide_bmesh(bm_g, props.square_size)
bmesh.ops.transform(bm_g, matrix=final_matrix, verts=bm_g.verts)
mesh_g = bpy.data.meshes.get(PREVIEW_MESH_NAME + "_Guide")
if not mesh_g: mesh_g = bpy.data.meshes.new(PREVIEW_MESH_NAME + "_Guide")
else: mesh_g.clear_geometry()
bm_g.to_mesh(mesh_g)
mesh_g.update()
finally: bm_g.free()
if not guide_obj:
guide_obj = bpy.data.objects.new(PREVIEW_GUIDE_NAME, mesh_g)
col.objects.link(guide_obj)
elif guide_obj.data != mesh_g: guide_obj.data = mesh_g
guide_obj.display_type = 'WIRE'
guide_obj.show_in_front = True
else:
if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)
_timer = None
def delayed_update():
global _timer
_timer = None
if bpy.context and bpy.context.scene: update_preview_geometry(bpy.context)
return None
def on_update(self, context):
global _timer
if _timer:
try: bpy.app.timers.unregister(_timer)
except: pass
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_TorusProps(PropertyGroup):
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
show_square_guide: BoolProperty(name="Show Square Guide", default=CURRENT_DEFAULTS['show_square_guide'], update=on_update)
torus_color: FloatVectorProperty(name="Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['torus_color'], update=on_update)
torus_plane: EnumProperty(
name="Plane",
items=[('XY', "XY Plane", ""), ('YZ', "YZ Plane", ""), ('ZX', "ZX Plane", "")],
default=CURRENT_DEFAULTS['torus_plane'], update=on_update
)
torus_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['torus_loc'], update=on_update)
torus_rot: FloatVectorProperty(name="Rotation (Deg)", size=3, default=CURRENT_DEFAULTS['torus_rot'], update=on_update)
square_size: FloatProperty(name="Square Size", default=CURRENT_DEFAULTS['square_size'], min=0.1, update=on_update)
corner_radius: FloatProperty(name="Corner Radius", default=CURRENT_DEFAULTS['corner_radius'], min=0.0, update=on_update)
minor_radius: FloatProperty(name="Tube Thickness", default=CURRENT_DEFAULTS['minor_radius'], min=0.01, update=on_update)
corner_segments: IntProperty(name="Corner Segs", default=CURRENT_DEFAULTS['corner_segments'], min=1, update=on_update)
minor_segments: IntProperty(name="Tube Segs", default=CURRENT_DEFAULTS['minor_segments'], min=3, update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CreateTorus(Operator):
bl_idname = f"{OP_PREFIX}.create_torus"
bl_label = "Create Square Torus"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
bm = bmesh.new()
create_square_torus_bmesh(
bm,
square_size=props.square_size,
corner_radius=props.corner_radius,
minor_radius=props.minor_radius,
corner_segments=props.corner_segments,
minor_segments=props.minor_segments
)
final_matrix = get_transform_matrix(props)
bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
mesh = bpy.data.meshes.new(f"SquareTorus_Mesh")
bm.to_mesh(mesh)
bm.free()
apply_auto_smooth(mesh)
obj = bpy.data.objects.new(f"SqTorus_{datetime.now().strftime('%H%M%S')}", mesh)
if context.collection: context.collection.objects.link(obj)
else: context.scene.collection.objects.link(obj)
unique_mat = create_unique_material(props.torus_color, "Mat_UniqueSqTorus")
obj.data.materials.append(unique_mat)
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
context.view_layer.objects.active = obj
self.report({'INFO'}, "Created Square Torus!")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string(): target_text = t; break
if not target_text:
self.report({'ERROR'}, "Script source not found.")
return {'CANCELLED'}
code = target_text.as_string()
c, l, r = props.torus_color, props.torus_loc, props.torus_rot
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "show_square_guide": {props.show_square_guide},\n'
new_dict += f' "torus_color": ({c[0]:.4f}, {c[1]:.4f}, {c[2]:.4f}, {c[3]:.4f}),\n'
new_dict += f' "torus_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
new_dict += f' "torus_rot": ({r[0]:.4f}, {r[1]:.4f}, {r[2]:.4f}),\n'
new_dict += f' "square_size": {props.square_size:.4f},\n'
new_dict += f' "corner_radius": {props.corner_radius:.4f},\n'
new_dict += f' "minor_radius": {props.minor_radius:.4f},\n'
new_dict += f' "corner_segments": {props.corner_segments},\n'
new_dict += f' "minor_segments": {props.minor_segments},\n'
new_dict += f' "torus_plane": "{props.torus_plane}",\n'
new_dict += "}\n"
try:
start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
pre, post = code.split(start)[0], code.split(end)[1]
final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
context.window_manager.clipboard = final
self.report({'INFO'}, "Code copied!")
except: return {'CANCELLED'}
return {'FINISHED'}
class OT_Reset(Operator):
bl_idname = f"{OP_PREFIX}.reset"
bl_label = "Reset Transform"
def execute(self, context):
p = getattr(context.scene, PROPS_NAME)
p.torus_loc = (0,0,0)
p.torus_rot = (0,0,0)
p.torus_plane = 'XY'
p.square_size = 10.0
p.corner_radius = 0.0
p.minor_radius = 0.5
return {'FINISHED'}
class OT_OpenUrl(Operator):
bl_idname = f"{OP_PREFIX}.open_url"; bl_label = "Open URL"; url: StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class OT_RemoveAddon(Operator):
bl_idname = f"{OP_PREFIX}.remove_addon"; bl_label = "Remove Addon"
def execute(self, context):
bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_MainPanel(Panel):
bl_label = PANEL_TITLE
bl_idname = f"{PREFIX}_PT_main"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: layout.label(text="Reload Script"); return
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
box = layout.box()
if not props.show_preview:
box.label(text="Preview is Hidden", icon='INFO')
box.prop(props, "torus_color")
col = box.column(align=True)
col.prop(props, "torus_plane")
col.prop(props, "torus_loc")
col.prop(props, "torus_rot")
box.separator()
# ガイドトグルとサイズ設定
box.prop(props, "show_square_guide", icon='MESH_PLANE')
col_s = box.column(align=True)
col_s.prop(props, "square_size")
# 角の丸み (0で直角になることを明示)
row_cr = col_s.row()
row_cr.prop(props, "corner_radius")
if props.corner_radius <= 0.001:
row_cr.label(text="[90° Mode]", icon='SNAP_VERTEX')
col_s.prop(props, "minor_radius")
row_seg = box.row()
row_seg.prop(props, "corner_segments")
row_seg.prop(props, "minor_segments")
box.operator(OT_Reset.bl_idname, icon='LOOP_BACK')
layout.separator()
col_exec = layout.column()
col_exec.scale_y = 1.5
col_exec.operator(OT_CreateTorus.bl_idname, icon='MESH_TORUS', text="Create Square Torus")
class PT_LinksPanel(Panel):
bl_label = "Links"; bl_idname = f"{PREFIX}_PT_links"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
for l in ADDON_LINKS: self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"]).url = l["url"]
class PT_RemovePanel(Panel):
bl_label = "System"; bl_idname = f"{PREFIX}_PT_remove"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): self.layout.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Remove Addon")
# ==============================================================================
# REGISTER
# ==============================================================================
classes = (PG_TorusProps, OT_CreateTorus, OT_CopyFullScript, OT_Reset, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, PT_LinksPanel, PT_RemovePanel)
def auto_open_sidebar():
try:
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
if not space.show_region_ui:
space.show_region_ui = True
except: pass
return None
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_TorusProps))
bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)
def unregister():
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes): bpy.utils.unregister_class(c)
if __name__ == "__main__": register()
「Corner Radius(角の丸み)」を 0 に設定した際に、完全な90度(直角)の枠が生成されるように専用の計算ロジックを追加しました。
単純に丸みをゼロにすると角が潰れてしまうため、額縁のように斜め45度で綺麗に接合される「マイタージョイント」方式を自動で計算するようにしています。
また、直角になった角の影が綺麗に表示されるように「自動スムース(Auto Smooth)」の処理も内部で追加しました。
# Copied: 15:00:01
import bpy
import bmesh
import webbrowser
import math
import mathutils
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime
# ==============================================================================
# 【 基本設定エリア 】 先頭で Prefix や 表示名 を変更できます
# ==============================================================================
PREFIX = "Torus20260227" # 内部ID用プレフィックス (大文字は自動で小文字化されます)
ADDON_NAME = "zionad 520[ Torus Gen ]" # アドオンの表示名 (環境設定などに表示)
TAB_NAME = " [ Torus copy ] " # 3Dビューサイドバー(Nパネル)のタブ名
PANEL_TITLE = "Torus Generator" # メインパネルのタイトル名
AUTHOR = "zionadchat" # 作者名
# ★ このスクリプト自身のID (コピー機能でテキスト検索に使用されます)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: TORUS_2026_02_27_V1 ###"
# ==============================================================================
# システム初期化 & ID管理
# ==============================================================================
bl_info = {
"name": f"{ADDON_NAME} {PREFIX}",
"author": AUTHOR,
"version": (3, 2, 1),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": f"Unique Material Torus Generator - {PREFIX}",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
ADDON_LINKS = (
{"label": "Code Copy Template", "url": "<https://www.notion.so/Code-copy-20260221>"},
{"label": "Theory Background", "url": "<https://www.notion.so/Einstein-from-20260119>"},
)
# ==============================================================================
# デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"show_preview": True,
"torus_color": (0.0391, 0.8000, 0.1647, 0.8000),
"torus_loc": (0.0000, 0.0000, 0.0000),
"torus_rot": (0.0000, 0.0000, 0.0000),
"torus_major_radius": 5.0000,
"torus_minor_radius": 1.0000,
"torus_major_segments": 48,
"torus_minor_segments": 16,
"torus_plane": "XY",
}
# <END_DICT>
# ==============================================================================
# 数学的トーラス生成 (bmesh.opsにtorusが無いため自作)
# ==============================================================================
def create_torus_bmesh(bm, major_segments, minor_segments, major_radius, minor_radius):
""" メモリ上で直接トーラスの頂点と面を計算して生成する超軽量関数 """
verts =[]
# 頂点の生成
for i in range(major_segments):
phi = i * 2.0 * math.pi / major_segments
cos_phi = math.cos(phi)
sin_phi = math.sin(phi)
row =[]
for j in range(minor_segments):
theta = j * 2.0 * math.pi / minor_segments
cos_theta = math.cos(theta)
sin_theta = math.sin(theta)
x = (major_radius + minor_radius * cos_theta) * cos_phi
y = (major_radius + minor_radius * cos_theta) * sin_phi
z = minor_radius * sin_theta
row.append(bm.verts.new((x, y, z)))
verts.append(row)
bm.verts.ensure_lookup_table()
# 面(ポリゴン)の生成
for i in range(major_segments):
for j in range(minor_segments):
v1 = verts[i][j]
v2 = verts[(i + 1) % major_segments][j]
v3 = verts[(i + 1) % major_segments][(j + 1) % minor_segments]
v4 = verts[i][(j + 1) % minor_segments]
bm.faces.new((v1, v2, v3, v4))
# スムーズシェード化と法線計算
for f in bm.faces:
f.smooth = True
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
# ==============================================================================
# マテリアル作成ロジック (実体化用)
# ==============================================================================
def create_unique_material(color, name_prefix="Mat"):
timestamp = datetime.now().strftime('%M%S%f')[:5]
mat_name = f"{name_prefix}_{timestamp}"
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
mat.diffuse_color = color
if mat.use_nodes:
tree = mat.node_tree
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (0, 0)
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (300, 0)
tree.links.new(bsdf.outputs[0], out.inputs[0])
if "Base Color" in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = color[3]
return mat
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_OBJ_NAME = f"[Preview] Torus_{PREFIX}"
PREVIEW_MESH_NAME = f"PreviewMesh_{PREFIX}"
PREVIEW_MAT_NAME = f"PreviewMat_{PREFIX}"
def get_or_create_preview_material():
mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
if not mat:
mat = bpy.data.materials.new(name=PREVIEW_MAT_NAME)
mat.use_nodes = True
mat.blend_method = 'BLEND'
return mat
def update_preview_material(mat, color):
mat.diffuse_color = color
if mat.use_nodes:
bsdf = None
for node in mat.node_tree.nodes:
if node.type == 'BSDF_PRINCIPLED':
bsdf = node
break
if not bsdf:
mat.node_tree.nodes.clear()
bsdf = mat.node_tree.nodes.new("ShaderNodeBsdfPrincipled")
out = mat.node_tree.nodes.new("ShaderNodeOutputMaterial")
mat.node_tree.links.new(bsdf.outputs[0], out.inputs[0])
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
def get_torus_transform_matrix(props):
rot_matrix = mathutils.Matrix.Identity(4)
# 平面設定 (ベースはXY平面)
if props.torus_plane == 'YZ':
rot_matrix = mathutils.Matrix.Rotation(math.radians(90.0), 4, 'Y')
elif props.torus_plane == 'ZX':
rot_matrix = mathutils.Matrix.Rotation(math.radians(-90.0), 4, 'X')
# XYZ ユーザー回転
user_rot = mathutils.Euler((
math.radians(props.torus_rot[0]),
math.radians(props.torus_rot[1]),
math.radians(props.torus_rot[2])
), 'XYZ').to_matrix().to_4x4()
# 位置
loc_matrix = mathutils.Matrix.Translation(mathutils.Vector(props.torus_loc))
return loc_matrix @ user_rot @ rot_matrix
def update_preview_geometry(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_NAME)
context.scene.collection.children.link(col)
obj = bpy.data.objects.get(PREVIEW_OBJ_NAME)
if not props.show_preview:
if obj: bpy.data.objects.remove(obj, do_unlink=True)
return
bm = bmesh.new()
try:
# 自作のトーラス生成関数を呼び出し
create_torus_bmesh(
bm,
major_segments=props.torus_major_segments,
minor_segments=props.torus_minor_segments,
major_radius=props.torus_major_radius,
minor_radius=props.torus_minor_radius
)
final_matrix = get_torus_transform_matrix(props)
bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
mesh = bpy.data.meshes.get(PREVIEW_MESH_NAME)
if not mesh:
mesh = bpy.data.meshes.new(PREVIEW_MESH_NAME)
else:
mesh.clear_geometry()
bm.to_mesh(mesh)
mesh.update()
finally:
bm.free()
if not obj:
obj = bpy.data.objects.new(PREVIEW_OBJ_NAME, mesh)
col.objects.link(obj)
elif obj.data != mesh:
obj.data = mesh
mat = get_or_create_preview_material()
update_preview_material(mat, props.torus_color)
if not obj.data.materials:
obj.data.materials.append(mat)
else:
obj.data.materials[0] = mat
_timer = None
def delayed_update():
global _timer
_timer = None
if bpy.context and bpy.context.scene: update_preview_geometry(bpy.context)
return None
def on_update(self, context):
global _timer
if _timer:
try: bpy.app.timers.unregister(_timer)
except: pass
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_TorusProps(PropertyGroup):
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
torus_color: FloatVectorProperty(name="Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['torus_color'], update=on_update)
torus_plane: EnumProperty(
name="Plane",
items=[('XY', "XY Plane", ""), ('YZ', "YZ Plane", ""), ('ZX', "ZX Plane", "")],
default=CURRENT_DEFAULTS['torus_plane'], update=on_update
)
torus_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['torus_loc'], update=on_update)
torus_rot: FloatVectorProperty(name="Rotation (Deg)", size=3, default=CURRENT_DEFAULTS['torus_rot'], update=on_update)
torus_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['torus_major_radius'], min=0.01, update=on_update)
torus_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['torus_minor_radius'], min=0.01, update=on_update)
torus_major_segments: IntProperty(name="Major Segments", default=CURRENT_DEFAULTS['torus_major_segments'], min=3, update=on_update)
torus_minor_segments: IntProperty(name="Minor Segments", default=CURRENT_DEFAULTS['torus_minor_segments'], min=3, update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CreateTorus(Operator):
bl_idname = f"{OP_PREFIX}.create_torus"
bl_label = "Create Torus"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
bm = bmesh.new()
# 自作のトーラス生成関数を呼び出し
create_torus_bmesh(
bm,
major_segments=props.torus_major_segments,
minor_segments=props.torus_minor_segments,
major_radius=props.torus_major_radius,
minor_radius=props.torus_minor_radius
)
final_matrix = get_torus_transform_matrix(props)
bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
mesh = bpy.data.meshes.new(f"Torus_Mesh")
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(f"Torus_{datetime.now().strftime('%H%M%S')}", mesh)
if context.collection: context.collection.objects.link(obj)
else: context.scene.collection.objects.link(obj)
unique_mat = create_unique_material(props.torus_color, "Mat_UniqueTorus")
obj.data.materials.append(unique_mat)
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
context.view_layer.objects.active = obj
self.report({'INFO'}, "Created Torus with Unique Material")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string():
target_text = t; break
if not target_text:
self.report({'ERROR'}, "Script source not found.")
return {'CANCELLED'}
code = target_text.as_string()
c, l, r = props.torus_color, props.torus_loc, props.torus_rot
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "torus_color": ({c[0]:.4f}, {c[1]:.4f}, {c[2]:.4f}, {c[3]:.4f}),\n'
new_dict += f' "torus_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
new_dict += f' "torus_rot": ({r[0]:.4f}, {r[1]:.4f}, {r[2]:.4f}),\n'
new_dict += f' "torus_major_radius": {props.torus_major_radius:.4f},\n'
new_dict += f' "torus_minor_radius": {props.torus_minor_radius:.4f},\n'
new_dict += f' "torus_major_segments": {props.torus_major_segments},\n'
new_dict += f' "torus_minor_segments": {props.torus_minor_segments},\n'
new_dict += f' "torus_plane": "{props.torus_plane}",\n'
new_dict += "}\n"
try:
start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
pre, post = code.split(start)[0], code.split(end)[1]
final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
context.window_manager.clipboard = final
self.report({'INFO'}, "Code copied!")
except: return {'CANCELLED'}
return {'FINISHED'}
class OT_Reset(Operator):
bl_idname = f"{OP_PREFIX}.reset"
bl_label = "Reset Transform"
def execute(self, context):
p = getattr(context.scene, PROPS_NAME)
p.torus_loc = (0,0,0)
p.torus_rot = (0,0,0)
p.torus_plane = 'XY'
p.torus_major_radius = 5.0
p.torus_minor_radius = 1.0
p.torus_major_segments = 48
p.torus_minor_segments = 16
return {'FINISHED'}
class OT_OpenUrl(Operator):
bl_idname = f"{OP_PREFIX}.open_url"; bl_label = "Open URL"; url: StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class OT_RemoveAddon(Operator):
bl_idname = f"{OP_PREFIX}.remove_addon"; bl_label = "Remove Addon"
def execute(self, context):
bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_MainPanel(Panel):
bl_label = PANEL_TITLE
bl_idname = f"{PREFIX}_PT_main"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: layout.label(text="Reload Script"); return
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
box = layout.box()
if not props.show_preview:
box.label(text="Preview is Hidden", icon='INFO')
box.prop(props, "torus_color")
col = box.column(align=True)
col.prop(props, "torus_plane")
col.prop(props, "torus_loc")
col.prop(props, "torus_rot")
row_r = box.row()
row_r.prop(props, "torus_major_radius", text="Maj Radius")
row_r.prop(props, "torus_minor_radius", text="Min Radius")
row_s = box.row()
row_s.prop(props, "torus_major_segments", text="Maj Segs")
row_s.prop(props, "torus_minor_segments", text="Min Segs")
box.operator(OT_Reset.bl_idname, icon='LOOP_BACK')
layout.separator()
col_exec = layout.column()
col_exec.scale_y = 1.5
col_exec.operator(OT_CreateTorus.bl_idname, icon='MESH_TORUS', text="Create Torus (Unique)")
class PT_LinksPanel(Panel):
bl_label = "Links"; bl_idname = f"{PREFIX}_PT_links"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
for l in ADDON_LINKS: self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"]).url = l["url"]
class PT_RemovePanel(Panel):
bl_label = "System"; bl_idname = f"{PREFIX}_PT_remove"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): self.layout.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Remove Addon")
# ==============================================================================
# REGISTER
# ==============================================================================
classes = (PG_TorusProps, OT_CreateTorus, OT_CopyFullScript, OT_Reset, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, PT_LinksPanel, PT_RemovePanel)
def auto_open_sidebar():
""" 登録時に3Dビューのサイドバーを自動で開く """
try:
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
if not space.show_region_ui:
space.show_region_ui = True
except: pass
return None
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_TorusProps))
# 0.1秒後にサイドバーを開く
bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)
def unregister():
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes): bpy.utils.unregister_class(c)
if __name__ == "__main__": register()