blender Million 2026
# Copied: 2026-03-24 21:03: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, 10, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": f"Multi-Shape Torus & Equation Cylinders - {PREFIX}",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
# ★ リンク設定
ADDON_LINKS = (
{"label": "時空図 交点 2060407", "url": "<https://www.notion.so/2060407-33af5dacaf43808d86bbf0d54d4d0dd5>"},
{"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,
"cyl_thickness": 0.5000,
"cyl_color": (0.1000, 0.6000, 0.9000, 1.0000),
"zx_color": (0.9000, 0.2000, 0.2000, 1.0000),
"minus_zx_color": (0.9000, 0.8000, 0.2000, 1.0000),
"custom_zx_color": (0.2000, 0.8000, 0.9000, 1.0000),
"cyl_slope_a": 0.6000,
"cyl_offset": 10.0000,
"cyl_limit": 50.0000,
"cyl_custom_b": 1.6000,
"show_cyl_group1": True,
"show_cyl_group2": True,
"show_cyl_group3": True,
"show_cyl_group4": True,
}
# <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)
# ==============================================================================
# ジオメトリ エンジン
# ==============================================================================
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
p = mathutils.Vector((a * math.cos(t), b * math.sin(t), 0))
n = mathutils.Vector((b * math.cos(t), a * math.sin(t), 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): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(major_segments):
try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % major_segments])
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
pts.append((mathutils.Vector((cx + actual_corner_radius * math.cos(angle), cy + actual_corner_radius * math.sin(angle), 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
ring.append(bm.verts.new(p + n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))))
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): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(total_rings):
try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % total_rings])
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
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()
bmesh.ops.transform(bm, matrix=mathutils.Vector((0,0,1)).rotation_difference(axis).to_matrix().to_4x4(), verts=geom['verts'])
bmesh.ops.translate(bm, verts=geom['verts'], vec=(v1 + v2) / 2.0)
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
# ==============================================================================
# 計算ロジック(直線の方程式をボックス制限内でクリッピング)
# ==============================================================================
def get_line_segment_in_bounds(M, C, limit):
pts = []
eps = 1e-4
z1 = M * (-limit) + C
if -limit - eps <= z1 <= limit + eps: pts.append((-limit, z1))
z2 = M * limit + C
if -limit - eps <= z2 <= limit + eps: pts.append((limit, z2))
if abs(M) > 1e-6:
x3 = (-limit - C) / M
if -limit - eps <= x3 <= limit + eps: pts.append((x3, -limit))
x4 = (limit - C) / M
if -limit - eps <= x4 <= limit + eps: pts.append((x4, limit))
unique_pts = []
for p in pts:
if not any(abs(p[0] - up[0]) < eps and abs(p[1] - up[1]) < eps for up in unique_pts):
unique_pts.append(p)
if len(unique_pts) >= 2: return unique_pts[0], unique_pts[1]
return None
def create_cylinder_line(M, C, limit, thickness, mat, target_collection):
pts = get_line_segment_in_bounds(M, C, limit)
if not pts: return None
p1_2d, p2_2d = pts
p1 = mathutils.Vector((p1_2d[0], 0.0, p1_2d[1]))
p2 = mathutils.Vector((p2_2d[0], 0.0, p2_2d[1]))
dist = (p2 - p1).length
if dist < 1e-4: return None
center = (p1 + p2) / 2.0
bm = bmesh.new()
geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=32, radius1=thickness, radius2=thickness, depth=dist)
axis = (p2 - p1).normalized()
up = mathutils.Vector((0, 0, 1))
bmesh.ops.transform(bm, matrix=up.rotation_difference(axis).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
mesh = bpy.data.meshes.new(f"EqCyl_Mesh")
bm.to_mesh(mesh)
bm.free(); apply_auto_smooth(mesh)
obj = bpy.data.objects.new(f"EqLine_{datetime.now().strftime('%H%M%S')}", mesh)
target_collection.objects.link(obj)
obj.data.materials.append(mat)
return obj
# ==============================================================================
# マテリアル・プレビュー制御
# ==============================================================================
def create_unique_material(color, name_prefix="Mat_UniqueShape"):
mat_name = f"{name_prefix}_{datetime.now().strftime('%M%S%f')[:5]}"
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")
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):
sx = min(max(props.size_x, 0.01), 10000.0); sy = min(max(props.size_y, 0.01), 10000.0)
mr = min(max(props.minor_radius, 0.001), 5000.0)
if props.base_shape == 'CUBE': create_cube_framework_bmesh(bm, sx, mr, props.minor_segments)
elif props.base_shape == 'SQUARE': create_square_torus_bmesh(bm, sx, props.corner_radius, mr, props.corner_segments, props.minor_segments)
elif props.base_shape == 'CIRCLE': create_ellipse_torus_bmesh(bm, sx, sx, mr, props.major_segments, props.minor_segments)
elif props.base_shape == 'ELLIPSE': create_ellipse_torus_bmesh(bm, sx, sy, mr, props.major_segments, props.minor_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:
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)
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)
# ★ 表示/非表示トグル用のアップデート関数
def update_cyl_visibility(self, context):
prefixes = {
'show_cyl_group1': "EqLine_1_over_aX_",
'show_cyl_group2': "EqLine_Z_eq_X_",
'show_cyl_group3': "EqLine_Z_eq_minus_X_",
'show_cyl_group4': "EqLine_Z_eq_1_over_bX_"
}
for prop_name, prefix in prefixes.items():
is_visible = getattr(self, prop_name)
for obj in bpy.data.objects:
if obj.name.startswith(prefix):
obj.hide_viewport = not is_visible
obj.hide_render = not is_visible
# ==============================================================================
# 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", ""), ('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", 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)
cyl_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['cyl_thickness'], min=0.01, max=50.0)
cyl_color: FloatVectorProperty(name="Color (1/a)(X±C)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['cyl_color'])
zx_color: FloatVectorProperty(name="Color (Z=X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['zx_color'])
minus_zx_color: FloatVectorProperty(name="Color (Z=-X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['minus_zx_color'])
custom_zx_color: FloatVectorProperty(name="Color Custom", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['custom_zx_color'])
cyl_slope_a: FloatProperty(name="Slope Param (a)", default=CURRENT_DEFAULTS['cyl_slope_a'], min=0.001)
cyl_offset: FloatProperty(name="Offset (C)", default=CURRENT_DEFAULTS['cyl_offset'], min=0.0)
cyl_limit: FloatProperty(name="Limit Bounds", default=CURRENT_DEFAULTS['cyl_limit'], min=1.0)
cyl_custom_b: FloatProperty(name="Custom Slope (b)", default=CURRENT_DEFAULTS['cyl_custom_b'])
show_cyl_group1: BoolProperty(name="Vis G1", default=CURRENT_DEFAULTS['show_cyl_group1'], update=update_cyl_visibility)
show_cyl_group2: BoolProperty(name="Vis G2", default=CURRENT_DEFAULTS['show_cyl_group2'], update=update_cyl_visibility)
show_cyl_group3: BoolProperty(name="Vis G3", default=CURRENT_DEFAULTS['show_cyl_group3'], update=update_cyl_visibility)
show_cyl_group4: BoolProperty(name="Vis G4", default=CURRENT_DEFAULTS['show_cyl_group4'], update=update_cyl_visibility)
# ==============================================================================
# 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")
col_name = f"ShapeGroup_{datetime.now().strftime('%H%M%S')}"
new_col = bpy.data.collections.new(col_name)
context.scene.collection.children.link(new_col)
obj = bpy.data.objects.new(f"{prefix_name}_{datetime.now().strftime('%H%M%S')}", mesh)
new_col.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} in collection '{col_name}'!")
return {'FINISHED'}
class OT_CreateEquationCylinders(Operator):
bl_idname = f"{OP_PREFIX}.create_equation_cylinders"
bl_label = "Create 6 Cylinders"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
a = props.cyl_slope_a; C = props.cyl_offset; limit = props.cyl_limit; thickness = props.cyl_thickness
b = props.cyl_custom_b
if abs(a) < 0.0001:
self.report({'ERROR'}, "Parameter 'a' is too close to zero!")
return {'CANCELLED'}
col_name = f"EqCylinders_{datetime.now().strftime('%H%M%S')}"
new_col = bpy.data.collections.new(col_name)
context.scene.collection.children.link(new_col)
mat_eq = create_unique_material(props.cyl_color, "Mat_EqLine")
mat_zx = create_unique_material(props.zx_color, "Mat_ZXLine")
mat_mzx = create_unique_material(props.minus_zx_color, "Mat_MinusZXLine")
mat_custom = create_unique_material(props.custom_zx_color, "Mat_CustomZXLine")
created_objs = []
# 1. Z = (1/a)(X ± C) (3 lines)
for C_sign in [-1, 0, 1]:
C_val = C_sign * C
obj = create_cylinder_line(1.0/a, C_val/a, limit, thickness, mat_eq, new_col)
if obj:
obj.name = f"EqLine_1_over_aX_C{C_val}_{datetime.now().strftime('%H%M%S')}"
obj.hide_viewport = not props.show_cyl_group1
obj.hide_render = not props.show_cyl_group1
created_objs.append(obj)
# 2. Z = X (1 line)
obj_zx = create_cylinder_line(1.0, 0.0, limit, thickness, mat_zx, new_col)
if obj_zx:
obj_zx.name = f"EqLine_Z_eq_X_{datetime.now().strftime('%H%M%S')}"
obj_zx.hide_viewport = not props.show_cyl_group2
obj_zx.hide_render = not props.show_cyl_group2
created_objs.append(obj_zx)
# 3. Z = -X (1 line)
obj_mzx = create_cylinder_line(-1.0, 0.0, limit, thickness, mat_mzx, new_col)
if obj_mzx:
obj_mzx.name = f"EqLine_Z_eq_minus_X_{datetime.now().strftime('%H%M%S')}"
obj_mzx.hide_viewport = not props.show_cyl_group3
obj_mzx.hide_render = not props.show_cyl_group3
created_objs.append(obj_mzx)
# 4. Z = (1/b)X (1 line)
if abs(b) < 0.0001:
self.report({'WARNING'}, "Custom parameter 'b' is zero. Skipped creating Custom Line.")
else:
obj_custom = create_cylinder_line(1.0 / b, 0.0, limit, thickness, mat_custom, new_col)
if obj_custom:
obj_custom.name = f"EqLine_Z_eq_1_over_bX_{datetime.now().strftime('%H%M%S')}"
obj_custom.hide_viewport = not props.show_cyl_group4
obj_custom.hide_render = not props.show_cyl_group4
created_objs.append(obj_custom)
bpy.ops.object.select_all(action='DESELECT')
for obj in created_objs: obj.select_set(True)
if created_objs:
context.view_layer.objects.active = created_objs[-1]
self.report({'INFO'}, f"Created {len(created_objs)} Cylinders in collection '{col_name}'!")
else:
self.report({'WARNING'}, "No cylinders created. Lines might be entirely outside the bounds.")
return {'FINISHED'}
class OT_CopyIntersectionInfo(Operator):
bl_idname = f"{OP_PREFIX}.copy_intersection"
bl_label = "Copy Intersections"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
a = props.cyl_slope_a; C = props.cyl_offset; b = props.cyl_custom_b
text = "Intersection Points:\n"
text += f"Equations & Parameters:\n"
text += f" Base Lines : Z = (1/{a:.4f})(X ± {C:.4f})\n\n"
text += "[ Z = X and Base Lines ]\n"
if abs(a - 1.0) < 0.0001: text += " Lines are parallel.\n"
else:
for sign in [-1, 0, 1]:
C_val = sign * C
x_val = C_val / (a - 1.0)
text += f" C={C_val:+.1f} : X={x_val:.4f}, Z={x_val:.4f}\n"
text += "\n[ Z = -X and Base Lines ]\n"
if abs(a + 1.0) < 0.0001: text += " Lines are parallel.\n"
else:
for sign in [-1, 0, 1]:
C_val = sign * C
x_val = -C_val / (a + 1.0)
text += f" C={C_val:+.1f} : X={x_val:.4f}, Z={-x_val:.4f}\n"
text += f"\n[ Z = (1/{b:.4f})X and Base Lines ]\n"
d = a - b
if abs(b) < 0.0001: text += " Parameter 'b' is zero. Invalid.\n"
elif abs(d) < 0.0001: text += " Lines are parallel.\n"
else:
for sign in [-1, 0, 1]:
C_val = sign * C
x_val = (b * C_val) / d
text += f" C={C_val:+.1f} : X={x_val:.4f}, Z={x_val/b:.4f}\n"
context.window_manager.clipboard = text
self.report({'INFO'}, "Intersection Data Copied to Clipboard!")
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: return {'CANCELLED'}
code = target_text.as_string()
c, l, r = props.torus_color, props.torus_loc, props.torus_rot
cc, zc, mzc, czc = props.cyl_color, props.zx_color, props.minus_zx_color, props.custom_zx_color
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'
new_dict += f' "cyl_thickness": {props.cyl_thickness:.4f},\n'
new_dict += f' "cyl_color": ({cc[0]:.4f}, {cc[1]:.4f}, {cc[2]:.4f}, {cc[3]:.4f}),\n'
new_dict += f' "zx_color": ({zc[0]:.4f}, {zc[1]:.4f}, {zc[2]:.4f}, {zc[3]:.4f}),\n'
new_dict += f' "minus_zx_color": ({mzc[0]:.4f}, {mzc[1]:.4f}, {mzc[2]:.4f}, {mzc[3]:.4f}),\n'
new_dict += f' "custom_zx_color": ({czc[0]:.4f}, {czc[1]:.4f}, {czc[2]:.4f}, {czc[3]:.4f}),\n'
new_dict += f' "cyl_slope_a": {props.cyl_slope_a:.4f},\n'
new_dict += f' "cyl_offset": {props.cyl_offset:.4f},\n'
new_dict += f' "cyl_limit": {props.cyl_limit:.4f},\n'
new_dict += f' "cyl_custom_b": {props.cyl_custom_b:.4f},\n'
new_dict += f' "show_cyl_group1": {props.show_cyl_group1},\n'
new_dict += f' "show_cyl_group2": {props.show_cyl_group2},\n'
new_dict += f' "show_cyl_group3": {props.show_cyl_group3},\n'
new_dict += f' "show_cyl_group4": {props.show_cyl_group4},\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: 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: 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: 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
p.cyl_thickness = 0.5; p.cyl_color = (0.1, 0.6, 0.9, 1.0)
p.zx_color = (0.9, 0.2, 0.2, 1.0); p.minus_zx_color = (0.9, 0.8, 0.2, 1.0)
p.custom_zx_color = (0.2, 0.8, 0.9, 1.0)
p.cyl_slope_a = 0.6; p.cyl_offset = 10.0; p.cyl_limit = 50.0; p.cyl_custom_b = 1.6
p.show_cyl_group1 = True; p.show_cyl_group2 = True
p.show_cyl_group3 = True; p.show_cyl_group4 = True
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]")
box.row().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_EquationCylindersPanel(Panel):
bl_label = "Equation Cylinders (6 Lines)"
bl_idname = f"{PREFIX}_PT_eq_cylinders"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
a = props.cyl_slope_a
C = props.cyl_offset
b = props.cyl_custom_b
box = layout.box()
box.label(text="Math Equations (Y=0 Plane):", icon='FILE_TEXT')
box.label(text=f" G1: Z = ( 1 / {a:.2f} ) ( X ± {C:.2f} )")
box.label(text=f" G2: Z = X")
box.label(text=f" G3: Z = -X")
box.label(text=f" G4: Z = ( 1 / {b:.2f} ) X")
box.separator()
col = box.column(align=True)
col.prop(props, "cyl_slope_a", text="Param (a)")
col.prop(props, "cyl_offset", text="Offset (C)")
col.prop(props, "cyl_limit", text="Limit Bounds (-L to L)")
col.prop(props, "cyl_custom_b", text="Custom Param (b)")
box.separator()
box.prop(props, "cyl_thickness", text="Thickness")
# ★ カラー設定 & 表示トグル
col_c = box.column(align=True)
r1 = col_c.row(align=True)
r1.prop(props, "show_cyl_group1", text="", icon='HIDE_OFF' if props.show_cyl_group1 else 'HIDE_ON')
r1.prop(props, "cyl_color", text=f"Z=(1/{a:.2f})(X±{C:.1f})")
r2 = col_c.row(align=True)
r2.prop(props, "show_cyl_group2", text="", icon='HIDE_OFF' if props.show_cyl_group2 else 'HIDE_ON')
r2.prop(props, "zx_color", text="Z = X")
r3 = col_c.row(align=True)
r3.prop(props, "show_cyl_group3", text="", icon='HIDE_OFF' if props.show_cyl_group3 else 'HIDE_ON')
r3.prop(props, "minus_zx_color", text="Z = -X")
r4 = col_c.row(align=True)
r4.prop(props, "show_cyl_group4", text="", icon='HIDE_OFF' if props.show_cyl_group4 else 'HIDE_ON')
r4.prop(props, "custom_zx_color", text=f"Z = (1/{b:.2f})X")
# ★ 交点表示エリア (縦1行ずつ表示)
box.separator()
box_int = box.box()
box_int.label(text=f"Intersections with Z=(1/{a:.2f})(X±{C:.1f}) :", icon='DRIVER')
# [ Z = X ] 側
box_int.label(text="--- [ with Z = X ] ---")
if abs(1.0 - a) < 0.0001:
box_int.label(text=" Parallel")
else:
for sign in [-1, 0, 1]:
C_val = sign * C
x_val = C_val / (a - 1.0)
box_int.label(text=f" C={C_val:+.1f} : X={x_val:.2f}, Z={x_val:.2f}")
box_int.separator()
# [ Z = -X ] 側
box_int.label(text="--- [ with Z = -X ] ---")
if abs(-1.0 - a) < 0.0001:
box_int.label(text=" Parallel")
else:
for sign in [-1, 0, 1]:
C_val = sign * C
x_val = -C_val / (a + 1.0)
box_int.label(text=f" C={C_val:+.1f} : X={x_val:.2f}, Z={-x_val:.2f}")
box_int.separator()
# [ Z = (1/b)X ] 側
box_int.label(text=f"--- [ with Z = (1/{b:.2f})X ] ---")
d = a - b
if abs(b) < 0.0001:
box_int.label(text=" Invalid (b=0)")
elif abs(d) < 0.0001:
box_int.label(text=" Parallel")
else:
for sign in [-1, 0, 1]:
C_val = sign * C
x_val = (b * C_val) / d
box_int.label(text=f" C={C_val:+.1f} : X={x_val:.2f}, Z={x_val/b:.2f}")
box_int.operator(OT_CopyIntersectionInfo.bl_idname, icon='COPYDOWN', text="Copy Intersections")
col_exec = box.column(); col_exec.scale_y = 1.2
col_exec.operator(OT_CreateEquationCylinders.bl_idname, icon='MESH_CYLINDER', text="Create 6 Cylinders")
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_CreateEquationCylinders,
OT_CopyIntersectionInfo,
OT_CopyFullScript,
OT_Reset,
OT_OpenUrl,
OT_RemoveAddon,
PT_MainPanel,
PT_EquationCylindersPanel,
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))
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)
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except ValueError: pass
if __name__ == "__main__": register()
# Copied: 2026-03-24 21:03: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, 9, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": f"Multi-Shape Torus & Equation Cylinders - {PREFIX}",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
# ★ リンク設定
ADDON_LINKS = (
{"label": "時空図 交点 2060407", "url": "<https://www.notion.so/2060407-33af5dacaf43808d86bbf0d54d4d0dd5>"},
{"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,
"cyl_thickness": 0.5000,
"cyl_color": (0.1000, 0.6000, 0.9000, 1.0000),
"zx_color": (0.9000, 0.2000, 0.2000, 1.0000),
"minus_zx_color": (0.9000, 0.8000, 0.2000, 1.0000),
"custom_zx_color": (0.2000, 0.8000, 0.9000, 1.0000),
"cyl_slope_a": 0.6000,
"cyl_offset": 10.0000,
"cyl_limit": 50.0000,
"cyl_custom_b": -1.6000,
"show_cyl_group1": True,
"show_cyl_group2": True,
"show_cyl_group3": True,
"show_cyl_group4": True,
}
# <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)
# ==============================================================================
# ジオメトリ エンジン
# ==============================================================================
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
p = mathutils.Vector((a * math.cos(t), b * math.sin(t), 0))
n = mathutils.Vector((b * math.cos(t), a * math.sin(t), 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): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(major_segments):
try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % major_segments])
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
pts.append((mathutils.Vector((cx + actual_corner_radius * math.cos(angle), cy + actual_corner_radius * math.sin(angle), 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
ring.append(bm.verts.new(p + n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))))
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): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(total_rings):
try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % total_rings])
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
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()
bmesh.ops.transform(bm, matrix=mathutils.Vector((0,0,1)).rotation_difference(axis).to_matrix().to_4x4(), verts=geom['verts'])
bmesh.ops.translate(bm, verts=geom['verts'], vec=(v1 + v2) / 2.0)
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
# ==============================================================================
# 計算ロジック(直線の方程式をボックス制限内でクリッピング)
# ==============================================================================
def get_line_segment_in_bounds(M, C, limit):
pts = []
eps = 1e-4
z1 = M * (-limit) + C
if -limit - eps <= z1 <= limit + eps: pts.append((-limit, z1))
z2 = M * limit + C
if -limit - eps <= z2 <= limit + eps: pts.append((limit, z2))
if abs(M) > 1e-6:
x3 = (-limit - C) / M
if -limit - eps <= x3 <= limit + eps: pts.append((x3, -limit))
x4 = (limit - C) / M
if -limit - eps <= x4 <= limit + eps: pts.append((x4, limit))
unique_pts = []
for p in pts:
if not any(abs(p[0] - up[0]) < eps and abs(p[1] - up[1]) < eps for up in unique_pts):
unique_pts.append(p)
if len(unique_pts) >= 2: return unique_pts[0], unique_pts[1]
return None
def create_cylinder_line(M, C, limit, thickness, mat, target_collection):
pts = get_line_segment_in_bounds(M, C, limit)
if not pts: return None
p1_2d, p2_2d = pts
p1 = mathutils.Vector((p1_2d[0], 0.0, p1_2d[1]))
p2 = mathutils.Vector((p2_2d[0], 0.0, p2_2d[1]))
dist = (p2 - p1).length
if dist < 1e-4: return None
center = (p1 + p2) / 2.0
bm = bmesh.new()
geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=32, radius1=thickness, radius2=thickness, depth=dist)
axis = (p2 - p1).normalized()
up = mathutils.Vector((0, 0, 1))
bmesh.ops.transform(bm, matrix=up.rotation_difference(axis).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
mesh = bpy.data.meshes.new(f"EqCyl_Mesh")
bm.to_mesh(mesh)
bm.free(); apply_auto_smooth(mesh)
obj = bpy.data.objects.new(f"EqLine_{datetime.now().strftime('%H%M%S')}", mesh)
target_collection.objects.link(obj)
obj.data.materials.append(mat)
return obj
# ==============================================================================
# マテリアル・プレビュー制御
# ==============================================================================
def create_unique_material(color, name_prefix="Mat_UniqueShape"):
mat_name = f"{name_prefix}_{datetime.now().strftime('%M%S%f')[:5]}"
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")
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):
sx = min(max(props.size_x, 0.01), 10000.0); sy = min(max(props.size_y, 0.01), 10000.0)
mr = min(max(props.minor_radius, 0.001), 5000.0)
if props.base_shape == 'CUBE': create_cube_framework_bmesh(bm, sx, mr, props.minor_segments)
elif props.base_shape == 'SQUARE': create_square_torus_bmesh(bm, sx, props.corner_radius, mr, props.corner_segments, props.minor_segments)
elif props.base_shape == 'CIRCLE': create_ellipse_torus_bmesh(bm, sx, sx, mr, props.major_segments, props.minor_segments)
elif props.base_shape == 'ELLIPSE': create_ellipse_torus_bmesh(bm, sx, sy, mr, props.major_segments, props.minor_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:
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)
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)
# ★ 表示/非表示トグル用のアップデート関数
def update_cyl_visibility(self, context):
prefixes = {
'show_cyl_group1': "EqLine_aX_",
'show_cyl_group2': "EqLine_Z_eq_X_",
'show_cyl_group3': "EqLine_Z_eq_minus_X_",
'show_cyl_group4': "EqLine_Z_eq_custom_bX_"
}
for prop_name, prefix in prefixes.items():
is_visible = getattr(self, prop_name)
for obj in bpy.data.objects:
if obj.name.startswith(prefix):
obj.hide_viewport = not is_visible
obj.hide_render = not is_visible
# ==============================================================================
# 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", ""), ('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", 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)
cyl_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['cyl_thickness'], min=0.01, max=50.0)
cyl_color: FloatVectorProperty(name="Color (a*X ± C)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['cyl_color'])
zx_color: FloatVectorProperty(name="Color (Z=X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['zx_color'])
minus_zx_color: FloatVectorProperty(name="Color (Z=-X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['minus_zx_color'])
custom_zx_color: FloatVectorProperty(name="Color Custom", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['custom_zx_color'])
cyl_slope_a: FloatProperty(name="Slope (a)", default=CURRENT_DEFAULTS['cyl_slope_a'])
cyl_offset: FloatProperty(name="Offset (C)", default=CURRENT_DEFAULTS['cyl_offset'], min=0.0)
cyl_limit: FloatProperty(name="Limit Bounds", default=CURRENT_DEFAULTS['cyl_limit'], min=1.0)
cyl_custom_b: FloatProperty(name="Custom Slope (b)", default=CURRENT_DEFAULTS['cyl_custom_b'])
show_cyl_group1: BoolProperty(name="Vis G1", default=CURRENT_DEFAULTS['show_cyl_group1'], update=update_cyl_visibility)
show_cyl_group2: BoolProperty(name="Vis G2", default=CURRENT_DEFAULTS['show_cyl_group2'], update=update_cyl_visibility)
show_cyl_group3: BoolProperty(name="Vis G3", default=CURRENT_DEFAULTS['show_cyl_group3'], update=update_cyl_visibility)
show_cyl_group4: BoolProperty(name="Vis G4", default=CURRENT_DEFAULTS['show_cyl_group4'], update=update_cyl_visibility)
# ==============================================================================
# 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")
col_name = f"ShapeGroup_{datetime.now().strftime('%H%M%S')}"
new_col = bpy.data.collections.new(col_name)
context.scene.collection.children.link(new_col)
obj = bpy.data.objects.new(f"{prefix_name}_{datetime.now().strftime('%H%M%S')}", mesh)
new_col.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} in collection '{col_name}'!")
return {'FINISHED'}
class OT_CreateEquationCylinders(Operator):
bl_idname = f"{OP_PREFIX}.create_equation_cylinders"
bl_label = "Create 6 Cylinders"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
a = props.cyl_slope_a; C_val = props.cyl_offset; limit = props.cyl_limit; thickness = props.cyl_thickness
b = props.cyl_custom_b
col_name = f"EqCylinders_{datetime.now().strftime('%H%M%S')}"
new_col = bpy.data.collections.new(col_name)
context.scene.collection.children.link(new_col)
mat_eq = create_unique_material(props.cyl_color, "Mat_EqLine")
mat_zx = create_unique_material(props.zx_color, "Mat_ZXLine")
mat_mzx = create_unique_material(props.minus_zx_color, "Mat_MinusZXLine")
mat_custom = create_unique_material(props.custom_zx_color, "Mat_CustomZXLine")
created_objs = []
# 1. Z = a*X ± C (3 lines)
for C in [-C_val, 0.0, C_val]:
obj = create_cylinder_line(a, C, limit, thickness, mat_eq, new_col)
if obj:
obj.name = f"EqLine_aX_C{C}_{datetime.now().strftime('%H%M%S')}"
obj.hide_viewport = not props.show_cyl_group1
obj.hide_render = not props.show_cyl_group1
created_objs.append(obj)
# 2. Z = X (1 line)
obj_zx = create_cylinder_line(1.0, 0.0, limit, thickness, mat_zx, new_col)
if obj_zx:
obj_zx.name = f"EqLine_Z_eq_X_{datetime.now().strftime('%H%M%S')}"
obj_zx.hide_viewport = not props.show_cyl_group2
obj_zx.hide_render = not props.show_cyl_group2
created_objs.append(obj_zx)
# 3. Z = -X (1 line)
obj_mzx = create_cylinder_line(-1.0, 0.0, limit, thickness, mat_mzx, new_col)
if obj_mzx:
obj_mzx.name = f"EqLine_Z_eq_minus_X_{datetime.now().strftime('%H%M%S')}"
obj_mzx.hide_viewport = not props.show_cyl_group3
obj_mzx.hide_render = not props.show_cyl_group3
created_objs.append(obj_mzx)
# 4. Z = b*X (1 line)
obj_custom = create_cylinder_line(b, 0.0, limit, thickness, mat_custom, new_col)
if obj_custom:
obj_custom.name = f"EqLine_Z_eq_custom_bX_{datetime.now().strftime('%H%M%S')}"
obj_custom.hide_viewport = not props.show_cyl_group4
obj_custom.hide_render = not props.show_cyl_group4
created_objs.append(obj_custom)
bpy.ops.object.select_all(action='DESELECT')
for obj in created_objs: obj.select_set(True)
if created_objs:
context.view_layer.objects.active = created_objs[-1]
self.report({'INFO'}, f"Created {len(created_objs)} Cylinders in collection '{col_name}'!")
else:
self.report({'WARNING'}, "No cylinders created. Lines might be entirely outside the bounds.")
return {'FINISHED'}
class OT_CopyIntersectionInfo(Operator):
bl_idname = f"{OP_PREFIX}.copy_intersection"
bl_label = "Copy Intersections"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
a = props.cyl_slope_a; C_val = props.cyl_offset; b = props.cyl_custom_b
text = "Intersection Points:\n"
text += f"Equations & Parameters:\n"
text += f" Base Lines : Z = {a:.4f} * X ± {C_val:.4f}\n\n"
text += "[ Z = X and Base Lines ]\n"
if abs(1.0 - a) < 0.0001: text += " Lines are parallel.\n"
else:
x_m = (-C_val) / (1.0 - a); x_p = C_val / (1.0 - a)
text += f" Line (C = -{C_val:.1f}): X = {x_m:.4f}, Z = {x_m:.4f}\n"
text += f" Line (C = 0.0): X = 0.0000, Z = 0.0000\n"
text += f" Line (C = +{C_val:.1f}): X = {x_p:.4f}, Z = {x_p:.4f}\n"
text += "\n[ Z = -X and Base Lines ]\n"
if abs(-1.0 - a) < 0.0001: text += " Lines are parallel.\n"
else:
xm_m = (-C_val) / (-1.0 - a); xm_p = C_val / (-1.0 - a)
text += f" Line (C = -{C_val:.1f}): X = {xm_m:.4f}, Z = {-xm_m:.4f}\n"
text += f" Line (C = 0.0): X = 0.0000, Z = 0.0000\n"
text += f" Line (C = +{C_val:.1f}): X = {xm_p:.4f}, Z = {-xm_p:.4f}\n"
text += f"\n[ Z = {b:.4f} * X and Base Lines ]\n"
d = b - a
if abs(d) < 0.0001: text += " Lines are parallel.\n"
else:
xb_m = (-C_val) / d; xb_p = C_val / d
text += f" Line (C = -{C_val:.1f}): X = {xb_m:.4f}, Z = {b*xb_m:.4f}\n"
text += f" Line (C = 0.0): X = 0.0000, Z = 0.0000\n"
text += f" Line (C = +{C_val:.1f}): X = {xb_p:.4f}, Z = {b*xb_p:.4f}\n"
context.window_manager.clipboard = text
self.report({'INFO'}, "Intersection Data Copied to Clipboard!")
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: return {'CANCELLED'}
code = target_text.as_string()
c, l, r = props.torus_color, props.torus_loc, props.torus_rot
cc, zc, mzc, czc = props.cyl_color, props.zx_color, props.minus_zx_color, props.custom_zx_color
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'
new_dict += f' "cyl_thickness": {props.cyl_thickness:.4f},\n'
new_dict += f' "cyl_color": ({cc[0]:.4f}, {cc[1]:.4f}, {cc[2]:.4f}, {cc[3]:.4f}),\n'
new_dict += f' "zx_color": ({zc[0]:.4f}, {zc[1]:.4f}, {zc[2]:.4f}, {zc[3]:.4f}),\n'
new_dict += f' "minus_zx_color": ({mzc[0]:.4f}, {mzc[1]:.4f}, {mzc[2]:.4f}, {mzc[3]:.4f}),\n'
new_dict += f' "custom_zx_color": ({czc[0]:.4f}, {czc[1]:.4f}, {czc[2]:.4f}, {czc[3]:.4f}),\n'
new_dict += f' "cyl_slope_a": {props.cyl_slope_a:.4f},\n'
new_dict += f' "cyl_offset": {props.cyl_offset:.4f},\n'
new_dict += f' "cyl_limit": {props.cyl_limit:.4f},\n'
new_dict += f' "cyl_custom_b": {props.cyl_custom_b:.4f},\n'
new_dict += f' "show_cyl_group1": {props.show_cyl_group1},\n'
new_dict += f' "show_cyl_group2": {props.show_cyl_group2},\n'
new_dict += f' "show_cyl_group3": {props.show_cyl_group3},\n'
new_dict += f' "show_cyl_group4": {props.show_cyl_group4},\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: 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: 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: 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
p.cyl_thickness = 0.5; p.cyl_color = (0.1, 0.6, 0.9, 1.0)
p.zx_color = (0.9, 0.2, 0.2, 1.0); p.minus_zx_color = (0.9, 0.8, 0.2, 1.0)
p.custom_zx_color = (0.2, 0.8, 0.9, 1.0)
p.cyl_slope_a = 0.6; p.cyl_offset = 10.0; p.cyl_limit = 50.0; p.cyl_custom_b = -1.6
p.show_cyl_group1 = True; p.show_cyl_group2 = True
p.show_cyl_group3 = True; p.show_cyl_group4 = True
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]")
box.row().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_EquationCylindersPanel(Panel):
bl_label = "Equation Cylinders (6 Lines)"
bl_idname = f"{PREFIX}_PT_eq_cylinders"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
a = props.cyl_slope_a
C_val = props.cyl_offset
b = props.cyl_custom_b
box = layout.box()
box.label(text="Math Equations (Y=0 Plane):", icon='FILE_TEXT')
box.label(text=f" G1: Z = {a:.2f} * X ± {C_val:.2f}")
box.label(text=f" G2: Z = X")
box.label(text=f" G3: Z = -X")
box.label(text=f" G4: Z = {b:.2f} * X")
box.separator()
col = box.column(align=True)
col.prop(props, "cyl_slope_a", text="Slope (a)")
col.prop(props, "cyl_offset", text="Offset (C)")
col.prop(props, "cyl_limit", text="Limit Bounds (-L to L)")
col.prop(props, "cyl_custom_b", text="Custom Slope (b)")
box.separator()
box.prop(props, "cyl_thickness", text="Thickness")
# ★ カラー設定 & 表示トグル
col_c = box.column(align=True)
r1 = col_c.row(align=True)
r1.prop(props, "show_cyl_group1", text="", icon='HIDE_OFF' if props.show_cyl_group1 else 'HIDE_ON')
r1.prop(props, "cyl_color", text=f"Z = {a:.2f}X ± {C_val:.1f}")
r2 = col_c.row(align=True)
r2.prop(props, "show_cyl_group2", text="", icon='HIDE_OFF' if props.show_cyl_group2 else 'HIDE_ON')
r2.prop(props, "zx_color", text="Z = X")
r3 = col_c.row(align=True)
r3.prop(props, "show_cyl_group3", text="", icon='HIDE_OFF' if props.show_cyl_group3 else 'HIDE_ON')
r3.prop(props, "minus_zx_color", text="Z = -X")
r4 = col_c.row(align=True)
r4.prop(props, "show_cyl_group4", text="", icon='HIDE_OFF' if props.show_cyl_group4 else 'HIDE_ON')
r4.prop(props, "custom_zx_color", text=f"Z = {b:.2f}X")
# ★ 交点表示エリア (縦1行ずつ表示)
box.separator()
box_int = box.box()
box_int.label(text=f"Intersections with Z={a:.2f}X ± {C_val:.1f} :", icon='DRIVER')
# [ Z = X ] 側
box_int.label(text="--- [ with Z = X ] ---")
if abs(1.0 - a) < 0.0001:
box_int.label(text=" Parallel")
else:
x_minus = (-C_val) / (1.0 - a); x_plus = C_val / (1.0 - a)
box_int.label(text=f" C=-{C_val:.1f} : X={x_minus:.2f}, Z={x_minus:.2f}")
box_int.label(text=f" C= 0.0 : X=0.00, Z=0.00")
box_int.label(text=f" C=+{C_val:.1f} : X={x_plus:.2f}, Z={x_plus:.2f}")
box_int.separator()
# [ Z = -X ] 側
box_int.label(text="--- [ with Z = -X ] ---")
if abs(-1.0 - a) < 0.0001:
box_int.label(text=" Parallel")
else:
xm_minus = (-C_val) / (-1.0 - a); xm_plus = C_val / (-1.0 - a)
box_int.label(text=f" C=-{C_val:.1f} : X={xm_minus:.2f}, Z={-xm_minus:.2f}")
box_int.label(text=f" C= 0.0 : X=0.00, Z=0.00")
box_int.label(text=f" C=+{C_val:.1f} : X={xm_plus:.2f}, Z={-xm_plus:.2f}")
box_int.separator()
# [ Z = b*X ] 側
box_int.label(text=f"--- [ with Z = {b:.2f}X ] ---")
d = b - a
if abs(d) < 0.0001:
box_int.label(text=" Parallel")
else:
xb_minus = (-C_val) / d; xb_plus = C_val / d
box_int.label(text=f" C=-{C_val:.1f} : X={xb_minus:.2f}, Z={b*xb_minus:.2f}")
box_int.label(text=f" C= 0.0 : X=0.00, Z=0.00")
box_int.label(text=f" C=+{C_val:.1f} : X={xb_plus:.2f}, Z={b*xb_plus:.2f}")
box_int.operator(OT_CopyIntersectionInfo.bl_idname, icon='COPYDOWN', text="Copy Intersections")
col_exec = box.column(); col_exec.scale_y = 1.2
col_exec.operator(OT_CreateEquationCylinders.bl_idname, icon='MESH_CYLINDER', text="Create 6 Cylinders")
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_CreateEquationCylinders,
OT_CopyIntersectionInfo,
OT_CopyFullScript,
OT_Reset,
OT_OpenUrl,
OT_RemoveAddon,
PT_MainPanel,
PT_EquationCylindersPanel,
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))
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)
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except ValueError: pass
if __name__ == "__main__": register()
# Copied: 2026-03-24 21:03: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, 8, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": f"Multi-Shape Torus & Equation Cylinders - {PREFIX}",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
# ★ リンク設定
ADDON_LINKS = (
{"label": "時空図 交点 2060407", "url": "<https://www.notion.so/2060407-33af5dacaf43808d86bbf0d54d4d0dd5>"},
{"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,
"cyl_thickness": 0.5000,
"cyl_color": (0.1000, 0.6000, 0.9000, 1.0000),
"zx_color": (0.9000, 0.2000, 0.2000, 1.0000),
"minus_zx_color": (0.9000, 0.8000, 0.2000, 1.0000),
"custom_zx_color": (0.2000, 0.8000, 0.9000, 1.0000),
"cyl_slope_denom": 0.6000,
"cyl_offset": 6.0000,
"cyl_limit": 50.0000,
"cyl_custom_denom": -1.6000,
"show_cyl_group1": True,
"show_cyl_group2": True,
"show_cyl_group3": True,
"show_cyl_group4": True,
}
# <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)
# ==============================================================================
# ジオメトリ エンジン
# ==============================================================================
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
p = mathutils.Vector((a * math.cos(t), b * math.sin(t), 0))
n = mathutils.Vector((b * math.cos(t), a * math.sin(t), 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): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(major_segments):
try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % major_segments])
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
pts.append((mathutils.Vector((cx + actual_corner_radius * math.cos(angle), cy + actual_corner_radius * math.sin(angle), 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
ring.append(bm.verts.new(p + n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))))
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): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(total_rings):
try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % total_rings])
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
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()
bmesh.ops.transform(bm, matrix=mathutils.Vector((0,0,1)).rotation_difference(axis).to_matrix().to_4x4(), verts=geom['verts'])
bmesh.ops.translate(bm, verts=geom['verts'], vec=(v1 + v2) / 2.0)
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
# ==============================================================================
# 計算ロジック(直線の方程式をボックス制限内でクリッピング)
# ==============================================================================
def get_line_segment_in_bounds(M, C, limit):
pts = []
eps = 1e-4
z1 = M * (-limit) + C
if -limit - eps <= z1 <= limit + eps: pts.append((-limit, z1))
z2 = M * limit + C
if -limit - eps <= z2 <= limit + eps: pts.append((limit, z2))
if abs(M) > 1e-6:
x3 = (-limit - C) / M
if -limit - eps <= x3 <= limit + eps: pts.append((x3, -limit))
x4 = (limit - C) / M
if -limit - eps <= x4 <= limit + eps: pts.append((x4, limit))
unique_pts = []
for p in pts:
if not any(abs(p[0] - up[0]) < eps and abs(p[1] - up[1]) < eps for up in unique_pts):
unique_pts.append(p)
if len(unique_pts) >= 2: return unique_pts[0], unique_pts[1]
return None
def create_cylinder_line(M, C, limit, thickness, mat, target_collection):
pts = get_line_segment_in_bounds(M, C, limit)
if not pts: return None
p1_2d, p2_2d = pts
p1 = mathutils.Vector((p1_2d[0], 0.0, p1_2d[1]))
p2 = mathutils.Vector((p2_2d[0], 0.0, p2_2d[1]))
dist = (p2 - p1).length
if dist < 1e-4: return None
center = (p1 + p2) / 2.0
bm = bmesh.new()
geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=32, radius1=thickness, radius2=thickness, depth=dist)
axis = (p2 - p1).normalized()
up = mathutils.Vector((0, 0, 1))
bmesh.ops.transform(bm, matrix=up.rotation_difference(axis).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
mesh = bpy.data.meshes.new(f"EqCyl_Mesh")
bm.to_mesh(mesh)
bm.free(); apply_auto_smooth(mesh)
obj = bpy.data.objects.new(f"EqLine_{datetime.now().strftime('%H%M%S')}", mesh)
# ★ 指定されたコレクションにリンク
target_collection.objects.link(obj)
obj.data.materials.append(mat)
return obj
# ==============================================================================
# マテリアル・プレビュー制御
# ==============================================================================
def create_unique_material(color, name_prefix="Mat_UniqueShape"):
mat_name = f"{name_prefix}_{datetime.now().strftime('%M%S%f')[:5]}"
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")
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):
sx = min(max(props.size_x, 0.01), 10000.0); sy = min(max(props.size_y, 0.01), 10000.0)
mr = min(max(props.minor_radius, 0.001), 5000.0)
if props.base_shape == 'CUBE': create_cube_framework_bmesh(bm, sx, mr, props.minor_segments)
elif props.base_shape == 'SQUARE': create_square_torus_bmesh(bm, sx, props.corner_radius, mr, props.corner_segments, props.minor_segments)
elif props.base_shape == 'CIRCLE': create_ellipse_torus_bmesh(bm, sx, sx, mr, props.major_segments, props.minor_segments)
elif props.base_shape == 'ELLIPSE': create_ellipse_torus_bmesh(bm, sx, sy, mr, props.major_segments, props.minor_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:
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)
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)
# ★ 表示/非表示トグル用のアップデート関数
def update_cyl_visibility(self, context):
prefixes = {
'show_cyl_group1': "EqLine_1_over_aX_",
'show_cyl_group2': "EqLine_Z_eq_X_",
'show_cyl_group3': "EqLine_Z_eq_minus_X_",
'show_cyl_group4': "EqLine_Z_eq_custom_bX_"
}
for prop_name, prefix in prefixes.items():
is_visible = getattr(self, prop_name)
for obj in bpy.data.objects:
if obj.name.startswith(prefix):
obj.hide_viewport = not is_visible
obj.hide_render = not is_visible
# ==============================================================================
# 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", ""), ('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", 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)
cyl_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['cyl_thickness'], min=0.01, max=50.0)
cyl_color: FloatVectorProperty(name="Color (1/a*X ± C)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['cyl_color'])
zx_color: FloatVectorProperty(name="Color (Z=X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['zx_color'])
minus_zx_color: FloatVectorProperty(name="Color (Z=-X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['minus_zx_color'])
custom_zx_color: FloatVectorProperty(name="Color Custom", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['custom_zx_color'])
cyl_slope_denom: FloatProperty(name="Denominator (a)", default=CURRENT_DEFAULTS['cyl_slope_denom'], min=0.001)
cyl_offset: FloatProperty(name="Offset (C)", default=CURRENT_DEFAULTS['cyl_offset'], min=0.0)
cyl_limit: FloatProperty(name="Limit Bounds", default=CURRENT_DEFAULTS['cyl_limit'], min=1.0)
cyl_custom_denom: FloatProperty(name="Custom Denom (b)", default=CURRENT_DEFAULTS['cyl_custom_denom'])
show_cyl_group1: BoolProperty(name="Vis G1", default=CURRENT_DEFAULTS['show_cyl_group1'], update=update_cyl_visibility)
show_cyl_group2: BoolProperty(name="Vis G2", default=CURRENT_DEFAULTS['show_cyl_group2'], update=update_cyl_visibility)
show_cyl_group3: BoolProperty(name="Vis G3", default=CURRENT_DEFAULTS['show_cyl_group3'], update=update_cyl_visibility)
show_cyl_group4: BoolProperty(name="Vis G4", default=CURRENT_DEFAULTS['show_cyl_group4'], update=update_cyl_visibility)
# ==============================================================================
# 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")
# ★ 専用コレクションの作成
col_name = f"ShapeGroup_{datetime.now().strftime('%H%M%S')}"
new_col = bpy.data.collections.new(col_name)
context.scene.collection.children.link(new_col)
obj = bpy.data.objects.new(f"{prefix_name}_{datetime.now().strftime('%H%M%S')}", mesh)
new_col.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} in collection '{col_name}'!")
return {'FINISHED'}
class OT_CreateEquationCylinders(Operator):
bl_idname = f"{OP_PREFIX}.create_equation_cylinders"
bl_label = "Create 6 Cylinders"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
a = props.cyl_slope_denom; C_val = props.cyl_offset; limit = props.cyl_limit; thickness = props.cyl_thickness
b = props.cyl_custom_denom
if abs(a) < 0.0001:
self.report({'ERROR'}, "Denominator 'a' is too close to zero!")
return {'CANCELLED'}
# ★ 専用コレクションの作成
col_name = f"EqCylinders_{datetime.now().strftime('%H%M%S')}"
new_col = bpy.data.collections.new(col_name)
context.scene.collection.children.link(new_col)
M = 1.0 / a
mat_eq = create_unique_material(props.cyl_color, "Mat_EqLine")
mat_zx = create_unique_material(props.zx_color, "Mat_ZXLine")
mat_mzx = create_unique_material(props.minus_zx_color, "Mat_MinusZXLine")
mat_custom = create_unique_material(props.custom_zx_color, "Mat_CustomZXLine")
created_objs = []
# 1. Z = (1/a)X ± C (3 lines)
for C in [-C_val, 0.0, C_val]:
obj = create_cylinder_line(M, C, limit, thickness, mat_eq, new_col)
if obj:
obj.name = f"EqLine_1_over_aX_C{C}_{datetime.now().strftime('%H%M%S')}"
obj.hide_viewport = not props.show_cyl_group1
obj.hide_render = not props.show_cyl_group1
created_objs.append(obj)
# 2. Z = X (1 line)
obj_zx = create_cylinder_line(1.0, 0.0, limit, thickness, mat_zx, new_col)
if obj_zx:
obj_zx.name = f"EqLine_Z_eq_X_{datetime.now().strftime('%H%M%S')}"
obj_zx.hide_viewport = not props.show_cyl_group2
obj_zx.hide_render = not props.show_cyl_group2
created_objs.append(obj_zx)
# 3. Z = -X (1 line)
obj_mzx = create_cylinder_line(-1.0, 0.0, limit, thickness, mat_mzx, new_col)
if obj_mzx:
obj_mzx.name = f"EqLine_Z_eq_minus_X_{datetime.now().strftime('%H%M%S')}"
obj_mzx.hide_viewport = not props.show_cyl_group3
obj_mzx.hide_render = not props.show_cyl_group3
created_objs.append(obj_mzx)
# 4. Z = (1/b)X (1 line)
if abs(b) < 0.0001:
self.report({'WARNING'}, "Custom Denominator 'b' is zero. Skipped creating Custom Line.")
else:
obj_custom = create_cylinder_line(1.0 / b, 0.0, limit, thickness, mat_custom, new_col)
if obj_custom:
obj_custom.name = f"EqLine_Z_eq_custom_bX_{datetime.now().strftime('%H%M%S')}"
obj_custom.hide_viewport = not props.show_cyl_group4
obj_custom.hide_render = not props.show_cyl_group4
created_objs.append(obj_custom)
bpy.ops.object.select_all(action='DESELECT')
for obj in created_objs: obj.select_set(True)
if created_objs:
context.view_layer.objects.active = created_objs[-1]
self.report({'INFO'}, f"Created {len(created_objs)} Cylinders in collection '{col_name}'!")
else:
self.report({'WARNING'}, "No cylinders created. Lines might be entirely outside the bounds.")
return {'FINISHED'}
class OT_CopyIntersectionInfo(Operator):
bl_idname = f"{OP_PREFIX}.copy_intersection"
bl_label = "Copy Intersections"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
a = props.cyl_slope_denom; C_val = props.cyl_offset; b = props.cyl_custom_denom
text = "Intersection Points:\n"
text += f"Equations & Parameters:\n"
text += f" Base Lines : Z = (1/{a:.4f})*X ± {C_val:.4f}\n\n"
text += "[ Z = X and Base Lines ]\n"
if abs(1.0 - a * 1.0) < 0.0001: text += " Lines are parallel.\n"
else:
x_m = (a * (-C_val)) / (1.0 - a); x_p = (a * C_val) / (1.0 - a)
text += f" Line (C = -{C_val:.1f}): X = {x_m:.4f}, Z = {x_m:.4f}\n"
text += f" Line (C = 0.0): X = 0.0000, Z = 0.0000\n"
text += f" Line (C = +{C_val:.1f}): X = {x_p:.4f}, Z = {x_p:.4f}\n"
text += "\n[ Z = -X and Base Lines ]\n"
if abs(1.0 - a * (-1.0)) < 0.0001: text += " Lines are parallel.\n"
else:
xm_m = (a * (-C_val)) / (1.0 + a); xm_p = (a * C_val) / (1.0 + a)
text += f" Line (C = -{C_val:.1f}): X = {xm_m:.4f}, Z = {-xm_m:.4f}\n"
text += f" Line (C = 0.0): X = 0.0000, Z = 0.0000\n"
text += f" Line (C = +{C_val:.1f}): X = {xm_p:.4f}, Z = {-xm_p:.4f}\n"
text += f"\n[ Z = (1/{b:.4f})X and Base Lines ]\n"
d = a - b
if abs(b) < 0.0001: text += " Denominator 'b' is zero. Invalid.\n"
elif abs(d) < 0.0001: text += " Lines are parallel.\n"
else:
xb_m = (a * b * (-C_val)) / d; xb_p = (a * b * C_val) / d
text += f" Line (C = -{C_val:.1f}): X = {xb_m:.4f}, Z = {xb_m/b:.4f}\n"
text += f" Line (C = 0.0): X = 0.0000, Z = 0.0000\n"
text += f" Line (C = +{C_val:.1f}): X = {xb_p:.4f}, Z = {xb_p/b:.4f}\n"
context.window_manager.clipboard = text
self.report({'INFO'}, "Intersection Data Copied to Clipboard!")
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: return {'CANCELLED'}
code = target_text.as_string()
c, l, r = props.torus_color, props.torus_loc, props.torus_rot
cc, zc, mzc, czc = props.cyl_color, props.zx_color, props.minus_zx_color, props.custom_zx_color
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'
new_dict += f' "cyl_thickness": {props.cyl_thickness:.4f},\n'
new_dict += f' "cyl_color": ({cc[0]:.4f}, {cc[1]:.4f}, {cc[2]:.4f}, {cc[3]:.4f}),\n'
new_dict += f' "zx_color": ({zc[0]:.4f}, {zc[1]:.4f}, {zc[2]:.4f}, {zc[3]:.4f}),\n'
new_dict += f' "minus_zx_color": ({mzc[0]:.4f}, {mzc[1]:.4f}, {mzc[2]:.4f}, {mzc[3]:.4f}),\n'
new_dict += f' "custom_zx_color": ({czc[0]:.4f}, {czc[1]:.4f}, {czc[2]:.4f}, {czc[3]:.4f}),\n'
new_dict += f' "cyl_slope_denom": {props.cyl_slope_denom:.4f},\n'
new_dict += f' "cyl_offset": {props.cyl_offset:.4f},\n'
new_dict += f' "cyl_limit": {props.cyl_limit:.4f},\n'
new_dict += f' "cyl_custom_denom": {props.cyl_custom_denom:.4f},\n'
new_dict += f' "show_cyl_group1": {props.show_cyl_group1},\n'
new_dict += f' "show_cyl_group2": {props.show_cyl_group2},\n'
new_dict += f' "show_cyl_group3": {props.show_cyl_group3},\n'
new_dict += f' "show_cyl_group4": {props.show_cyl_group4},\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: 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: 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: 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
p.cyl_thickness = 0.5; p.cyl_color = (0.1, 0.6, 0.9, 1.0)
p.zx_color = (0.9, 0.2, 0.2, 1.0); p.minus_zx_color = (0.9, 0.8, 0.2, 1.0)
p.custom_zx_color = (0.2, 0.8, 0.9, 1.0)
p.cyl_slope_denom = 0.6; p.cyl_offset = 10.0; p.cyl_limit = 50.0; p.cyl_custom_denom = -1.6
p.show_cyl_group1 = True; p.show_cyl_group2 = True
p.show_cyl_group3 = True; p.show_cyl_group4 = True
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]")
box.row().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_EquationCylindersPanel(Panel):
bl_label = "Equation Cylinders (6 Lines)"
bl_idname = f"{PREFIX}_PT_eq_cylinders"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
a = props.cyl_slope_denom
C_val = props.cyl_offset
b = props.cyl_custom_denom
box = layout.box()
box.label(text="Math Equations (Y=0 Plane):", icon='FILE_TEXT')
box.label(text=f" G1: Z = ( 1 / {a:.2f} ) * X ± {C_val:.2f}")
box.label(text=f" G2: Z = X")
box.label(text=f" G3: Z = -X")
box.label(text=f" G4: Z = ( 1 / {b:.2f} ) * X")
box.separator()
col = box.column(align=True)
col.prop(props, "cyl_slope_denom", text="Denominator (a)")
col.prop(props, "cyl_offset", text="Offset (C)")
col.prop(props, "cyl_limit", text="Limit Bounds (-L to L)")
col.prop(props, "cyl_custom_denom", text="Custom Denom (b)")
box.separator()
box.prop(props, "cyl_thickness", text="Thickness")
# ★ カラー設定 & 表示トグル
col_c = box.column(align=True)
r1 = col_c.row(align=True)
r1.prop(props, "show_cyl_group1", text="", icon='HIDE_OFF' if props.show_cyl_group1 else 'HIDE_ON')
r1.prop(props, "cyl_color", text=f"Z=(1/{a:.2f})X ± {C_val:.1f}")
r2 = col_c.row(align=True)
r2.prop(props, "show_cyl_group2", text="", icon='HIDE_OFF' if props.show_cyl_group2 else 'HIDE_ON')
r2.prop(props, "zx_color", text="Z = X")
r3 = col_c.row(align=True)
r3.prop(props, "show_cyl_group3", text="", icon='HIDE_OFF' if props.show_cyl_group3 else 'HIDE_ON')
r3.prop(props, "minus_zx_color", text="Z = -X")
r4 = col_c.row(align=True)
r4.prop(props, "show_cyl_group4", text="", icon='HIDE_OFF' if props.show_cyl_group4 else 'HIDE_ON')
r4.prop(props, "custom_zx_color", text=f"Z = (1/{b:.2f})X")
# ★ 交点表示エリア (縦1行ずつ表示)
box.separator()
box_int = box.box()
box_int.label(text=f"Intersections with Z=(1/{a:.2f})X ± {C_val:.1f} :", icon='DRIVER')
# [ Z = X ] 側
box_int.label(text="--- [ with Z = X ] ---")
if abs(1.0 - a * 1.0) < 0.0001:
box_int.label(text=" Parallel")
else:
x_minus = (a * (-C_val)) / (1.0 - a); x_plus = (a * C_val) / (1.0 - a)
box_int.label(text=f" C=-{C_val:.1f} : X={x_minus:.2f}, Z={x_minus:.2f}")
box_int.label(text=f" C= 0.0 : X=0.00, Z=0.00")
box_int.label(text=f" C=+{C_val:.1f} : X={x_plus:.2f}, Z={x_plus:.2f}")
box_int.separator()
# [ Z = -X ] 側
box_int.label(text="--- [ with Z = -X ] ---")
if abs(1.0 - a * (-1.0)) < 0.0001:
box_int.label(text=" Parallel")
else:
xm_minus = (a * (-C_val)) / (1.0 + a); xm_plus = (a * C_val) / (1.0 + a)
box_int.label(text=f" C=-{C_val:.1f} : X={xm_minus:.2f}, Z={-xm_minus:.2f}")
box_int.label(text=f" C= 0.0 : X=0.00, Z=0.00")
box_int.label(text=f" C=+{C_val:.1f} : X={xm_plus:.2f}, Z={-xm_plus:.2f}")
box_int.separator()
# [ Z = (1/b)X ] 側
box_int.label(text=f"--- [ with Z = (1/{b:.2f})X ] ---")
d = a - b
if abs(b) < 0.0001:
box_int.label(text=" Invalid (b=0)")
elif abs(d) < 0.0001:
box_int.label(text=" Parallel")
else:
xb_minus = (a * b * (-C_val)) / d; xb_plus = (a * b * C_val) / d
box_int.label(text=f" C=-{C_val:.1f} : X={xb_minus:.2f}, Z={xb_minus/b:.2f}")
box_int.label(text=f" C= 0.0 : X=0.00, Z=0.00")
box_int.label(text=f" C=+{C_val:.1f} : X={xb_plus:.2f}, Z={xb_plus/b:.2f}")
box_int.operator(OT_CopyIntersectionInfo.bl_idname, icon='COPYDOWN', text="Copy Intersections")
col_exec = box.column(); col_exec.scale_y = 1.2
col_exec.operator(OT_CreateEquationCylinders.bl_idname, icon='MESH_CYLINDER', text="Create 6 Cylinders")
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_CreateEquationCylinders,
OT_CopyIntersectionInfo,
OT_CopyFullScript,
OT_Reset,
OT_OpenUrl,
OT_RemoveAddon,
PT_MainPanel,
PT_EquationCylindersPanel,
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))
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)
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except ValueError: pass
if __name__ == "__main__": register()
# Copied: 2026-03-24 21:03: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, 7, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": f"Multi-Shape Torus & Equation Cylinders - {PREFIX}",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
# ★ リンク設定
ADDON_LINKS = (
{"label": "時空図 交点 2060407", "url": "<https://www.notion.so/2060407-33af5dacaf43808d86bbf0d54d4d0dd5>"},
{"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,
"cyl_thickness": 0.5000,
"cyl_color": (0.1000, 0.6000, 0.9000, 1.0000),
"zx_color": (0.9000, 0.2000, 0.2000, 1.0000),
"minus_zx_color": (0.9000, 0.8000, 0.2000, 1.0000),
"custom_zx_color": (0.2000, 0.8000, 0.9000, 1.0000),
"cyl_slope_denom": 0.6000,
"cyl_offset": 10.0000,
"cyl_limit": 50.0000,
"cyl_custom_denom": -1.6000,
"show_cyl_group1": True,
"show_cyl_group2": True,
"show_cyl_group3": True,
"show_cyl_group4": True,
}
# <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)
# ==============================================================================
# ジオメトリ エンジン
# ==============================================================================
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
p = mathutils.Vector((a * math.cos(t), b * math.sin(t), 0))
n = mathutils.Vector((b * math.cos(t), a * math.sin(t), 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): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(major_segments):
try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % major_segments])
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
pts.append((mathutils.Vector((cx + actual_corner_radius * math.cos(angle), cy + actual_corner_radius * math.sin(angle), 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
ring.append(bm.verts.new(p + n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))))
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): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(total_rings):
try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % total_rings])
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
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()
bmesh.ops.transform(bm, matrix=mathutils.Vector((0,0,1)).rotation_difference(axis).to_matrix().to_4x4(), verts=geom['verts'])
bmesh.ops.translate(bm, verts=geom['verts'], vec=(v1 + v2) / 2.0)
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
# ==============================================================================
# 計算ロジック(直線の方程式をボックス制限内でクリッピング)
# ==============================================================================
def get_line_segment_in_bounds(M, C, limit):
pts = []
eps = 1e-4
z1 = M * (-limit) + C
if -limit - eps <= z1 <= limit + eps: pts.append((-limit, z1))
z2 = M * limit + C
if -limit - eps <= z2 <= limit + eps: pts.append((limit, z2))
if abs(M) > 1e-6:
x3 = (-limit - C) / M
if -limit - eps <= x3 <= limit + eps: pts.append((x3, -limit))
x4 = (limit - C) / M
if -limit - eps <= x4 <= limit + eps: pts.append((x4, limit))
unique_pts = []
for p in pts:
if not any(abs(p[0] - up[0]) < eps and abs(p[1] - up[1]) < eps for up in unique_pts):
unique_pts.append(p)
if len(unique_pts) >= 2: return unique_pts[0], unique_pts[1]
return None
def create_cylinder_line(M, C, limit, thickness, mat, context):
pts = get_line_segment_in_bounds(M, C, limit)
if not pts: return None
p1_2d, p2_2d = pts
p1 = mathutils.Vector((p1_2d[0], 0.0, p1_2d[1]))
p2 = mathutils.Vector((p2_2d[0], 0.0, p2_2d[1]))
dist = (p2 - p1).length
if dist < 1e-4: return None
center = (p1 + p2) / 2.0
bm = bmesh.new()
geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=32, radius1=thickness, radius2=thickness, depth=dist)
axis = (p2 - p1).normalized()
up = mathutils.Vector((0, 0, 1))
bmesh.ops.transform(bm, matrix=up.rotation_difference(axis).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
mesh = bpy.data.meshes.new(f"EqCyl_Mesh")
bm.to_mesh(mesh)
bm.free(); apply_auto_smooth(mesh)
obj = bpy.data.objects.new(f"EqLine_{datetime.now().strftime('%H%M%S')}", mesh)
if context.collection: context.collection.objects.link(obj)
else: context.scene.collection.objects.link(obj)
obj.data.materials.append(mat)
return obj
# ==============================================================================
# マテリアル・プレビュー制御
# ==============================================================================
def create_unique_material(color, name_prefix="Mat_UniqueShape"):
mat_name = f"{name_prefix}_{datetime.now().strftime('%M%S%f')[:5]}"
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")
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):
sx = min(max(props.size_x, 0.01), 10000.0); sy = min(max(props.size_y, 0.01), 10000.0)
mr = min(max(props.minor_radius, 0.001), 5000.0)
if props.base_shape == 'CUBE': create_cube_framework_bmesh(bm, sx, mr, props.minor_segments)
elif props.base_shape == 'SQUARE': create_square_torus_bmesh(bm, sx, props.corner_radius, mr, props.corner_segments, props.minor_segments)
elif props.base_shape == 'CIRCLE': create_ellipse_torus_bmesh(bm, sx, sx, mr, props.major_segments, props.minor_segments)
elif props.base_shape == 'ELLIPSE': create_ellipse_torus_bmesh(bm, sx, sy, mr, props.major_segments, props.minor_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:
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)
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)
# ★ 表示/非表示トグル用のアップデート関数
def update_cyl_visibility(self, context):
prefixes = {
'show_cyl_group1': "EqLine_1_over_aX_",
'show_cyl_group2': "EqLine_Z_eq_X_",
'show_cyl_group3': "EqLine_Z_eq_minus_X_",
'show_cyl_group4': "EqLine_Z_eq_custom_bX_"
}
for prop_name, prefix in prefixes.items():
is_visible = getattr(self, prop_name)
for obj in bpy.data.objects:
if obj.name.startswith(prefix):
obj.hide_viewport = not is_visible
obj.hide_render = not is_visible
# ==============================================================================
# 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", ""), ('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", 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)
cyl_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['cyl_thickness'], min=0.01, max=50.0)
cyl_color: FloatVectorProperty(name="Color (1/a*X ± C)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['cyl_color'])
zx_color: FloatVectorProperty(name="Color (Z=X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['zx_color'])
minus_zx_color: FloatVectorProperty(name="Color (Z=-X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['minus_zx_color'])
custom_zx_color: FloatVectorProperty(name="Color Custom", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['custom_zx_color'])
cyl_slope_denom: FloatProperty(name="Denominator (a)", default=CURRENT_DEFAULTS['cyl_slope_denom'], min=0.001)
cyl_offset: FloatProperty(name="Offset (C)", default=CURRENT_DEFAULTS['cyl_offset'], min=0.0)
cyl_limit: FloatProperty(name="Limit Bounds", default=CURRENT_DEFAULTS['cyl_limit'], min=1.0)
cyl_custom_denom: FloatProperty(name="Custom Denom (b)", default=CURRENT_DEFAULTS['cyl_custom_denom'])
show_cyl_group1: BoolProperty(name="Vis G1", default=CURRENT_DEFAULTS['show_cyl_group1'], update=update_cyl_visibility)
show_cyl_group2: BoolProperty(name="Vis G2", default=CURRENT_DEFAULTS['show_cyl_group2'], update=update_cyl_visibility)
show_cyl_group3: BoolProperty(name="Vis G3", default=CURRENT_DEFAULTS['show_cyl_group3'], update=update_cyl_visibility)
show_cyl_group4: BoolProperty(name="Vis G4", default=CURRENT_DEFAULTS['show_cyl_group4'], update=update_cyl_visibility)
# ==============================================================================
# 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_CreateEquationCylinders(Operator):
bl_idname = f"{OP_PREFIX}.create_equation_cylinders"
bl_label = "Create 6 Cylinders"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
a = props.cyl_slope_denom; C_val = props.cyl_offset; limit = props.cyl_limit; thickness = props.cyl_thickness
b = props.cyl_custom_denom
if abs(a) < 0.0001:
self.report({'ERROR'}, "Denominator 'a' is too close to zero!")
return {'CANCELLED'}
M = 1.0 / a
mat_eq = create_unique_material(props.cyl_color, "Mat_EqLine")
mat_zx = create_unique_material(props.zx_color, "Mat_ZXLine")
mat_mzx = create_unique_material(props.minus_zx_color, "Mat_MinusZXLine")
mat_custom = create_unique_material(props.custom_zx_color, "Mat_CustomZXLine")
created_objs = []
# 1. Z = (1/a)X ± C (3 lines)
for C in [-C_val, 0.0, C_val]:
obj = create_cylinder_line(M, C, limit, thickness, mat_eq, context)
if obj:
obj.name = f"EqLine_1_over_aX_C{C}_{datetime.now().strftime('%H%M%S')}"
obj.hide_viewport = not props.show_cyl_group1
obj.hide_render = not props.show_cyl_group1
created_objs.append(obj)
# 2. Z = X (1 line)
obj_zx = create_cylinder_line(1.0, 0.0, limit, thickness, mat_zx, context)
if obj_zx:
obj_zx.name = f"EqLine_Z_eq_X_{datetime.now().strftime('%H%M%S')}"
obj_zx.hide_viewport = not props.show_cyl_group2
obj_zx.hide_render = not props.show_cyl_group2
created_objs.append(obj_zx)
# 3. Z = -X (1 line)
obj_mzx = create_cylinder_line(-1.0, 0.0, limit, thickness, mat_mzx, context)
if obj_mzx:
obj_mzx.name = f"EqLine_Z_eq_minus_X_{datetime.now().strftime('%H%M%S')}"
obj_mzx.hide_viewport = not props.show_cyl_group3
obj_mzx.hide_render = not props.show_cyl_group3
created_objs.append(obj_mzx)
# 4. Z = (1/b)X (1 line)
if abs(b) < 0.0001:
self.report({'WARNING'}, "Custom Denominator 'b' is zero. Skipped creating Custom Line.")
else:
obj_custom = create_cylinder_line(1.0 / b, 0.0, limit, thickness, mat_custom, context)
if obj_custom:
obj_custom.name = f"EqLine_Z_eq_custom_bX_{datetime.now().strftime('%H%M%S')}"
obj_custom.hide_viewport = not props.show_cyl_group4
obj_custom.hide_render = not props.show_cyl_group4
created_objs.append(obj_custom)
bpy.ops.object.select_all(action='DESELECT')
for obj in created_objs: obj.select_set(True)
if created_objs:
context.view_layer.objects.active = created_objs[-1]
self.report({'INFO'}, f"Created {len(created_objs)} Cylinders Successfully!")
else:
self.report({'WARNING'}, "No cylinders created. Lines might be entirely outside the bounds.")
return {'FINISHED'}
class OT_CopyIntersectionInfo(Operator):
bl_idname = f"{OP_PREFIX}.copy_intersection"
bl_label = "Copy Intersections"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
a = props.cyl_slope_denom; C_val = props.cyl_offset; b = props.cyl_custom_denom
text = "Intersection Points:\n"
text += f"Equations & Parameters:\n"
text += f" Base Lines : Z = (1/{a:.4f})*X ± {C_val:.4f}\n\n"
text += "[ Z = X and Base Lines ]\n"
if abs(1.0 - a * 1.0) < 0.0001: text += " Lines are parallel.\n"
else:
x_m = (a * (-C_val)) / (1.0 - a); x_p = (a * C_val) / (1.0 - a)
text += f" Line (C = -{C_val:.1f}): X = {x_m:.4f}, Z = {x_m:.4f}\n"
text += f" Line (C = 0.0): X = 0.0000, Z = 0.0000\n"
text += f" Line (C = +{C_val:.1f}): X = {x_p:.4f}, Z = {x_p:.4f}\n"
text += "\n[ Z = -X and Base Lines ]\n"
if abs(1.0 - a * (-1.0)) < 0.0001: text += " Lines are parallel.\n"
else:
xm_m = (a * (-C_val)) / (1.0 + a); xm_p = (a * C_val) / (1.0 + a)
text += f" Line (C = -{C_val:.1f}): X = {xm_m:.4f}, Z = {-xm_m:.4f}\n"
text += f" Line (C = 0.0): X = 0.0000, Z = 0.0000\n"
text += f" Line (C = +{C_val:.1f}): X = {xm_p:.4f}, Z = {-xm_p:.4f}\n"
text += f"\n[ Z = (1/{b:.4f})X and Base Lines ]\n"
d = a - b
if abs(b) < 0.0001: text += " Denominator 'b' is zero. Invalid.\n"
elif abs(d) < 0.0001: text += " Lines are parallel.\n"
else:
xb_m = (a * b * (-C_val)) / d; xb_p = (a * b * C_val) / d
text += f" Line (C = -{C_val:.1f}): X = {xb_m:.4f}, Z = {xb_m/b:.4f}\n"
text += f" Line (C = 0.0): X = 0.0000, Z = 0.0000\n"
text += f" Line (C = +{C_val:.1f}): X = {xb_p:.4f}, Z = {xb_p/b:.4f}\n"
context.window_manager.clipboard = text
self.report({'INFO'}, "Intersection Data Copied to Clipboard!")
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: return {'CANCELLED'}
code = target_text.as_string()
c, l, r = props.torus_color, props.torus_loc, props.torus_rot
cc, zc, mzc, czc = props.cyl_color, props.zx_color, props.minus_zx_color, props.custom_zx_color
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'
new_dict += f' "cyl_thickness": {props.cyl_thickness:.4f},\n'
new_dict += f' "cyl_color": ({cc[0]:.4f}, {cc[1]:.4f}, {cc[2]:.4f}, {cc[3]:.4f}),\n'
new_dict += f' "zx_color": ({zc[0]:.4f}, {zc[1]:.4f}, {zc[2]:.4f}, {zc[3]:.4f}),\n'
new_dict += f' "minus_zx_color": ({mzc[0]:.4f}, {mzc[1]:.4f}, {mzc[2]:.4f}, {mzc[3]:.4f}),\n'
new_dict += f' "custom_zx_color": ({czc[0]:.4f}, {czc[1]:.4f}, {czc[2]:.4f}, {czc[3]:.4f}),\n'
new_dict += f' "cyl_slope_denom": {props.cyl_slope_denom:.4f},\n'
new_dict += f' "cyl_offset": {props.cyl_offset:.4f},\n'
new_dict += f' "cyl_limit": {props.cyl_limit:.4f},\n'
new_dict += f' "cyl_custom_denom": {props.cyl_custom_denom:.4f},\n'
new_dict += f' "show_cyl_group1": {props.show_cyl_group1},\n'
new_dict += f' "show_cyl_group2": {props.show_cyl_group2},\n'
new_dict += f' "show_cyl_group3": {props.show_cyl_group3},\n'
new_dict += f' "show_cyl_group4": {props.show_cyl_group4},\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: 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: 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: 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
p.cyl_thickness = 0.5; p.cyl_color = (0.1, 0.6, 0.9, 1.0)
p.zx_color = (0.9, 0.2, 0.2, 1.0); p.minus_zx_color = (0.9, 0.8, 0.2, 1.0)
p.custom_zx_color = (0.2, 0.8, 0.9, 1.0)
p.cyl_slope_denom = 0.6; p.cyl_offset = 10.0; p.cyl_limit = 50.0; p.cyl_custom_denom = -1.6
p.show_cyl_group1 = True; p.show_cyl_group2 = True
p.show_cyl_group3 = True; p.show_cyl_group4 = True
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]")
box.row().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_EquationCylindersPanel(Panel):
bl_label = "Equation Cylinders (6 Lines)"
bl_idname = f"{PREFIX}_PT_eq_cylinders"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
a = props.cyl_slope_denom
C_val = props.cyl_offset
b = props.cyl_custom_denom
box = layout.box()
box.label(text="Math Equations (Y=0 Plane):", icon='FILE_TEXT')
box.label(text=f" G1: Z = ( 1 / {a:.2f} ) * X ± {C_val:.2f}")
box.label(text=f" G2: Z = X")
box.label(text=f" G3: Z = -X")
box.label(text=f" G4: Z = ( 1 / {b:.2f} ) * X")
box.separator()
col = box.column(align=True)
col.prop(props, "cyl_slope_denom", text="Denominator (a)")
col.prop(props, "cyl_offset", text="Offset (C)")
col.prop(props, "cyl_limit", text="Limit Bounds (-L to L)")
col.prop(props, "cyl_custom_denom", text="Custom Denom (b)")
box.separator()
box.prop(props, "cyl_thickness", text="Thickness")
# ★ カラー設定 & 表示トグル
col_c = box.column(align=True)
r1 = col_c.row(align=True)
r1.prop(props, "show_cyl_group1", text="", icon='HIDE_OFF' if props.show_cyl_group1 else 'HIDE_ON')
r1.prop(props, "cyl_color", text=f"Z=(1/{a:.2f})X ± {C_val:.1f}")
r2 = col_c.row(align=True)
r2.prop(props, "show_cyl_group2", text="", icon='HIDE_OFF' if props.show_cyl_group2 else 'HIDE_ON')
r2.prop(props, "zx_color", text="Z = X")
r3 = col_c.row(align=True)
r3.prop(props, "show_cyl_group3", text="", icon='HIDE_OFF' if props.show_cyl_group3 else 'HIDE_ON')
r3.prop(props, "minus_zx_color", text="Z = -X")
r4 = col_c.row(align=True)
r4.prop(props, "show_cyl_group4", text="", icon='HIDE_OFF' if props.show_cyl_group4 else 'HIDE_ON')
r4.prop(props, "custom_zx_color", text=f"Z = (1/{b:.2f})X")
# ★ 交点表示エリア
box.separator()
box_int = box.box()
box_int.label(text=f"Intersections with Z=(1/{a:.2f})X ± {C_val:.1f} :", icon='DRIVER')
col_pts = box_int.column()
row1 = col_pts.row(); col1 = row1.column(); col2 = row1.column()
row2 = col_pts.row(); col3 = row2.column(); col4 = row2.column()
# [ Z = X ] 側
col1.label(text="[ with Z = X ]")
if abs(1.0 - a * 1.0) < 0.0001: col1.label(text="Parallel")
else:
x_minus = (a * (-C_val)) / (1.0 - a); x_plus = (a * C_val) / (1.0 - a)
col1.label(text=f"C=-{C_val:.1f} : ({x_minus:.1f}, {x_minus:.1f})")
col1.label(text=f"C= 0.0 : (0.0, 0.0)")
col1.label(text=f"C=+{C_val:.1f} : ({x_plus:.1f}, {x_plus:.1f})")
# [ Z = -X ] 側
col2.label(text="[ with Z = -X ]")
if abs(1.0 - a * (-1.0)) < 0.0001: col2.label(text="Parallel")
else:
xm_minus = (a * (-C_val)) / (1.0 + a); xm_plus = (a * C_val) / (1.0 + a)
col2.label(text=f"C=-{C_val:.1f} : ({xm_minus:.1f}, {-xm_minus:.1f})")
col2.label(text=f"C= 0.0 : (0.0, 0.0)")
col2.label(text=f"C=+{C_val:.1f} : ({xm_plus:.1f}, {-xm_plus:.1f})")
# [ Z = (1/b)X ] 側
col3.label(text=f"[ with Z = (1/{b:.2f})X ]")
d = a - b
if abs(b) < 0.0001: col3.label(text="Invalid (b=0)")
elif abs(d) < 0.0001: col3.label(text="Parallel")
else:
xb_minus = (a * b * (-C_val)) / d; xb_plus = (a * b * C_val) / d
col3.label(text=f"C=-{C_val:.1f} : ({xb_minus:.1f}, {xb_minus/b:.1f})")
col3.label(text=f"C= 0.0 : (0.0, 0.0)")
col3.label(text=f"C=+{C_val:.1f} : ({xb_plus:.1f}, {xb_plus/b:.1f})")
box_int.operator(OT_CopyIntersectionInfo.bl_idname, icon='COPYDOWN', text="Copy Intersections")
col_exec = box.column(); col_exec.scale_y = 1.2
col_exec.operator(OT_CreateEquationCylinders.bl_idname, icon='MESH_CYLINDER', text="Create 6 Cylinders")
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_CreateEquationCylinders,
OT_CopyIntersectionInfo,
OT_CopyFullScript,
OT_Reset,
OT_OpenUrl,
OT_RemoveAddon,
PT_MainPanel,
PT_EquationCylindersPanel,
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))
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)
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except ValueError: pass
if __name__ == "__main__": register()
Intersection Points:
Parameters: a = 0.6000, C = 10.0000
[ Z = X and Z = (1/a)*X + C ]
Line (C = -10.0): X = -15.0000, Z = -15.0000
Line (C = 0.0): X = 0.0000, Z = 0.0000
Line (C = +10.0): X = 15.0000, Z = 15.0000
[ Z = -X and Z = (1/a)*X + C ]
Line (C = -10.0): X = -3.7500, Z = 3.7500
Line (C = 0.0): X = 0.0000, Z = 0.0000
Line (C = +10.0): X = 3.7500, Z = -3.7500
[ Z = -1.60X and Z = (1/a)*X + C ]
Line (C = -10.0): X = -3.0612, Z = 4.8980
Line (C = 0.0): X = 0.0000, Z = 0.0000
Line (C = +10.0): X = 3.0612, Z = -4.8980
# Copied: 2026-03-24 21:03: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, 4, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": f"Multi-Shape Torus & Equation Cylinders - {PREFIX}",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
# ★ リンク設定
ADDON_LINKS = (
{"label": "時空図 交点 2060407", "url": "<https://www.notion.so/2060407-33af5dacaf43808d86bbf0d54d4d0dd5>"},
{"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,
"cyl_thickness": 0.5000,
"cyl_color": (0.1000, 0.6000, 0.9000, 1.0000),
"zx_color": (0.9000, 0.2000, 0.2000, 1.0000),
"minus_zx_color": (0.9000, 0.8000, 0.2000, 1.0000),
"cyl_slope_denom": 0.6000,
"cyl_offset": 10.0000,
"cyl_limit": 50.0000,
}
# <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)
# ==============================================================================
# ジオメトリ エンジン
# ==============================================================================
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
p = mathutils.Vector((a * math.cos(t), b * math.sin(t), 0))
n = mathutils.Vector((b * math.cos(t), a * math.sin(t), 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): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(major_segments):
try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % major_segments])
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
pts.append((mathutils.Vector((cx + actual_corner_radius * math.cos(angle), cy + actual_corner_radius * math.sin(angle), 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
ring.append(bm.verts.new(p + n * (minor_radius * math.cos(theta)) + b * (minor_radius * math.sin(theta))))
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): edges.append(bm.edges.new((ring[j], ring[(j + 1) % minor_segments])))
edge_loops.append(edges)
bm.edges.ensure_lookup_table()
for i in range(total_rings):
try: bmesh.ops.bridge_loops(bm, edges=edge_loops[i] + edge_loops[(i + 1) % total_rings])
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
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()
bmesh.ops.transform(bm, matrix=mathutils.Vector((0,0,1)).rotation_difference(axis).to_matrix().to_4x4(), verts=geom['verts'])
bmesh.ops.translate(bm, verts=geom['verts'], vec=(v1 + v2) / 2.0)
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
# ==============================================================================
# 計算ロジック(直線の方程式をボックス制限内でクリッピング)
# ==============================================================================
def get_line_segment_in_bounds(M, C, limit):
pts = []
eps = 1e-4
z1 = M * (-limit) + C
if -limit - eps <= z1 <= limit + eps: pts.append((-limit, z1))
z2 = M * limit + C
if -limit - eps <= z2 <= limit + eps: pts.append((limit, z2))
if abs(M) > 1e-6:
x3 = (-limit - C) / M
if -limit - eps <= x3 <= limit + eps: pts.append((x3, -limit))
if abs(M) > 1e-6:
x4 = (limit - C) / M
if -limit - eps <= x4 <= limit + eps: pts.append((x4, limit))
unique_pts = []
for p in pts:
if not any(abs(p[0] - up[0]) < eps and abs(p[1] - up[1]) < eps for up in unique_pts):
unique_pts.append(p)
if len(unique_pts) >= 2:
return unique_pts[0], unique_pts[1]
return None
def create_cylinder_line(M, C, limit, thickness, mat, context):
pts = get_line_segment_in_bounds(M, C, limit)
if not pts: return None
p1_2d, p2_2d = pts
p1 = mathutils.Vector((p1_2d[0], 0.0, p1_2d[1]))
p2 = mathutils.Vector((p2_2d[0], 0.0, p2_2d[1]))
dist = (p2 - p1).length
if dist < 1e-4: return None
center = (p1 + p2) / 2.0
bm = bmesh.new()
geom = bmesh.ops.create_cone(
bm, cap_ends=True, cap_tris=False, segments=32,
radius1=thickness, radius2=thickness, depth=dist
)
axis = (p2 - p1).normalized()
up = mathutils.Vector((0, 0, 1))
rot = up.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
mesh = bpy.data.meshes.new(f"EqCyl_Mesh")
bm.to_mesh(mesh)
bm.free()
apply_auto_smooth(mesh)
name_label = f"EqLine_{datetime.now().strftime('%H%M%S')}"
obj = bpy.data.objects.new(name_label, mesh)
if context.collection: context.collection.objects.link(obj)
else: context.scene.collection.objects.link(obj)
obj.data.materials.append(mat)
return obj
# ==============================================================================
# マテリアル・プレビュー制御
# ==============================================================================
def create_unique_material(color, name_prefix="Mat_UniqueShape"):
mat_name = f"{name_prefix}_{datetime.now().strftime('%M%S%f')[:5]}"
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")
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):
sx = min(max(props.size_x, 0.01), 10000.0)
sy = min(max(props.size_y, 0.01), 10000.0)
mr = min(max(props.minor_radius, 0.001), 5000.0)
if props.base_shape == 'CUBE': create_cube_framework_bmesh(bm, sx, mr, props.minor_segments)
elif props.base_shape == 'SQUARE': create_square_torus_bmesh(bm, sx, props.corner_radius, mr, props.corner_segments, props.minor_segments)
elif props.base_shape == 'CIRCLE': create_ellipse_torus_bmesh(bm, sx, sx, mr, props.major_segments, props.minor_segments)
elif props.base_shape == 'ELLIPSE': create_ellipse_torus_bmesh(bm, sx, sy, mr, props.major_segments, props.minor_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:
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)
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", ""), ('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", 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)
cyl_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['cyl_thickness'], min=0.01, max=50.0)
cyl_color: FloatVectorProperty(name="Color (a*X+C)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['cyl_color'])
zx_color: FloatVectorProperty(name="Color (Z=X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['zx_color'])
minus_zx_color: FloatVectorProperty(name="Color (Z=-X)", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['minus_zx_color'])
cyl_slope_denom: FloatProperty(name="Denominator (a)", default=CURRENT_DEFAULTS['cyl_slope_denom'], min=0.001)
cyl_offset: FloatProperty(name="Offset (C)", default=CURRENT_DEFAULTS['cyl_offset'], min=0.0)
cyl_limit: FloatProperty(name="Limit Bounds", default=CURRENT_DEFAULTS['cyl_limit'], min=1.0)
# ==============================================================================
# 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_CreateEquationCylinders(Operator):
bl_idname = f"{OP_PREFIX}.create_equation_cylinders"
bl_label = "Create 5 Cylinders"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
a = props.cyl_slope_denom; C_val = props.cyl_offset; limit = props.cyl_limit; thickness = props.cyl_thickness
if abs(a) < 0.0001:
self.report({'ERROR'}, "Denominator 'a' is too close to zero!")
return {'CANCELLED'}
M = 1.0 / a
mat_eq = create_unique_material(props.cyl_color, "Mat_EqLine")
mat_zx = create_unique_material(props.zx_color, "Mat_ZXLine")
mat_mzx = create_unique_material(props.minus_zx_color, "Mat_MinusZXLine")
created_objs = []
# 1. Z = (1/a)X ± C (3 lines)
for C in [-C_val, 0.0, C_val]:
obj = create_cylinder_line(M, C, limit, thickness, mat_eq, context)
if obj:
obj.name = f"EqLine_1_over_aX_C{C}_{datetime.now().strftime('%H%M%S')}"
created_objs.append(obj)
# 2. Z = X (1 line)
obj_zx = create_cylinder_line(1.0, 0.0, limit, thickness, mat_zx, context)
if obj_zx:
obj_zx.name = f"EqLine_Z_eq_X_{datetime.now().strftime('%H%M%S')}"
created_objs.append(obj_zx)
# 3. Z = -X (1 line)
obj_mzx = create_cylinder_line(-1.0, 0.0, limit, thickness, mat_mzx, context)
if obj_mzx:
obj_mzx.name = f"EqLine_Z_eq_minus_X_{datetime.now().strftime('%H%M%S')}"
created_objs.append(obj_mzx)
bpy.ops.object.select_all(action='DESELECT')
for obj in created_objs: obj.select_set(True)
if created_objs:
context.view_layer.objects.active = created_objs[-1]
self.report({'INFO'}, f"Created {len(created_objs)} Cylinders Successfully!")
else:
self.report({'WARNING'}, "No cylinders created. Lines might be entirely outside the bounds.")
return {'FINISHED'}
class OT_CopyIntersectionInfo(Operator):
bl_idname = f"{OP_PREFIX}.copy_intersection"
bl_label = "Copy Intersections"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
a = props.cyl_slope_denom
C_val = props.cyl_offset
text = "Intersection Points:\n"
text += f"Parameters: a = {a:.4f}, C = {C_val:.4f}\n\n"
text += "[ Z = X and Z = (1/a)*X + C ]\n"
if abs(a - 1.0) < 0.0001:
text += " Lines are parallel. (a = 1)\n"
else:
x_m = (a * (-C_val)) / (a - 1.0)
x_p = (a * C_val) / (a - 1.0)
text += f" Line (C = -{C_val:.1f}): X = {x_m:.4f}, Z = {x_m:.4f}\n"
text += f" Line (C = 0.0): X = 0.0000, Z = 0.0000\n"
text += f" Line (C = +{C_val:.1f}): X = {x_p:.4f}, Z = {x_p:.4f}\n"
text += "\n[ Z = -X and Z = (1/a)*X + C ]\n"
if abs(a + 1.0) < 0.0001:
text += " Lines are parallel. (a = -1)\n"
else:
xm_m = (-a * (-C_val)) / (a + 1.0)
xm_p = (-a * C_val) / (a + 1.0)
text += f" Line (C = -{C_val:.1f}): X = {xm_m:.4f}, Z = {-xm_m:.4f}\n"
text += f" Line (C = 0.0): X = 0.0000, Z = 0.0000\n"
text += f" Line (C = +{C_val:.1f}): X = {xm_p:.4f}, Z = {-xm_p:.4f}\n"
context.window_manager.clipboard = text
self.report({'INFO'}, "Intersection Data Copied to Clipboard!")
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: return {'CANCELLED'}
code = target_text.as_string()
c, l, r = props.torus_color, props.torus_loc, props.torus_rot
cc, zc, mzc = props.cyl_color, props.zx_color, props.minus_zx_color
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'
new_dict += f' "cyl_thickness": {props.cyl_thickness:.4f},\n'
new_dict += f' "cyl_color": ({cc[0]:.4f}, {cc[1]:.4f}, {cc[2]:.4f}, {cc[3]:.4f}),\n'
new_dict += f' "zx_color": ({zc[0]:.4f}, {zc[1]:.4f}, {zc[2]:.4f}, {zc[3]:.4f}),\n'
new_dict += f' "minus_zx_color": ({mzc[0]:.4f}, {mzc[1]:.4f}, {mzc[2]:.4f}, {mzc[3]:.4f}),\n'
new_dict += f' "cyl_slope_denom": {props.cyl_slope_denom:.4f},\n'
new_dict += f' "cyl_offset": {props.cyl_offset:.4f},\n'
new_dict += f' "cyl_limit": {props.cyl_limit:.4f},\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: 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: 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: 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
p.cyl_thickness = 0.5; p.cyl_color = (0.1, 0.6, 0.9, 1.0)
p.zx_color = (0.9, 0.2, 0.2, 1.0); p.minus_zx_color = (0.9, 0.8, 0.2, 1.0)
p.cyl_slope_denom = 0.6; p.cyl_offset = 10.0; p.cyl_limit = 50.0
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]")
box.row().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_EquationCylindersPanel(Panel):
bl_label = "Equation Cylinders (5 Lines)"
bl_idname = f"{PREFIX}_PT_eq_cylinders"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
box = layout.box()
box.label(text="Math Equations (Y=0 Plane):", icon='FILE_TEXT')
box.label(text=" Z = ( 1 / a ) * X ± C")
box.label(text=" Z = X and Z = -X")
box.separator()
col = box.column(align=True)
col.prop(props, "cyl_slope_denom", text="Denominator (a)")
col.prop(props, "cyl_offset", text="Offset (C)")
col.prop(props, "cyl_limit", text="Limit Bounds (-L to L)")
box.separator()
box.prop(props, "cyl_thickness", text="Thickness")
row_c1 = box.row()
row_c1.prop(props, "cyl_color", text="(1/a)*X ± C")
row_c2 = box.row()
row_c2.prop(props, "zx_color", text="Z = X")
row_c2.prop(props, "minus_zx_color", text="Z = -X")
# ★ 交点表示エリア (Z=X と Z=-X)
box.separator()
box_int = box.box()
box_int.label(text="Intersections:", icon='DRIVER')
a = props.cyl_slope_denom
C_val = props.cyl_offset
col_pts = box_int.column()
row_pts = col_pts.row()
col_L = row_pts.column()
col_R = row_pts.column()
# [ Z = X ] 側
col_L.label(text="[ with Z = X ]")
if abs(a - 1.0) < 0.0001:
col_L.label(text="Parallel (a=1)")
else:
x_minus = (a * (-C_val)) / (a - 1.0)
x_plus = (a * C_val) / (a - 1.0)
col_L.label(text=f"C=-{C_val:.1f} : ({x_minus:.1f}, {x_minus:.1f})")
col_L.label(text=f"C= 0.0 : (0.0, 0.0)")
col_L.label(text=f"C=+{C_val:.1f} : ({x_plus:.1f}, {x_plus:.1f})")
# [ Z = -X ] 側
col_R.label(text="[ with Z = -X ]")
if abs(a + 1.0) < 0.0001:
col_R.label(text="Parallel (a=-1)")
else:
xm_minus = (-a * (-C_val)) / (a + 1.0)
xm_plus = (-a * C_val) / (a + 1.0)
col_R.label(text=f"C=-{C_val:.1f} : ({xm_minus:.1f}, {-xm_minus:.1f})")
col_R.label(text=f"C= 0.0 : (0.0, 0.0)")
col_R.label(text=f"C=+{C_val:.1f} : ({xm_plus:.1f}, {-xm_plus:.1f})")
box_int.operator(OT_CopyIntersectionInfo.bl_idname, icon='COPYDOWN', text="Copy Intersections")
col_exec = box.column()
col_exec.scale_y = 1.2
col_exec.operator(OT_CreateEquationCylinders.bl_idname, icon='MESH_CYLINDER', text="Create 5 Cylinders")
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_CreateEquationCylinders,
OT_CopyIntersectionInfo,
OT_CopyFullScript,
OT_Reset,
OT_OpenUrl,
OT_RemoveAddon,
PT_MainPanel,
PT_EquationCylindersPanel,
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))
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)
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except ValueError: pass
if __name__ == "__main__": register()
Intersection Points ( Z = X and Z = (1/a)*X + C ):
Parameters: a = 0.6000, C = 10.0000
Line (C = -10.0): X = 15.00, Z = 15.00
Line (C = 0.0): X = 0.00, Z = 0.00
Line (C = +10.0): X = -15.00, Z = -15.00