blender Million 2026
アイスクリーム コーン 20260327
# Copied: 2026-03-27 13:14:00
import bpy
import bmesh
import webbrowser
import math
import mathutils
import time
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime
# ==============================================================================
# 【 基本設定エリア 】
# ==============================================================================
PREFIX = "ShapeTorus20260324"
ADDON_NAME = "zionad 520[ Shape-Torus ]"
TAB_NAME = "[ Shape Torus copy ] "
PANEL_TITLE = "Multi-Shape Generator"
AUTHOR = "zionadchat"
# ★ このスクリプト自身のID
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SHAPE_TORUS_2026_03_24_V9_MULTI_SHAPE ###"
# ==============================================================================
# システム初期化 & ID管理
# ==============================================================================
bl_info = {
"name": f"{ADDON_NAME} {PREFIX}",
"author": AUTHOR,
"version": (9, 2, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": f"Multi-Shape Torus & Pie Generator - {PREFIX}",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
PIE_PROPS_NAME = f"{PREFIX}_pie_props"
ADDON_LINKS = (
{"label": "アイスクリーム コーン 20260327", "url": "<https://www.notion.so/20260327-32ff5dacaf43801cbc83c834db284e5f>"},
{"label": "Prefix トーラス正方形 20260324", "url": "<https://www.notion.so/Prefix-20260324-32df5dacaf4380528980db6a989d6306>"},
)
# ==============================================================================
# デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"show_preview": True,
"show_guide": True,
"torus_color": (0.0391, 0.8000, 0.1647, 1.0000),
"torus_loc": (0.0000, 0.0000, 0.0000),
"torus_rot": (0.0000, 0.0000, 0.0000),
"base_shape": "SQUARE",
"torus_plane": "XY",
"size_x": 10.0000,
"size_y": 5.0000,
"corner_radius": 0.0000,
"minor_radius": 0.5000,
"major_segments": 32,
"corner_segments": 8,
"minor_segments": 16,
"pie_show_preview": True,
"pie_plane": "XY",
"pie_radius": 100.0000,
"pie_thickness": 10.0000,
"pie_start_angle": 45.0000,
"pie_end_angle": 135.0000,
"color_tri": (1.0000, 0.2000, 0.2000, 1.0000),
"color_bow": (0.2000, 0.5000, 1.0000, 1.0000),
"color_rest": (0.8000, 0.8000, 0.8000, 1.0000),
"pie_segments": 64,
}
# <END_DICT>
# ==============================================================================
# データ クリーンアップ管理
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_OBJ_NAME = f"[Preview] Shape_{PREFIX}"
PREVIEW_GUIDE_NAME = f"[Preview] Guide_{PREFIX}"
PREVIEW_MAT_NAME = f"PreviewMat_{PREFIX}"
PREVIEW_PIE_TRI_NAME = f"[Preview] Pie_Tri_{PREFIX}"
PREVIEW_PIE_BOW_NAME = f"[Preview] Pie_Bow_{PREFIX}"
PREVIEW_PIE_REST_NAME = f"[Preview] Pie_Rest_{PREFIX}"
def cleanup_preview_data():
preview_objs =[
PREVIEW_OBJ_NAME, PREVIEW_GUIDE_NAME,
PREVIEW_PIE_TRI_NAME, PREVIEW_PIE_BOW_NAME, PREVIEW_PIE_REST_NAME
]
for name in preview_objs:
obj = bpy.data.objects.get(name)
if obj:
mesh = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if mesh and mesh.users == 0:
bpy.data.meshes.remove(mesh)
meshes_to_remove =[m for m in bpy.data.meshes if m.name.startswith(f"PreviewMesh_{PREFIX}") or m.name.startswith("Mesh_[Preview]")]
for m in meshes_to_remove:
if m.users == 0:
bpy.data.meshes.remove(m)
mat_names =[PREVIEW_MAT_NAME, f"Mat_{PREVIEW_PIE_TRI_NAME}", f"Mat_{PREVIEW_PIE_BOW_NAME}", f"Mat_{PREVIEW_PIE_REST_NAME}"]
for m_name in mat_names:
mat = bpy.data.materials.get(m_name)
if mat and mat.users == 0:
bpy.data.materials.remove(mat)
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if col and len(col.objects) == 0:
bpy.data.collections.remove(col)
def cleanup_old_materials(prefix="Mat_UniqueShape", limit=50):
mats =[m for m in bpy.data.materials if m.name.startswith(prefix)]
if len(mats) > limit:
for m in mats[:-limit]:
if m.users == 0:
bpy.data.materials.remove(m)
# ==============================================================================
# ガイド & メッシュ生成エンジン (Torus)
# ==============================================================================
def create_square_guide_bmesh(bm, size):
S = 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_cube_guide_bmesh(bm, size):
geom = bmesh.ops.create_cube(bm, size=size)
faces = [f for f in bm.faces]
bmesh.ops.delete(bm, geom=faces, context='FACES_ONLY')
return bm
def create_ellipse_guide_bmesh(bm, size_x, size_y, segments=64):
a = size_x / 2.0
b = size_y / 2.0
verts =[]
for i in range(segments):
t = i * 2.0 * math.pi / segments
verts.append(bm.verts.new((a * math.cos(t), b * math.sin(t), 0)))
bm.verts.ensure_lookup_table()
for i in range(segments):
bm.edges.new((verts[i], verts[(i + 1) % segments]))
return bm
def create_ellipse_torus_bmesh(bm, size_x, size_y, minor_radius, major_segments, minor_segments):
a = size_x / 2.0
b = size_y / 2.0
rings =[]
for i in range(major_segments):
t = i * 2.0 * math.pi / major_segments
x = a * math.cos(t)
y = b * math.sin(t)
p = mathutils.Vector((x, y, 0))
nx = b * math.cos(t)
ny = a * math.sin(t)
n = mathutils.Vector((nx, ny, 0)).normalized()
up = 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)) + up * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
bm.verts.ensure_lookup_table()
edge_loops = []
for ring in rings:
edges =[]
for j in range(minor_segments):
v1 = ring[j]
v2 = ring[(j + 1) % minor_segments]
edges.append(bm.edges.new((v1, v2)))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(major_segments):
next_i = (i + 1) % major_segments
try:
bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[next_i])
except Exception:
pass
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
for f in bm.faces: f.smooth = True
if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
def create_square_torus_bmesh(bm, size, corner_radius, minor_radius, corner_segments, minor_segments):
half_size = size / 2.0
actual_corner_radius = min(max(corner_radius, 0.0), half_size)
rings =[]
EPS = 1e-6
if actual_corner_radius < EPS:
L = half_size
corners =[
(mathutils.Vector((L, L, 0)), mathutils.Vector((1, 1, 0)).normalized()),
(mathutils.Vector((-L, L, 0)), mathutils.Vector((-1, 1, 0)).normalized()),
(mathutils.Vector((-L, -L, 0)), mathutils.Vector((-1, -1, 0)).normalized()),
(mathutils.Vector((L, -L, 0)), mathutils.Vector((1, -1, 0)).normalized())
]
scale_xy = 1.0 / math.cos(math.pi / 4)
for p, n in corners:
b = mathutils.Vector((0, 0, 1))
ring =[]
for j in range(minor_segments):
theta = j * 2.0 * math.pi / minor_segments
offset = n * (minor_radius * math.cos(theta) * scale_xy) + b * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
else:
L = half_size - actual_corner_radius
pts =[]
for q in range(4):
cx = L if q in [0, 3] else -L
cy = L if q in[0, 1] else -L
for i in range(corner_segments + 1):
angle = q * (math.pi / 2) + i * (math.pi / 2) / corner_segments
x = cx + actual_corner_radius * math.cos(angle)
y = cy + actual_corner_radius * math.sin(angle)
pts.append((mathutils.Vector((x, y, 0)), mathutils.Vector((math.cos(angle), math.sin(angle), 0))))
unique_pts =[]
for p, n in pts:
if not unique_pts or (unique_pts[-1][0] - p).length > EPS:
unique_pts.append((p, n))
if len(unique_pts) > 1 and (unique_pts[-1][0] - unique_pts[0][0]).length < EPS:
unique_pts.pop()
for p, n in unique_pts:
b = mathutils.Vector((0, 0, 1))
ring =[]
for j in range(minor_segments):
theta = j * 2.0 * math.pi / minor_segments
offset = n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
bm.verts.ensure_lookup_table()
total_rings = len(rings)
if total_rings < 3: return bm
edge_loops =[]
for ring in rings:
edges =[]
for j in range(minor_segments):
v1 = ring[j]
v2 = ring[(j + 1) % minor_segments]
edges.append(bm.edges.new((v1, v2)))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(total_rings):
next_i = (i + 1) % total_rings
try:
bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[next_i])
except Exception:
pass
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
for f in bm.faces: f.smooth = True
if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
def create_cube_framework_bmesh(bm, size, minor_radius, minor_segments):
L = size / 2.0
verts_co =[
mathutils.Vector(( L, L, L)), mathutils.Vector((-L, L, L)),
mathutils.Vector((-L, -L, L)), mathutils.Vector(( L, -L, L)),
mathutils.Vector(( L, L, -L)), mathutils.Vector((-L, L, -L)),
mathutils.Vector((-L, -L, -L)), mathutils.Vector(( L, -L, -L)),
]
edges_idx =[
(0,1), (1,2), (2,3), (3,0),
(4,5), (5,6), (6,7), (7,4),
(0,4), (1,5), (2,6), (3,7)
]
for co in verts_co:
geom = bmesh.ops.create_uvsphere(bm, u_segments=minor_segments, v_segments=max(minor_segments//2, 3), radius=minor_radius)
bmesh.ops.translate(bm, verts=geom['verts'], vec=co)
for idx1, idx2 in edges_idx:
v1 = verts_co[idx1]
v2 = verts_co[idx2]
dist = (v1 - v2).length
center = (v1 + v2) / 2.0
geom = bmesh.ops.create_cone(
bm, cap_ends=False, cap_tris=False, segments=minor_segments,
radius1=minor_radius, radius2=minor_radius, depth=dist
)
axis = (v1 - v2).normalized()
rot = mathutils.Vector((0,0,1)).rotation_difference(axis)
bmesh.ops.transform(bm, matrix=rot.to_matrix().to_4x4(), verts=geom['verts'])
bmesh.ops.translate(bm, verts=geom['verts'], vec=center)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
for f in bm.faces: f.smooth = True
if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
def apply_auto_smooth(mesh):
if bpy.app.version < (4, 1, 0):
try:
if hasattr(mesh, "use_auto_smooth"):
mesh.use_auto_smooth = True
mesh.auto_smooth_angle = math.radians(30)
except AttributeError:
pass
# ==============================================================================
# Pie / Sector / Triangle ジェネレーター (NEW)
# ==============================================================================
def generate_prism_bmesh(bm, verts_2d, thickness, plane):
""" Bmesh上に押し出し立体を生成する """
z_top = thickness / 2.0
z_bot = -thickness / 2.0
top_verts =[bm.verts.new((x, y, z_top)) for x, y in verts_2d]
bot_verts =[bm.verts.new((x, y, z_bot)) for x, y in verts_2d]
if len(verts_2d) >= 3:
bm.faces.new(top_verts)
bm.faces.new(reversed(bot_verts))
n = len(verts_2d)
for i in range(n):
next_i = (i + 1) % n
bm.faces.new((bot_verts[i], bot_verts[next_i], top_verts[next_i], top_verts[i]))
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
if plane == 'YZ':
rot_matrix = mathutils.Matrix(((0, 0, 1, 0), (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 0, 1)))
elif plane == 'ZX':
rot_matrix = mathutils.Matrix(((0, 1, 0, 0), (0, 0, 1, 0), (1, 0, 0, 0), (0, 0, 0, 1)))
else:
rot_matrix = mathutils.Matrix.Identity(4)
bmesh.ops.transform(bm, matrix=rot_matrix, verts=bm.verts)
return bm
def create_prism_object(name, verts_2d, thickness, color, plane, context):
""" 指定された2D頂点リストから押し出し立体を作成し、オブジェクトとしてシーンに配置する """
bm = bmesh.new()
generate_prism_bmesh(bm, verts_2d, thickness, plane)
mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(name, mesh)
if context.collection: context.collection.objects.link(obj)
else: context.scene.collection.objects.link(obj)
mat = create_unique_material(color, f"Mat_{name}")
obj.data.materials.append(mat)
for poly in mesh.polygons: poly.use_smooth = True
apply_auto_smooth(mesh)
return obj
# ==============================================================================
# マテリアル作成ロジック
# ==============================================================================
def create_unique_material(color, name_prefix="Mat_UniqueShape"):
timestamp = datetime.now().strftime('%M%S%f')[:5]
mat_name = f"{name_prefix}_{timestamp}"
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
if mat.use_nodes:
tree = mat.node_tree
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (0, 0)
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (300, 0)
tree.links.new(bsdf.outputs[0], out.inputs[0])
if "Base Color" in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = color[3]
cleanup_old_materials(name_prefix)
return mat
def get_or_create_preview_material(mat_name):
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(name=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 generate_shape_bmesh(bm, props):
size_x = min(max(props.size_x, 0.01), 10000.0)
size_y = min(max(props.size_y, 0.01), 10000.0)
minor_radius = min(max(props.minor_radius, 0.001), 5000.0)
if props.base_shape == 'CUBE':
create_cube_framework_bmesh(bm, size_x, minor_radius, props.minor_segments)
elif props.base_shape == 'SQUARE':
create_square_torus_bmesh(bm, size_x, props.corner_radius, minor_radius, props.corner_segments, props.minor_segments)
elif props.base_shape == 'CIRCLE':
create_ellipse_torus_bmesh(bm, size_x, size_x, minor_radius, props.major_segments, props.minor_segments)
elif props.base_shape == 'ELLIPSE':
create_ellipse_torus_bmesh(bm, size_x, size_y, minor_radius, props.major_segments, props.minor_segments)
def generate_guide_bmesh(bm_g, props):
if props.base_shape == 'CUBE':
create_cube_guide_bmesh(bm_g, props.size_x)
elif props.base_shape == 'SQUARE':
create_square_guide_bmesh(bm_g, props.size_x)
elif props.base_shape == 'CIRCLE':
create_ellipse_guide_bmesh(bm_g, props.size_x, props.size_x, segments=props.major_segments)
elif props.base_shape == 'ELLIPSE':
create_ellipse_guide_bmesh(bm_g, props.size_x, props.size_y, segments=props.major_segments)
def update_pie_preview_geometry(context, col):
pie_props = getattr(context.scene, PIE_PROPS_NAME, None)
if not pie_props: return
obj_tri = bpy.data.objects.get(PREVIEW_PIE_TRI_NAME)
obj_bow = bpy.data.objects.get(PREVIEW_PIE_BOW_NAME)
obj_rest = bpy.data.objects.get(PREVIEW_PIE_REST_NAME)
if not pie_props.show_preview:
for obj in[obj_tri, obj_bow, obj_rest]:
if obj: bpy.data.objects.remove(obj, do_unlink=True)
return
start_rad = math.radians(pie_props.pie_start_angle)
end_rad = math.radians(pie_props.pie_end_angle)
while end_rad <= start_rad:
end_rad += math.pi * 2
radius = pie_props.pie_radius
thickness = pie_props.pie_thickness
segments = pie_props.pie_segments
plane = pie_props.pie_plane
arc_angle = end_rad - start_rad
rest_angle = (math.pi * 2) - arc_angle
arc_steps = max(3, int(segments * (arc_angle / (math.pi * 2))))
rest_steps = max(3, int(segments * (rest_angle / (math.pi * 2))))
p1 = (radius * math.cos(start_rad), radius * math.sin(start_rad))
p2 = (radius * math.cos(end_rad), radius * math.sin(end_rad))
tri_verts = [(0.0, 0.0), p1, p2]
bow_verts =[p1]
for i in range(1, arc_steps):
t = start_rad + arc_angle * (i / arc_steps)
bow_verts.append((radius * math.cos(t), radius * math.sin(t)))
bow_verts.append(p2)
rest_verts = [(0.0, 0.0), p2]
for i in range(1, rest_steps):
t = end_rad + rest_angle * (i / rest_steps)
rest_verts.append((radius * math.cos(t), radius * math.sin(t)))
rest_verts.append(p1)
parts_data =[
(PREVIEW_PIE_TRI_NAME, tri_verts, pie_props.color_tri, obj_tri),
(PREVIEW_PIE_BOW_NAME, bow_verts, pie_props.color_bow, obj_bow),
(PREVIEW_PIE_REST_NAME, rest_verts, pie_props.color_rest, obj_rest),
]
for name, verts, color, obj in parts_data:
bm = bmesh.new()
try:
generate_prism_bmesh(bm, verts, thickness, plane)
mesh_name = f"Mesh_{name}_{context.scene.name}"
mesh = bpy.data.meshes.get(mesh_name)
if not mesh: mesh = bpy.data.meshes.new(mesh_name)
else: mesh.clear_geometry()
bm.to_mesh(mesh)
apply_auto_smooth(mesh)
mesh.update(calc_edges=True)
finally:
bm.free()
if not obj:
obj = bpy.data.objects.new(name, mesh)
col.objects.link(obj)
elif obj.data != mesh:
obj.data = mesh
mat = get_or_create_preview_material(f"Mat_{name}")
update_preview_material(mat, color)
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
def update_preview_geometry(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_NAME)
if col.name not in context.scene.collection.children:
context.scene.collection.children.link(col)
# 1. Update Torus Generator Preview
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)
else:
final_matrix = get_transform_matrix(props)
scene_mesh_name = f"PreviewMesh_{PREFIX}_{context.scene.name}"
bm = bmesh.new()
try:
generate_shape_bmesh(bm, props)
bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
mesh = bpy.data.meshes.get(scene_mesh_name)
if not mesh: mesh = bpy.data.meshes.new(scene_mesh_name)
else: mesh.clear_geometry()
bm.to_mesh(mesh)
apply_auto_smooth(mesh)
mesh.update(calc_edges=True)
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(PREVIEW_MAT_NAME)
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_guide:
bm_g = bmesh.new()
try:
generate_guide_bmesh(bm_g, props)
bmesh.ops.transform(bm_g, matrix=final_matrix, verts=bm_g.verts)
guide_mesh_name = scene_mesh_name + "_Guide"
mesh_g = bpy.data.meshes.get(guide_mesh_name)
if not mesh_g: mesh_g = bpy.data.meshes.new(guide_mesh_name)
else: mesh_g.clear_geometry()
bm_g.to_mesh(mesh_g)
mesh_g.update(calc_edges=True)
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)
# 2. Update Pie Generator Preview
update_pie_preview_geometry(context, col)
_timer = None
_last_update_time = 0
def delayed_update():
global _timer, _last_update_time
_timer = None
now = time.time()
if now - _last_update_time < 0.05:
if _timer is None: _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
return None
_last_update_time = now
ctx = bpy.context
if not ctx or not ctx.scene: return None
if ctx.object and ctx.object.mode != 'OBJECT': return None
update_preview_geometry(ctx)
return None
def on_update(self, context):
global _timer
if _timer is None: _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_TorusProps(PropertyGroup):
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
show_guide: BoolProperty(name="Show Guide", default=CURRENT_DEFAULTS['show_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)
base_shape: EnumProperty(
name="Shape",
items=[('CUBE', "Cube (3D)", ""), ('SQUARE', "Square", ""), ('CIRCLE', "Circle", ""), ('ELLIPSE', "Ellipse", "")],
default=CURRENT_DEFAULTS['base_shape'], 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)
size_x: FloatProperty(name="Size", default=CURRENT_DEFAULTS['size_x'], min=0.1, max=10000.0, update=on_update)
size_y: FloatProperty(name="Size Y", default=CURRENT_DEFAULTS['size_y'], min=0.1, max=10000.0, update=on_update)
corner_radius: FloatProperty(name="Corner Radius", default=CURRENT_DEFAULTS['corner_radius'], min=0.0, max=5000.0, update=on_update)
minor_radius: FloatProperty(name="Tube Thickness", default=CURRENT_DEFAULTS['minor_radius'], min=0.01, max=5000.0, update=on_update)
major_segments: IntProperty(name="Resolution", default=CURRENT_DEFAULTS['major_segments'], min=3, soft_max=128, update=on_update)
corner_segments: IntProperty(name="Corner Segs", default=CURRENT_DEFAULTS['corner_segments'], min=1, soft_max=128, update=on_update)
minor_segments: IntProperty(name="Tube Segs", default=CURRENT_DEFAULTS['minor_segments'], min=3, soft_max=128, update=on_update)
class PG_PieProps(PropertyGroup):
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['pie_show_preview'], update=on_update)
pie_plane: EnumProperty(
name="Plane",
items=[('XY', "XY Plane", ""), ('YZ', "YZ Plane", ""), ('ZX', "ZX Plane", "")],
default=CURRENT_DEFAULTS['pie_plane'], update=on_update
)
pie_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['pie_radius'], min=0.01, soft_max=1000.0, update=on_update)
pie_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['pie_thickness'], min=0.0, update=on_update)
pie_start_angle: FloatProperty(name="Start Angle", default=CURRENT_DEFAULTS['pie_start_angle'], update=on_update)
pie_end_angle: FloatProperty(name="End Angle", default=CURRENT_DEFAULTS['pie_end_angle'], update=on_update)
color_tri: FloatVectorProperty(name="Triangle Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_tri'], update=on_update)
color_bow: FloatVectorProperty(name="Bow Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_bow'], update=on_update)
color_rest: FloatVectorProperty(name="Rest Circle Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_rest'], update=on_update)
pie_segments: IntProperty(name="Resolution", default=CURRENT_DEFAULTS['pie_segments'], min=8, max=512, update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CreateTorus(Operator):
bl_idname = f"{OP_PREFIX}.create_torus"
bl_label = "Create Shape Torus"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
bm = bmesh.new()
generate_shape_bmesh(bm, props)
final_matrix = get_transform_matrix(props)
bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
mesh = bpy.data.meshes.new(f"Shape_Mesh")
bm.to_mesh(mesh)
bm.free()
apply_auto_smooth(mesh)
name_dict = {'CUBE': "CubeFrame", 'SQUARE': "SqTorus", 'CIRCLE': "CircTorus", 'ELLIPSE': "ElpsTorus"}
prefix_name = name_dict.get(props.base_shape, "Shape")
obj = bpy.data.objects.new(f"{prefix_name}_{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_Unique")
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'}, f"Created {prefix_name} Successfully!")
return {'FINISHED'}
class OT_CreatePieShape(Operator):
bl_idname = f"{OP_PREFIX}.create_pie_shape"
bl_label = "Create Pie Shapes"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PIE_PROPS_NAME, None)
if not props: return {'CANCELLED'}
start_rad = math.radians(props.pie_start_angle)
end_rad = math.radians(props.pie_end_angle)
while end_rad <= start_rad:
end_rad += math.pi * 2
radius = props.pie_radius
thickness = props.pie_thickness
segments = props.pie_segments
plane = props.pie_plane
arc_angle = end_rad - start_rad
rest_angle = (math.pi * 2) - arc_angle
arc_steps = max(3, int(segments * (arc_angle / (math.pi * 2))))
rest_steps = max(3, int(segments * (rest_angle / (math.pi * 2))))
p1 = (radius * math.cos(start_rad), radius * math.sin(start_rad))
p2 = (radius * math.cos(end_rad), radius * math.sin(end_rad))
tri_verts =[(0.0, 0.0), p1, p2]
obj_tri = create_prism_object("Pie_Triangle", tri_verts, thickness, props.color_tri, plane, context)
bow_verts = [p1]
for i in range(1, arc_steps):
t = start_rad + arc_angle * (i / arc_steps)
bow_verts.append((radius * math.cos(t), radius * math.sin(t)))
bow_verts.append(p2)
obj_bow = create_prism_object("Pie_Bow", bow_verts, thickness, props.color_bow, plane, context)
rest_verts =[(0.0, 0.0), p2]
for i in range(1, rest_steps):
t = end_rad + rest_angle * (i / rest_steps)
rest_verts.append((radius * math.cos(t), radius * math.sin(t)))
rest_verts.append(p1)
obj_rest = create_prism_object("Pie_Rest", rest_verts, thickness, props.color_rest, plane, context)
bpy.ops.object.select_all(action='DESELECT')
for o in [obj_tri, obj_bow, obj_rest]:
o.select_set(True)
context.view_layer.objects.active = obj_tri
self.report({'INFO'}, "Created Pie/Triangle Shapes Successfully!")
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)
pie = getattr(context.scene, PIE_PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string(): target_text = t; break
if not target_text:
self.report({'WARNING'}, "Source script not found in Text Editor.")
return {'CANCELLED'}
code = target_text.as_string()
c, l, r = props.torus_color, props.torus_loc, props.torus_rot
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "show_guide": {props.show_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' "base_shape": "{props.base_shape}",\n'
new_dict += f' "torus_plane": "{props.torus_plane}",\n'
new_dict += f' "size_x": {props.size_x:.4f},\n'
new_dict += f' "size_y": {props.size_y:.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' "major_segments": {props.major_segments},\n'
new_dict += f' "corner_segments": {props.corner_segments},\n'
new_dict += f' "minor_segments": {props.minor_segments},\n'
if pie:
new_dict += f' "pie_show_preview": {pie.show_preview},\n'
new_dict += f' "pie_plane": "{pie.pie_plane}",\n'
new_dict += f' "pie_radius": {pie.pie_radius:.4f},\n'
new_dict += f' "pie_thickness": {pie.pie_thickness:.4f},\n'
new_dict += f' "pie_start_angle": {pie.pie_start_angle:.4f},\n'
new_dict += f' "pie_end_angle": {pie.pie_end_angle:.4f},\n'
c_tri = pie.color_tri
new_dict += f' "color_tri": ({c_tri[0]:.4f}, {c_tri[1]:.4f}, {c_tri[2]:.4f}, {c_tri[3]:.4f}),\n'
c_bow = pie.color_bow
new_dict += f' "color_bow": ({c_bow[0]:.4f}, {c_bow[1]:.4f}, {c_bow[2]:.4f}, {c_bow[3]:.4f}),\n'
c_rest = pie.color_rest
new_dict += f' "color_rest": ({c_rest[0]:.4f}, {c_rest[1]:.4f}, {c_rest[2]:.4f}, {c_rest[3]:.4f}),\n'
new_dict += f' "pie_segments": {pie.pie_segments},\n'
new_dict += "}\n"
try:
tag_start = "# <BEGIN" + "_DICT>"
tag_end = "# <END" + "_DICT>"
if tag_start not in code or tag_end not in code:
self.report({'ERROR'}, "DICT tags missing! Script might be corrupted.")
return {'CANCELLED'}
pre_code, rest = code.split(tag_start, 1)
_, post_code = rest.split(tag_end, 1)
final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
if SOURCE_ID_TAG not in final_code:
self.report({'ERROR'}, "Critical Error: SOURCE_ID_TAG lost during copy.")
return {'CANCELLED'}
lines = final_code.split("\n")
if len(lines) > 0 and lines[0].startswith("# Copied:"):
lines[0] = f"# Copied: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "Code copied safely!")
except Exception as e:
self.report({'ERROR'}, f"Copy failed: {e}")
return {'CANCELLED'}
return {'FINISHED'}
class OT_Reset(Operator):
bl_idname = f"{OP_PREFIX}.reset"
bl_label = "Reset Transform"
def execute(self, context):
p = getattr(context.scene, PROPS_NAME)
p.torus_loc = (0,0,0); p.torus_rot = (0,0,0)
p.torus_plane = 'XY'; p.base_shape = 'SQUARE'
p.size_x = 10.0; p.size_y = 5.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, "base_shape")
col.prop(props, "torus_plane")
col.separator()
col.prop(props, "torus_loc")
col.prop(props, "torus_rot")
box.separator()
box.prop(props, "show_guide", icon='MESH_GRID', text="Show Guide Wire")
col_s = box.column(align=True)
if props.base_shape == 'ELLIPSE':
col_s.prop(props, "size_x", text="Size X")
col_s.prop(props, "size_y", text="Size Y")
else:
col_s.prop(props, "size_x", text="Size")
row_cr = col_s.row()
row_cr.enabled = (props.base_shape == 'SQUARE')
row_cr.prop(props, "corner_radius")
if props.corner_radius <= 0.001 and props.base_shape == 'SQUARE':
row_cr.label(text="[90° Mode]", icon='SNAP_VERTEX')
col_s.prop(props, "minor_radius")
row_seg = box.row()
if props.base_shape in['CIRCLE', 'ELLIPSE']:
row_seg.prop(props, "major_segments", text="Resolution")
elif props.base_shape == 'SQUARE':
row_seg.prop(props, "corner_segments", text="Corner Segs")
else:
row_seg.label(text="[Cube has fixed corners]")
row_s2 = box.row()
row_s2.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
icons = {'CUBE': 'MESH_CUBE', 'SQUARE': 'MESH_PLANE', 'CIRCLE': 'MESH_CIRCLE', 'ELLIPSE': 'MESH_CIRCLE'}
texts = {'CUBE': "Create Cube Frame", 'SQUARE': "Create Square Torus", 'CIRCLE': "Create Circle Torus", 'ELLIPSE': "Create Ellipse Torus"}
col_exec.operator(OT_CreateTorus.bl_idname, icon=icons.get(props.base_shape, 'MESH_TORUS'), text=texts.get(props.base_shape, "Create Torus"))
class PT_PieGeneratorPanel(Panel):
bl_label = "Pie & Triangle Generator"
bl_idname = f"{PREFIX}_PT_pie"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PIE_PROPS_NAME, None)
if not props: return
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.label(text="Base Settings:", icon='MESH_CIRCLE')
box.prop(props, "pie_plane")
box.prop(props, "pie_radius")
box.prop(props, "pie_thickness")
box.prop(props, "pie_segments")
box.separator()
box.label(text="Angles (X-Axis=0°, Y-Axis=90°):", icon='ORIENTATION_GIMBAL')
row = box.row(align=True)
row.prop(props, "pie_start_angle")
row.prop(props, "pie_end_angle")
box.separator()
box.label(text="Colors for Parts:", icon='COLOR')
box.prop(props, "color_tri", text="Triangle Color")
box.prop(props, "color_bow", text="Bow (Segment) Color")
box.prop(props, "color_rest", text="Rest Circle Color")
layout.separator()
col = layout.column()
col.scale_y = 1.5
col.operator(OT_CreatePieShape.bl_idname, icon='MESH_CIRCLE', text="Create Pie Objects")
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,
PG_PieProps,
OT_CreateTorus,
OT_CreatePieShape,
OT_CopyFullScript,
OT_Reset,
OT_OpenUrl,
OT_RemoveAddon,
PT_MainPanel,
PT_PieGeneratorPanel,
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:
try: bpy.utils.register_class(c)
except ValueError: pass
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_TorusProps))
setattr(bpy.types.Scene, PIE_PROPS_NAME, PointerProperty(type=PG_PieProps))
bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)
def unregister():
global _timer
if _timer is not None:
try: bpy.app.timers.unregister(_timer)
except Exception: pass
_timer = None
cleanup_preview_data()
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
if hasattr(bpy.types.Scene, PIE_PROPS_NAME): delattr(bpy.types.Scene, PIE_PROPS_NAME)
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except ValueError: pass
if __name__ == "__main__":
register()
# Copied: 2026-03-27 12:28:00
import bpy
import bmesh
import webbrowser
import math
import mathutils
import time
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime
# ==============================================================================
# 【 基本設定エリア 】
# ==============================================================================
PREFIX = "ShapeTorus20260324"
ADDON_NAME = "zionad 520[ Shape-Torus ]"
TAB_NAME = "[ Shape Torus copy ] "
PANEL_TITLE = "Multi-Shape Generator"
AUTHOR = "zionadchat"
# ★ このスクリプト自身のID
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SHAPE_TORUS_2026_03_24_V9_MULTI_SHAPE ###"
# ==============================================================================
# システム初期化 & ID管理
# ==============================================================================
bl_info = {
"name": f"{ADDON_NAME} {PREFIX}",
"author": AUTHOR,
"version": (9, 1, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": f"Multi-Shape Torus & Pie Generator - {PREFIX}",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
PIE_PROPS_NAME = f"{PREFIX}_pie_props"
ADDON_LINKS = (
{"label": "Prefix トーラス正方形 20260324", "url": "<https://www.notion.so/Prefix-20260324-32df5dacaf4380528980db6a989d6306>"},
)
# ==============================================================================
# デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"show_preview": True,
"show_guide": True,
"torus_color": (0.0391, 0.8000, 0.1647, 1.0000),
"torus_loc": (0.0000, 0.0000, 0.0000),
"torus_rot": (0.0000, 0.0000, 0.0000),
"base_shape": "SQUARE",
"torus_plane": "XY",
"size_x": 10.0000,
"size_y": 5.0000,
"corner_radius": 0.0000,
"minor_radius": 0.5000,
"major_segments": 32,
"corner_segments": 8,
"minor_segments": 16,
"pie_plane": "XY",
"pie_radius": 100.0000,
"pie_thickness": 10.0000,
"pie_start_angle": 45.0000,
"pie_end_angle": 135.0000,
"color_tri": (1.0000, 0.2000, 0.2000, 1.0000),
"color_bow": (0.2000, 0.5000, 1.0000, 1.0000),
"color_rest": (0.8000, 0.8000, 0.8000, 1.0000),
"pie_segments": 64,
}
# <END_DICT>
# ==============================================================================
# データ クリーンアップ管理
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_OBJ_NAME = f"[Preview] Shape_{PREFIX}"
PREVIEW_GUIDE_NAME = f"[Preview] Guide_{PREFIX}"
PREVIEW_MAT_NAME = f"PreviewMat_{PREFIX}"
def cleanup_preview_data():
for name in [PREVIEW_OBJ_NAME, PREVIEW_GUIDE_NAME]:
obj = bpy.data.objects.get(name)
if obj:
mesh = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if mesh and mesh.users == 0:
bpy.data.meshes.remove(mesh)
meshes_to_remove =[m for m in bpy.data.meshes if m.name.startswith(f"PreviewMesh_{PREFIX}")]
for m in meshes_to_remove:
if m.users == 0:
bpy.data.meshes.remove(m)
mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
if mat and mat.users == 0:
bpy.data.materials.remove(mat)
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if col and len(col.objects) == 0:
bpy.data.collections.remove(col)
def cleanup_old_materials(prefix="Mat_UniqueShape", limit=50):
mats = [m for m in bpy.data.materials if m.name.startswith(prefix)]
if len(mats) > limit:
for m in mats[:-limit]:
if m.users == 0:
bpy.data.materials.remove(m)
# ==============================================================================
# ガイド & メッシュ生成エンジン (Torus)
# ==============================================================================
def create_square_guide_bmesh(bm, size):
S = 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_cube_guide_bmesh(bm, size):
geom = bmesh.ops.create_cube(bm, size=size)
faces = [f for f in bm.faces]
bmesh.ops.delete(bm, geom=faces, context='FACES_ONLY')
return bm
def create_ellipse_guide_bmesh(bm, size_x, size_y, segments=64):
a = size_x / 2.0
b = size_y / 2.0
verts =[]
for i in range(segments):
t = i * 2.0 * math.pi / segments
verts.append(bm.verts.new((a * math.cos(t), b * math.sin(t), 0)))
bm.verts.ensure_lookup_table()
for i in range(segments):
bm.edges.new((verts[i], verts[(i + 1) % segments]))
return bm
def create_ellipse_torus_bmesh(bm, size_x, size_y, minor_radius, major_segments, minor_segments):
a = size_x / 2.0
b = size_y / 2.0
rings =[]
for i in range(major_segments):
t = i * 2.0 * math.pi / major_segments
x = a * math.cos(t)
y = b * math.sin(t)
p = mathutils.Vector((x, y, 0))
nx = b * math.cos(t)
ny = a * math.sin(t)
n = mathutils.Vector((nx, ny, 0)).normalized()
up = 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)) + up * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
bm.verts.ensure_lookup_table()
edge_loops = []
for ring in rings:
edges =[]
for j in range(minor_segments):
v1 = ring[j]
v2 = ring[(j + 1) % minor_segments]
edges.append(bm.edges.new((v1, v2)))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(major_segments):
next_i = (i + 1) % major_segments
try:
bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[next_i])
except Exception:
pass
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
for f in bm.faces: f.smooth = True
if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
def create_square_torus_bmesh(bm, size, corner_radius, minor_radius, corner_segments, minor_segments):
half_size = size / 2.0
actual_corner_radius = min(max(corner_radius, 0.0), half_size)
rings =[]
EPS = 1e-6
if actual_corner_radius < EPS:
L = half_size
corners =[
(mathutils.Vector((L, L, 0)), mathutils.Vector((1, 1, 0)).normalized()),
(mathutils.Vector((-L, L, 0)), mathutils.Vector((-1, 1, 0)).normalized()),
(mathutils.Vector((-L, -L, 0)), mathutils.Vector((-1, -1, 0)).normalized()),
(mathutils.Vector((L, -L, 0)), mathutils.Vector((1, -1, 0)).normalized())
]
scale_xy = 1.0 / math.cos(math.pi / 4)
for p, n in corners:
b = mathutils.Vector((0, 0, 1))
ring =[]
for j in range(minor_segments):
theta = j * 2.0 * math.pi / minor_segments
offset = n * (minor_radius * math.cos(theta) * scale_xy) + b * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
else:
L = half_size - actual_corner_radius
pts =[]
for q in range(4):
cx = L if q in [0, 3] else -L
cy = L if q in[0, 1] else -L
for i in range(corner_segments + 1):
angle = q * (math.pi / 2) + i * (math.pi / 2) / corner_segments
x = cx + actual_corner_radius * math.cos(angle)
y = cy + actual_corner_radius * math.sin(angle)
pts.append((mathutils.Vector((x, y, 0)), mathutils.Vector((math.cos(angle), math.sin(angle), 0))))
unique_pts =[]
for p, n in pts:
if not unique_pts or (unique_pts[-1][0] - p).length > EPS:
unique_pts.append((p, n))
if len(unique_pts) > 1 and (unique_pts[-1][0] - unique_pts[0][0]).length < EPS:
unique_pts.pop()
for p, n in unique_pts:
b = mathutils.Vector((0, 0, 1))
ring =[]
for j in range(minor_segments):
theta = j * 2.0 * math.pi / minor_segments
offset = n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))
ring.append(bm.verts.new(p + offset))
rings.append(ring)
bm.verts.ensure_lookup_table()
total_rings = len(rings)
if total_rings < 3: return bm
edge_loops =[]
for ring in rings:
edges =[]
for j in range(minor_segments):
v1 = ring[j]
v2 = ring[(j + 1) % minor_segments]
edges.append(bm.edges.new((v1, v2)))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(total_rings):
next_i = (i + 1) % total_rings
try:
bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[next_i])
except Exception:
pass
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
for f in bm.faces: f.smooth = True
if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
def create_cube_framework_bmesh(bm, size, minor_radius, minor_segments):
L = size / 2.0
verts_co =[
mathutils.Vector(( L, L, L)), mathutils.Vector((-L, L, L)),
mathutils.Vector((-L, -L, L)), mathutils.Vector(( L, -L, L)),
mathutils.Vector(( L, L, -L)), mathutils.Vector((-L, L, -L)),
mathutils.Vector((-L, -L, -L)), mathutils.Vector(( L, -L, -L)),
]
edges_idx =[
(0,1), (1,2), (2,3), (3,0),
(4,5), (5,6), (6,7), (7,4),
(0,4), (1,5), (2,6), (3,7)
]
for co in verts_co:
geom = bmesh.ops.create_uvsphere(bm, u_segments=minor_segments, v_segments=max(minor_segments//2, 3), radius=minor_radius)
bmesh.ops.translate(bm, verts=geom['verts'], vec=co)
for idx1, idx2 in edges_idx:
v1 = verts_co[idx1]
v2 = verts_co[idx2]
dist = (v1 - v2).length
center = (v1 + v2) / 2.0
geom = bmesh.ops.create_cone(
bm, cap_ends=False, cap_tris=False, segments=minor_segments,
radius1=minor_radius, radius2=minor_radius, depth=dist
)
axis = (v1 - v2).normalized()
rot = mathutils.Vector((0,0,1)).rotation_difference(axis)
bmesh.ops.transform(bm, matrix=rot.to_matrix().to_4x4(), verts=geom['verts'])
bmesh.ops.translate(bm, verts=geom['verts'], vec=center)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
for f in bm.faces: f.smooth = True
if bm.faces: bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
def apply_auto_smooth(mesh):
if bpy.app.version < (4, 1, 0):
try:
if hasattr(mesh, "use_auto_smooth"):
mesh.use_auto_smooth = True
mesh.auto_smooth_angle = math.radians(30)
except AttributeError:
pass
# ==============================================================================
# Pie / Sector / Triangle ジェネレーター (NEW)
# ==============================================================================
def create_prism_object(name, verts_2d, thickness, color, plane, context):
""" 指定された2D頂点リストから押し出し立体を作成し、平面回転を適用してオブジェクトを生成する """
bm = bmesh.new()
z_top = thickness / 2.0
z_bot = -thickness / 2.0
# 頂点の追加
top_verts =[bm.verts.new((x, y, z_top)) for x, y in verts_2d]
bot_verts =[bm.verts.new((x, y, z_bot)) for x, y in verts_2d]
# 面の作成
if len(verts_2d) >= 3:
bm.faces.new(top_verts) # 上面
bm.faces.new(reversed(bot_verts)) # 下面
n = len(verts_2d)
for i in range(n): # 側面
next_i = (i + 1) % n
bm.faces.new((bot_verts[i], bot_verts[next_i], top_verts[next_i], top_verts[i]))
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
# 平面に応じた回転
# X=0度, Y=90度を基準として、各平面にマップ
if plane == 'YZ':
rot_matrix = mathutils.Matrix((
(0, 0, 1, 0),
(1, 0, 0, 0),
(0, 1, 0, 0),
(0, 0, 0, 1)
))
elif plane == 'ZX':
rot_matrix = mathutils.Matrix((
(0, 1, 0, 0),
(0, 0, 1, 0),
(1, 0, 0, 0),
(0, 0, 0, 1)
))
else: # XY Plane
rot_matrix = mathutils.Matrix.Identity(4)
bmesh.ops.transform(bm, matrix=rot_matrix, verts=bm.verts)
# メッシュ・オブジェクト化
mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(name, mesh)
if context.collection:
context.collection.objects.link(obj)
else:
context.scene.collection.objects.link(obj)
# マテリアル設定
mat = create_unique_material(color, f"Mat_{name}")
obj.data.materials.append(mat)
for poly in mesh.polygons:
poly.use_smooth = True
apply_auto_smooth(mesh)
return obj
# ==============================================================================
# マテリアル作成ロジック
# ==============================================================================
def create_unique_material(color, name_prefix="Mat_UniqueShape"):
timestamp = datetime.now().strftime('%M%S%f')[:5]
mat_name = f"{name_prefix}_{timestamp}"
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
if mat.use_nodes:
tree = mat.node_tree
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (0, 0)
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (300, 0)
tree.links.new(bsdf.outputs[0], out.inputs[0])
if "Base Color" in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = color[3]
cleanup_old_materials(name_prefix)
return mat
def get_or_create_preview_material():
mat = bpy.data.materials.get(PREVIEW_MAT_NAME)
if not mat:
mat = bpy.data.materials.new(name=PREVIEW_MAT_NAME)
mat.use_nodes = True
mat.blend_method = 'BLEND'
return mat
def update_preview_material(mat, color):
if mat.use_nodes:
bsdf = None
for node in mat.node_tree.nodes:
if node.type == 'BSDF_PRINCIPLED':
bsdf = node; break
if not bsdf:
mat.node_tree.nodes.clear()
bsdf = mat.node_tree.nodes.new("ShaderNodeBsdfPrincipled")
out = mat.node_tree.nodes.new("ShaderNodeOutputMaterial")
mat.node_tree.links.new(bsdf.outputs[0], out.inputs[0])
if "Base Color" in bsdf.inputs: bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs: bsdf.inputs["Alpha"].default_value = color[3]
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
def get_transform_matrix(props):
rot_matrix = mathutils.Matrix.Identity(4)
if props.torus_plane == 'YZ':
rot_matrix = mathutils.Matrix.Rotation(math.radians(90.0), 4, 'Y')
elif props.torus_plane == 'ZX':
rot_matrix = mathutils.Matrix.Rotation(math.radians(-90.0), 4, 'X')
user_rot = mathutils.Euler((
math.radians(props.torus_rot[0]),
math.radians(props.torus_rot[1]),
math.radians(props.torus_rot[2])
), 'XYZ').to_matrix().to_4x4()
loc_matrix = mathutils.Matrix.Translation(mathutils.Vector(props.torus_loc))
return loc_matrix @ user_rot @ rot_matrix
def generate_shape_bmesh(bm, props):
size_x = min(max(props.size_x, 0.01), 10000.0)
size_y = min(max(props.size_y, 0.01), 10000.0)
minor_radius = min(max(props.minor_radius, 0.001), 5000.0)
if props.base_shape == 'CUBE':
create_cube_framework_bmesh(bm, size_x, minor_radius, props.minor_segments)
elif props.base_shape == 'SQUARE':
create_square_torus_bmesh(bm, size_x, props.corner_radius, minor_radius, props.corner_segments, props.minor_segments)
elif props.base_shape == 'CIRCLE':
create_ellipse_torus_bmesh(bm, size_x, size_x, minor_radius, props.major_segments, props.minor_segments)
elif props.base_shape == 'ELLIPSE':
create_ellipse_torus_bmesh(bm, size_x, size_y, minor_radius, props.major_segments, props.minor_segments)
def generate_guide_bmesh(bm_g, props):
if props.base_shape == 'CUBE':
create_cube_guide_bmesh(bm_g, props.size_x)
elif props.base_shape == 'SQUARE':
create_square_guide_bmesh(bm_g, props.size_x)
elif props.base_shape == 'CIRCLE':
create_ellipse_guide_bmesh(bm_g, props.size_x, props.size_x, segments=props.major_segments)
elif props.base_shape == 'ELLIPSE':
create_ellipse_guide_bmesh(bm_g, props.size_x, props.size_y, segments=props.major_segments)
def update_preview_geometry(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_NAME)
if col.name not in context.scene.collection.children:
context.scene.collection.children.link(col)
obj = bpy.data.objects.get(PREVIEW_OBJ_NAME)
guide_obj = bpy.data.objects.get(PREVIEW_GUIDE_NAME)
if not props.show_preview:
if obj: bpy.data.objects.remove(obj, do_unlink=True)
if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)
return
final_matrix = get_transform_matrix(props)
scene_mesh_name = f"PreviewMesh_{PREFIX}_{context.scene.name}"
bm = bmesh.new()
try:
generate_shape_bmesh(bm, props)
bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
mesh = bpy.data.meshes.get(scene_mesh_name)
if not mesh: mesh = bpy.data.meshes.new(scene_mesh_name)
else: mesh.clear_geometry()
bm.to_mesh(mesh)
apply_auto_smooth(mesh)
mesh.update(calc_edges=True)
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_guide:
bm_g = bmesh.new()
try:
generate_guide_bmesh(bm_g, props)
bmesh.ops.transform(bm_g, matrix=final_matrix, verts=bm_g.verts)
guide_mesh_name = scene_mesh_name + "_Guide"
mesh_g = bpy.data.meshes.get(guide_mesh_name)
if not mesh_g: mesh_g = bpy.data.meshes.new(guide_mesh_name)
else: mesh_g.clear_geometry()
bm_g.to_mesh(mesh_g)
mesh_g.update(calc_edges=True)
finally:
bm_g.free()
if not guide_obj:
guide_obj = bpy.data.objects.new(PREVIEW_GUIDE_NAME, mesh_g)
col.objects.link(guide_obj)
elif guide_obj.data != mesh_g:
guide_obj.data = mesh_g
guide_obj.display_type = 'WIRE'
guide_obj.show_in_front = True
else:
if guide_obj: bpy.data.objects.remove(guide_obj, do_unlink=True)
_timer = None
_last_update_time = 0
def delayed_update():
global _timer, _last_update_time
_timer = None
now = time.time()
if now - _last_update_time < 0.05:
if _timer is None: _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
return None
_last_update_time = now
ctx = bpy.context
if not ctx or not ctx.scene: return None
if ctx.object and ctx.object.mode != 'OBJECT': return None
update_preview_geometry(ctx)
return None
def on_update(self, context):
global _timer
if _timer is None: _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_TorusProps(PropertyGroup):
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
show_guide: BoolProperty(name="Show Guide", default=CURRENT_DEFAULTS['show_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)
base_shape: EnumProperty(
name="Shape",
items=[('CUBE', "Cube (3D)", ""), ('SQUARE', "Square", ""), ('CIRCLE', "Circle", ""), ('ELLIPSE', "Ellipse", "")],
default=CURRENT_DEFAULTS['base_shape'], 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)
size_x: FloatProperty(name="Size", default=CURRENT_DEFAULTS['size_x'], min=0.1, max=10000.0, update=on_update)
size_y: FloatProperty(name="Size Y", default=CURRENT_DEFAULTS['size_y'], min=0.1, max=10000.0, update=on_update)
corner_radius: FloatProperty(name="Corner Radius", default=CURRENT_DEFAULTS['corner_radius'], min=0.0, max=5000.0, update=on_update)
minor_radius: FloatProperty(name="Tube Thickness", default=CURRENT_DEFAULTS['minor_radius'], min=0.01, max=5000.0, update=on_update)
major_segments: IntProperty(name="Resolution", default=CURRENT_DEFAULTS['major_segments'], min=3, soft_max=128, update=on_update)
corner_segments: IntProperty(name="Corner Segs", default=CURRENT_DEFAULTS['corner_segments'], min=1, soft_max=128, update=on_update)
minor_segments: IntProperty(name="Tube Segs", default=CURRENT_DEFAULTS['minor_segments'], min=3, soft_max=128, update=on_update)
class PG_PieProps(PropertyGroup):
pie_plane: EnumProperty(
name="Plane",
items=[('XY', "XY Plane", ""), ('YZ', "YZ Plane", ""), ('ZX', "ZX Plane", "")],
default=CURRENT_DEFAULTS['pie_plane']
)
pie_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['pie_radius'], min=0.01, soft_max=1000.0)
pie_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['pie_thickness'], min=0.0)
pie_start_angle: FloatProperty(name="Start Angle", default=CURRENT_DEFAULTS['pie_start_angle'])
pie_end_angle: FloatProperty(name="End Angle", default=CURRENT_DEFAULTS['pie_end_angle'])
color_tri: FloatVectorProperty(name="Triangle Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_tri'])
color_bow: FloatVectorProperty(name="Bow Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_bow'])
color_rest: FloatVectorProperty(name="Rest Circle Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_rest'])
pie_segments: IntProperty(name="Resolution", default=CURRENT_DEFAULTS['pie_segments'], min=8, max=512)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CreateTorus(Operator):
bl_idname = f"{OP_PREFIX}.create_torus"
bl_label = "Create Shape Torus"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
bm = bmesh.new()
generate_shape_bmesh(bm, props)
final_matrix = get_transform_matrix(props)
bmesh.ops.transform(bm, matrix=final_matrix, verts=bm.verts)
mesh = bpy.data.meshes.new(f"Shape_Mesh")
bm.to_mesh(mesh)
bm.free()
apply_auto_smooth(mesh)
name_dict = {'CUBE': "CubeFrame", 'SQUARE': "SqTorus", 'CIRCLE': "CircTorus", 'ELLIPSE': "ElpsTorus"}
prefix_name = name_dict.get(props.base_shape, "Shape")
obj = bpy.data.objects.new(f"{prefix_name}_{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_Unique")
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'}, f"Created {prefix_name} Successfully!")
return {'FINISHED'}
class OT_CreatePieShape(Operator):
bl_idname = f"{OP_PREFIX}.create_pie_shape"
bl_label = "Create Pie Shapes"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PIE_PROPS_NAME, None)
if not props: return {'CANCELLED'}
start_rad = math.radians(props.pie_start_angle)
end_rad = math.radians(props.pie_end_angle)
while end_rad <= start_rad:
end_rad += math.pi * 2
radius = props.pie_radius
thickness = props.pie_thickness
segments = props.pie_segments
plane = props.pie_plane
arc_angle = end_rad - start_rad
rest_angle = (math.pi * 2) - arc_angle
arc_steps = max(3, int(segments * (arc_angle / (math.pi * 2))))
rest_steps = max(3, int(segments * (rest_angle / (math.pi * 2))))
p1 = (radius * math.cos(start_rad), radius * math.sin(start_rad))
p2 = (radius * math.cos(end_rad), radius * math.sin(end_rad))
# 1. 逆三角形を作成
tri_verts =[(0.0, 0.0), p1, p2]
obj_tri = create_prism_object("Pie_Triangle", tri_verts, thickness, props.color_tri, plane, context)
# 2. 弓形を作成
bow_verts = [p1]
for i in range(1, arc_steps):
t = start_rad + arc_angle * (i / arc_steps)
bow_verts.append((radius * math.cos(t), radius * math.sin(t)))
bow_verts.append(p2)
obj_bow = create_prism_object("Pie_Bow", bow_verts, thickness, props.color_bow, plane, context)
# 3. 残りの円を作成
rest_verts =[(0.0, 0.0), p2]
for i in range(1, rest_steps):
t = end_rad + rest_angle * (i / rest_steps)
rest_verts.append((radius * math.cos(t), radius * math.sin(t)))
rest_verts.append(p1)
obj_rest = create_prism_object("Pie_Rest", rest_verts, thickness, props.color_rest, plane, context)
# すべてを選択状態にする
bpy.ops.object.select_all(action='DESELECT')
for o in[obj_tri, obj_bow, obj_rest]:
o.select_set(True)
context.view_layer.objects.active = obj_tri
self.report({'INFO'}, "Created Pie/Triangle Shapes Successfully!")
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)
pie = getattr(context.scene, PIE_PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string(): target_text = t; break
if not target_text:
self.report({'WARNING'}, "Source script not found in Text Editor.")
return {'CANCELLED'}
code = target_text.as_string()
c, l, r = props.torus_color, props.torus_loc, props.torus_rot
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "show_guide": {props.show_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' "base_shape": "{props.base_shape}",\n'
new_dict += f' "torus_plane": "{props.torus_plane}",\n'
new_dict += f' "size_x": {props.size_x:.4f},\n'
new_dict += f' "size_y": {props.size_y:.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' "major_segments": {props.major_segments},\n'
new_dict += f' "corner_segments": {props.corner_segments},\n'
new_dict += f' "minor_segments": {props.minor_segments},\n'
if pie:
new_dict += f' "pie_plane": "{pie.pie_plane}",\n'
new_dict += f' "pie_radius": {pie.pie_radius:.4f},\n'
new_dict += f' "pie_thickness": {pie.pie_thickness:.4f},\n'
new_dict += f' "pie_start_angle": {pie.pie_start_angle:.4f},\n'
new_dict += f' "pie_end_angle": {pie.pie_end_angle:.4f},\n'
c_tri = pie.color_tri
new_dict += f' "color_tri": ({c_tri[0]:.4f}, {c_tri[1]:.4f}, {c_tri[2]:.4f}, {c_tri[3]:.4f}),\n'
c_bow = pie.color_bow
new_dict += f' "color_bow": ({c_bow[0]:.4f}, {c_bow[1]:.4f}, {c_bow[2]:.4f}, {c_bow[3]:.4f}),\n'
c_rest = pie.color_rest
new_dict += f' "color_rest": ({c_rest[0]:.4f}, {c_rest[1]:.4f}, {c_rest[2]:.4f}, {c_rest[3]:.4f}),\n'
new_dict += f' "pie_segments": {pie.pie_segments},\n'
new_dict += "}\n"
try:
tag_start = "# <BEGIN" + "_DICT>"
tag_end = "# <END" + "_DICT>"
if tag_start not in code or tag_end not in code:
self.report({'ERROR'}, "DICT tags missing! Script might be corrupted.")
return {'CANCELLED'}
pre_code, rest = code.split(tag_start, 1)
_, post_code = rest.split(tag_end, 1)
final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
if SOURCE_ID_TAG not in final_code:
self.report({'ERROR'}, "Critical Error: SOURCE_ID_TAG lost during copy.")
return {'CANCELLED'}
lines = final_code.split("\n")
if len(lines) > 0 and lines[0].startswith("# Copied:"):
lines[0] = f"# Copied: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "Code copied safely!")
except Exception as e:
self.report({'ERROR'}, f"Copy failed: {e}")
return {'CANCELLED'}
return {'FINISHED'}
class OT_Reset(Operator):
bl_idname = f"{OP_PREFIX}.reset"
bl_label = "Reset Transform"
def execute(self, context):
p = getattr(context.scene, PROPS_NAME)
p.torus_loc = (0,0,0); p.torus_rot = (0,0,0)
p.torus_plane = 'XY'; p.base_shape = 'SQUARE'
p.size_x = 10.0; p.size_y = 5.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, "base_shape")
col.prop(props, "torus_plane")
col.separator()
col.prop(props, "torus_loc")
col.prop(props, "torus_rot")
box.separator()
box.prop(props, "show_guide", icon='MESH_GRID', text="Show Guide Wire")
col_s = box.column(align=True)
if props.base_shape == 'ELLIPSE':
col_s.prop(props, "size_x", text="Size X")
col_s.prop(props, "size_y", text="Size Y")
else:
col_s.prop(props, "size_x", text="Size")
row_cr = col_s.row()
row_cr.enabled = (props.base_shape == 'SQUARE')
row_cr.prop(props, "corner_radius")
if props.corner_radius <= 0.001 and props.base_shape == 'SQUARE':
row_cr.label(text="[90° Mode]", icon='SNAP_VERTEX')
col_s.prop(props, "minor_radius")
row_seg = box.row()
if props.base_shape in['CIRCLE', 'ELLIPSE']:
row_seg.prop(props, "major_segments", text="Resolution")
elif props.base_shape == 'SQUARE':
row_seg.prop(props, "corner_segments", text="Corner Segs")
else:
row_seg.label(text="[Cube has fixed corners]")
row_s2 = box.row()
row_s2.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
icons = {'CUBE': 'MESH_CUBE', 'SQUARE': 'MESH_PLANE', 'CIRCLE': 'MESH_CIRCLE', 'ELLIPSE': 'MESH_CIRCLE'}
texts = {'CUBE': "Create Cube Frame", 'SQUARE': "Create Square Torus", 'CIRCLE': "Create Circle Torus", 'ELLIPSE': "Create Ellipse Torus"}
col_exec.operator(OT_CreateTorus.bl_idname, icon=icons.get(props.base_shape, 'MESH_TORUS'), text=texts.get(props.base_shape, "Create Torus"))
class PT_PieGeneratorPanel(Panel):
bl_label = "Pie & Triangle Generator"
bl_idname = f"{PREFIX}_PT_pie"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PIE_PROPS_NAME, None)
if not props: return
box = layout.box()
box.label(text="Base Settings:", icon='MESH_CIRCLE')
box.prop(props, "pie_plane")
box.prop(props, "pie_radius")
box.prop(props, "pie_thickness")
box.prop(props, "pie_segments")
box.separator()
box.label(text="Angles (X-Axis=0°, Y-Axis=90°):", icon='ORIENTATION_GIMBAL')
row = box.row(align=True)
row.prop(props, "pie_start_angle")
row.prop(props, "pie_end_angle")
box.separator()
box.label(text="Colors for Parts:", icon='COLOR')
box.prop(props, "color_tri", text="Triangle Color")
box.prop(props, "color_bow", text="Bow (Segment) Color")
box.prop(props, "color_rest", text="Rest Circle Color")
layout.separator()
col = layout.column()
col.scale_y = 1.5
col.operator(OT_CreatePieShape.bl_idname, icon='MESH_CIRCLE', text="Create Pie Objects")
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,
PG_PieProps,
OT_CreateTorus,
OT_CreatePieShape,
OT_CopyFullScript,
OT_Reset,
OT_OpenUrl,
OT_RemoveAddon,
PT_MainPanel,
PT_PieGeneratorPanel,
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:
try: bpy.utils.register_class(c)
except ValueError: pass
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_TorusProps))
setattr(bpy.types.Scene, PIE_PROPS_NAME, PointerProperty(type=PG_PieProps))
bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)
def unregister():
global _timer
if _timer is not None:
try: bpy.app.timers.unregister(_timer)
except Exception: pass
_timer = None
cleanup_preview_data()
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
if hasattr(bpy.types.Scene, PIE_PROPS_NAME): delattr(bpy.types.Scene, PIE_PROPS_NAME)
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except ValueError: pass
if __name__ == "__main__":
register()