blender Million 2026
角度情報 20260405
# Copied: 2026-04-05 16:00:00
import bpy
import bmesh
import webbrowser
import math
import mathutils
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime
# ==============================================================================
# 設定エリア & ID管理
# ==============================================================================
PREFIX = "Sphere20260227"
TAB_NAME = " [ Sphere Angle ] "
# ★ このスクリプト自身のID (絶対に消さないこと)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SPHERE_2026_02_27_FIXED ###"
bl_info = {
"name": f"zionad 520[ Sphere Angle ] {PREFIX}",
"author": "zionadchat",
"version": (6, 1, 3),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": "Spheres & Arrows Angle Tool with X/Y Mirrors",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props_v17"
ADDON_LINKS = (
{"label": "角度情報 20260405", "url": "<https://www.notion.so/20260405-338f5dacaf4380afa9a9f565e52f966a>"},
{"label": "Code Copy Template", "url": "<https://www.notion.so/Code-copy-20260221>"},
{"label": "Theory Background", "url": "<https://www.notion.so/Einstein-from-20260119>"},
)
# ==============================================================================
# デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"show_preview": True,
"origin_pt": (0.0000, 0.0000, 0.0000),
"pt_a": (10.0000, 0.0000, 0.0000),
"pt_b": (0.0000, 10.0000, 0.0000),
"v_return": 1.6000,
"v_return_y": 0.8000,
"origin_color": (1.0000, 1.0000, 1.0000, 1.0000),
"origin_radius": 0.5000,
"pt_a_color": (1.0000, 0.1000, 0.1000, 1.0000),
"pt_a_radius": 0.5000,
"pt_b_color": (0.1000, 0.3000, 1.0000, 1.0000),
"pt_b_radius": 0.5000,
"mirror_x_color": (1.0000, 0.5000, 0.5000, 1.0000),
"mirror_x_radius": 0.5000,
"mirror_y_color": (0.5000, 0.5000, 1.0000, 1.0000),
"mirror_y_radius": 0.5000,
"arrow_a_color": (1.0000, 0.5000, 0.0000, 1.0000),
"arrow_a_thickness": 0.1500,
"arrow_b_color": (0.0000, 0.8000, 1.0000, 1.0000),
"arrow_b_thickness": 0.1500,
"arrow_mx_color": (1.0000, 0.2000, 0.2000, 1.0000),
"arrow_mx_thickness": 0.1500,
"arrow_my_color": (0.2000, 0.2000, 1.0000, 1.0000),
"arrow_my_thickness": 0.1500,
}
# <END_DICT>
# ==============================================================================
# データ クリーンアップ管理
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_MATS =[
f"PreviewMat_Origin_{PREFIX}",
f"PreviewMat_PtA_{PREFIX}",
f"PreviewMat_PtB_{PREFIX}",
f"PreviewMat_MirrorX_{PREFIX}",
f"PreviewMat_MirrorY_{PREFIX}",
f"PreviewMat_ArrowA_{PREFIX}",
f"PreviewMat_ArrowB_{PREFIX}",
f"PreviewMat_ArrowMX_{PREFIX}",
f"PreviewMat_ArrowMY_{PREFIX}"
]
def safe_remove_object(obj):
if not obj: return
mesh = obj.data
try: bpy.data.objects.remove(obj, do_unlink=True)
except: pass
if mesh and mesh.users == 0:
try: bpy.data.meshes.remove(mesh)
except: pass
def cleanup_preview_data():
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if col:
for obj in list(col.objects):
safe_remove_object(obj)
try: bpy.data.collections.remove(col)
except: pass
for mat_name in PREVIEW_MATS:
mat = bpy.data.materials.get(mat_name)
if mat and mat.users == 0:
try: bpy.data.materials.remove(mat)
except: pass
# ==============================================================================
# 数学計算用関数
# ==============================================================================
def get_plane_equation_str(O, A, B):
try:
vec_a = A - O
vec_b = B - O
n = vec_a.cross(vec_b)
if n.length > 1e-6:
n.normalize()
d = -n.dot(O)
def fmt(val, is_first=False):
if abs(val) < 1e-5: val = 0.0
if is_first: return f"{val:.3f}"
return f"+ {val:.3f}" if val >= 0 else f"- {abs(val):.3f}"
return f"{fmt(n.x, True)}x {fmt(n.y)}y {fmt(n.z)}z {fmt(d)} = 0"
except: pass
return "Undefined (Collinear)"
def calc_mirror_x_pos(pt_a, v_return):
v = v_return if abs(v_return) > 1e-6 else 1e-6
x = pt_a[0] - (pt_a[0] / v)
y = pt_a[1]
z = -(pt_a[0] / v)
return Vector((x, y, z))
def calc_mirror_y_pos(pt_b, v_return, v_return_y):
vy = v_return_y if abs(v_return_y) > 1e-6 else 1e-6
# y反射鏡 見かけ速度 x成分 = 1 - (v_return - 1)
app_vx = 1.0 - (v_return - 1.0)
# ゼロ割り防止
if abs(app_vx) < 1e-6:
app_vx = 1e-6 if app_vx >= 0 else -1e-6
# x成分: -(y成分 / (1 - (x軸の速度v - 1)))
x = -(pt_b[1] / app_vx)
# y成分: 同じ
y = pt_b[1]
# z成分: -(y成分 / y軸の速度v)
z = -(pt_b[1] / vy)
return Vector((x, y, z))
# ==============================================================================
# マテリアル作成ロジック
# ==============================================================================
def ensure_bsdf(mat):
nodes = mat.node_tree.nodes
links = mat.node_tree.links
bsdf = next((n for n in nodes if n.type == 'BSDF_PRINCIPLED'), None)
out = next((n for n in nodes if n.type == 'OUTPUT_MATERIAL'), None)
if not bsdf: bsdf = nodes.new("ShaderNodeBsdfPrincipled")
if not out: out = nodes.new("ShaderNodeOutputMaterial")
if not bsdf.outputs[0].is_linked: links.new(bsdf.outputs[0], out.inputs[0])
return bsdf
def create_unique_material(color, name_prefix="Mat"):
timestamp = datetime.now().strftime('%M%S%f')[:5]
mat_name = f"{name_prefix}_{timestamp}"
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
mat.diffuse_color = color
if mat.use_nodes:
bsdf = ensure_bsdf(mat)
if "Base Color" in bsdf.inputs: bsdf.inputs['Base Color'].default_value = color
if "Alpha" in bsdf.inputs: bsdf.inputs['Alpha'].default_value = color[3]
return mat
def get_or_create_preview_material(mat_name):
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
return mat
def update_preview_material(mat, color):
mat.diffuse_color = color
if mat.use_nodes:
bsdf = ensure_bsdf(mat)
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 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[c.name for c in context.scene.collection.children]:
context.scene.collection.children.link(col)
for obj in list(col.objects):
safe_remove_object(obj)
if not props.show_preview: return
def create_prev_obj(name_suffix, bm, mat_name, color):
mesh = bpy.data.meshes.new(f"PreviewMesh_{name_suffix}")
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(f"[Preview] {name_suffix}", mesh)
col.objects.link(obj)
mat = get_or_create_preview_material(mat_name)
update_preview_material(mat, color)
obj.data.materials.append(mat)
O_vec = Vector(props.origin_pt).copy()
A_vec = Vector(props.pt_a).copy()
B_vec = Vector(props.pt_b).copy()
MirrorX_vec = calc_mirror_x_pos(A_vec, props.v_return)
# 引数に props.v_return と props.v_return_y の両方を渡す
MirrorY_vec = calc_mirror_y_pos(B_vec, props.v_return, props.v_return_y)
# 1. 5つのSpheres
spheres_data =[
(O_vec, props.origin_radius, props.origin_color, "Origin", PREVIEW_MATS[0]),
(A_vec, props.pt_a_radius, props.pt_a_color, "PtA", PREVIEW_MATS[1]),
(B_vec, props.pt_b_radius, props.pt_b_color, "PtB", PREVIEW_MATS[2]),
(MirrorX_vec, props.mirror_x_radius, props.mirror_x_color, "MirrorX", PREVIEW_MATS[3]),
(MirrorY_vec, props.mirror_y_radius, props.mirror_y_color, "MirrorY", PREVIEW_MATS[4]),
]
for pt, r, c, name, mat_name in spheres_data:
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=max(r, 0.001))
bmesh.ops.translate(bm, vec=pt, verts=bm.verts)
create_prev_obj(name, bm, mat_name, c)
# 2. 4つのArrows (from -> to)
arrows_data =[
(O_vec, A_vec, props.arrow_a_thickness, props.arrow_a_color, "ArrowA", PREVIEW_MATS[5]),
(O_vec, B_vec, props.arrow_b_thickness, props.arrow_b_color, "ArrowB", PREVIEW_MATS[6]),
(MirrorX_vec, O_vec, props.arrow_mx_thickness, props.arrow_mx_color, "ArrowMX", PREVIEW_MATS[7]),
(MirrorY_vec, O_vec, props.arrow_my_thickness, props.arrow_my_color, "ArrowMY", PREVIEW_MATS[8]),
]
for p_from, p_to, thick, c, name, mat_name in arrows_data:
bm = bmesh.new()
vec = p_to - p_from
length = vec.length
if length > 1e-6:
Z = Vector((0,0,1))
try: rot = Z.rotation_difference(vec).to_matrix().to_4x4()
except: rot = mathutils.Matrix.Identity(4)
else:
rot = mathutils.Matrix.Identity(4)
head_len = min(length * 0.2, thick * 6)
body_len = length - head_len
if body_len > 0:
c_pos = p_from + vec.normalized() * (body_len / 2)
mat_body = mathutils.Matrix.Translation(c_pos) @ rot
geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=16, radius1=thick, radius2=thick, depth=body_len)
bmesh.ops.transform(bm, matrix=mat_body, verts=geom['verts'])
if length > 1e-6:
c_pos_head = p_from + vec.normalized() * (body_len + head_len / 2)
mat_head = mathutils.Matrix.Translation(c_pos_head) @ rot
geom_head = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=16, radius1=thick*2.5, radius2=0, depth=head_len)
bmesh.ops.transform(bm, matrix=mat_head, verts=geom_head['verts'])
create_prev_obj(name, bm, mat_name, c)
# ==============================================================================
# タイマー管理
# ==============================================================================
_timer = None
def delayed_update_safe():
global _timer
if not bpy.context or not getattr(bpy.context, "scene", None):
_timer = None
return None
try: update_preview_geometry(bpy.context)
except Exception as e: print("Preview update error:", e)
_timer = None
return None
def on_update(self, context):
global _timer
if _timer:
try: bpy.app.timers.unregister(_timer)
except: pass
_timer = None
_timer = bpy.app.timers.register(delayed_update_safe, first_interval=0.05)
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_SphereProps(PropertyGroup):
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
origin_pt: FloatVectorProperty(name="Origin", size=3, default=CURRENT_DEFAULTS['origin_pt'], update=on_update)
pt_a: FloatVectorProperty(name="Point A", size=3, default=CURRENT_DEFAULTS['pt_a'], update=on_update)
pt_b: FloatVectorProperty(name="Point B", size=3, default=CURRENT_DEFAULTS['pt_b'], update=on_update)
v_return: FloatProperty(name="速度Vreturn (X)", default=CURRENT_DEFAULTS['v_return'], min=0.001, update=on_update)
v_return_y: FloatProperty(name="速度Vreturn (Y)", default=CURRENT_DEFAULTS['v_return_y'], min=0.001, update=on_update)
origin_color: FloatVectorProperty(name="Origin Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['origin_color'], update=on_update)
origin_radius: FloatProperty(name="Origin Radius", default=CURRENT_DEFAULTS['origin_radius'], min=0.01, update=on_update)
pt_a_color: FloatVectorProperty(name="Point A Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['pt_a_color'], update=on_update)
pt_a_radius: FloatProperty(name="Point A Radius", default=CURRENT_DEFAULTS['pt_a_radius'], min=0.01, update=on_update)
pt_b_color: FloatVectorProperty(name="Point B Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['pt_b_color'], update=on_update)
pt_b_radius: FloatProperty(name="Point B Radius", default=CURRENT_DEFAULTS['pt_b_radius'], min=0.01, update=on_update)
mirror_x_color: FloatVectorProperty(name="Mirror X Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['mirror_x_color'], update=on_update)
mirror_x_radius: FloatProperty(name="Mirror X Radius", default=CURRENT_DEFAULTS['mirror_x_radius'], min=0.01, update=on_update)
mirror_y_color: FloatVectorProperty(name="Mirror Y Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['mirror_y_color'], update=on_update)
mirror_y_radius: FloatProperty(name="Mirror Y Radius", default=CURRENT_DEFAULTS['mirror_y_radius'], min=0.01, update=on_update)
arrow_a_color: FloatVectorProperty(name="Arrow A Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['arrow_a_color'], update=on_update)
arrow_a_thickness: FloatProperty(name="Arrow A Thick", default=CURRENT_DEFAULTS['arrow_a_thickness'], min=0.001, update=on_update)
arrow_b_color: FloatVectorProperty(name="Arrow B Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['arrow_b_color'], update=on_update)
arrow_b_thickness: FloatProperty(name="Arrow B Thick", default=CURRENT_DEFAULTS['arrow_b_thickness'], min=0.001, update=on_update)
arrow_mx_color: FloatVectorProperty(name="Arrow MX Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['arrow_mx_color'], update=on_update)
arrow_mx_thickness: FloatProperty(name="Arrow MX Thick", default=CURRENT_DEFAULTS['arrow_mx_thickness'], min=0.001, update=on_update)
arrow_my_color: FloatVectorProperty(name="Arrow MY Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['arrow_my_color'], update=on_update)
arrow_my_thickness: FloatProperty(name="Arrow MY Thick", default=CURRENT_DEFAULTS['arrow_my_thickness'], min=0.001, update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CreateAngleObjects(Operator):
bl_idname = f"{OP_PREFIX}.create_objects"
bl_label = "Create Objects"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
timestamp = datetime.now().strftime('%H%M%S')
O_vec = Vector(props.origin_pt).copy()
A_vec = Vector(props.pt_a).copy()
B_vec = Vector(props.pt_b).copy()
MirrorX_vec = calc_mirror_x_pos(A_vec, props.v_return)
MirrorY_vec = calc_mirror_y_pos(B_vec, props.v_return, props.v_return_y)
def create_sphere_obj(name, loc, radius, color):
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=max(radius, 0.001))
bmesh.ops.translate(bm, vec=loc, verts=bm.verts)
mesh = bpy.data.meshes.new(f"{name}_Mesh")
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(f"{name}_{timestamp}", mesh)
if context.collection: context.collection.objects.link(obj)
else: context.scene.collection.objects.link(obj)
mat = create_unique_material(color, f"Mat_{name}")
obj.data.materials.append(mat)
return obj
def create_arrow_obj(name, loc_from, loc_to, thick, color):
bm = bmesh.new()
vec = loc_to - loc_from
length = vec.length
if length > 1e-6:
Z = Vector((0,0,1))
try: rot = Z.rotation_difference(vec).to_matrix().to_4x4()
except: rot = mathutils.Matrix.Identity(4)
else:
rot = mathutils.Matrix.Identity(4)
head_len = min(length * 0.2, thick * 6)
body_len = length - head_len
if body_len > 0:
c_pos = loc_from + vec.normalized() * (body_len / 2)
mat_body = mathutils.Matrix.Translation(c_pos) @ rot
geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=16, radius1=thick, radius2=thick, depth=body_len)
bmesh.ops.transform(bm, matrix=mat_body, verts=geom['verts'])
if length > 1e-6:
c_pos_head = loc_from + vec.normalized() * (body_len + head_len / 2)
mat_head = mathutils.Matrix.Translation(c_pos_head) @ rot
geom_head = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=16, radius1=thick*2.5, radius2=0, depth=head_len)
bmesh.ops.transform(bm, matrix=mat_head, verts=geom_head['verts'])
mesh = bpy.data.meshes.new(f"{name}_Mesh")
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(f"{name}_{timestamp}", mesh)
if context.collection: context.collection.objects.link(obj)
else: context.scene.collection.objects.link(obj)
mat = create_unique_material(color, f"Mat_{name}")
obj.data.materials.append(mat)
return obj
bpy.ops.object.select_all(action='DESELECT')
objs =[]
objs.append(create_sphere_obj("Origin", O_vec, props.origin_radius, props.origin_color))
objs.append(create_sphere_obj("PointA", A_vec, props.pt_a_radius, props.pt_a_color))
objs.append(create_sphere_obj("PointB", B_vec, props.pt_b_radius, props.pt_b_color))
objs.append(create_sphere_obj("MirrorX", MirrorX_vec, props.mirror_x_radius, props.mirror_x_color))
objs.append(create_sphere_obj("MirrorY", MirrorY_vec, props.mirror_y_radius, props.mirror_y_color))
objs.append(create_arrow_obj("ArrowA", O_vec, A_vec, props.arrow_a_thickness, props.arrow_a_color))
objs.append(create_arrow_obj("ArrowB", O_vec, B_vec, props.arrow_b_thickness, props.arrow_b_color))
objs.append(create_arrow_obj("ArrowMX", MirrorX_vec, O_vec, props.arrow_mx_thickness, props.arrow_mx_color))
objs.append(create_arrow_obj("ArrowMY", MirrorY_vec, O_vec, props.arrow_my_thickness, props.arrow_my_color))
for o in objs: o.select_set(True)
if objs: context.view_layer.objects.active = objs[-1]
self.report({'INFO'}, "Created Spheres & Arrows with Unique Materials!")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string(): target_text = t; break
if not target_text:
self.report({'WARNING'}, "Source script not found in Text Editor.")
return {'CANCELLED'}
code = target_text.as_string()
oc, ac, bc = props.origin_color, props.pt_a_color, props.pt_b_color
mxc, myc = props.mirror_x_color, props.mirror_y_color
aac, abc = props.arrow_a_color, props.arrow_b_color
amxc, amyc = props.arrow_mx_color, props.arrow_my_color
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "origin_pt": ({props.origin_pt[0]:.4f}, {props.origin_pt[1]:.4f}, {props.origin_pt[2]:.4f}),\n'
new_dict += f' "pt_a": ({props.pt_a[0]:.4f}, {props.pt_a[1]:.4f}, {props.pt_a[2]:.4f}),\n'
new_dict += f' "pt_b": ({props.pt_b[0]:.4f}, {props.pt_b[1]:.4f}, {props.pt_b[2]:.4f}),\n'
new_dict += f' "v_return": {props.v_return:.4f},\n'
new_dict += f' "v_return_y": {props.v_return_y:.4f},\n'
new_dict += f' "origin_color": ({oc[0]:.4f}, {oc[1]:.4f}, {oc[2]:.4f}, {oc[3]:.4f}),\n'
new_dict += f' "origin_radius": {props.origin_radius:.4f},\n'
new_dict += f' "pt_a_color": ({ac[0]:.4f}, {ac[1]:.4f}, {ac[2]:.4f}, {ac[3]:.4f}),\n'
new_dict += f' "pt_a_radius": {props.pt_a_radius:.4f},\n'
new_dict += f' "pt_b_color": ({bc[0]:.4f}, {bc[1]:.4f}, {bc[2]:.4f}, {bc[3]:.4f}),\n'
new_dict += f' "pt_b_radius": {props.pt_b_radius:.4f},\n'
new_dict += f' "mirror_x_color": ({mxc[0]:.4f}, {mxc[1]:.4f}, {mxc[2]:.4f}, {mxc[3]:.4f}),\n'
new_dict += f' "mirror_x_radius": {props.mirror_x_radius:.4f},\n'
new_dict += f' "mirror_y_color": ({myc[0]:.4f}, {myc[1]:.4f}, {myc[2]:.4f}, {myc[3]:.4f}),\n'
new_dict += f' "mirror_y_radius": {props.mirror_y_radius:.4f},\n'
new_dict += f' "arrow_a_color": ({aac[0]:.4f}, {aac[1]:.4f}, {aac[2]:.4f}, {aac[3]:.4f}),\n'
new_dict += f' "arrow_a_thickness": {props.arrow_a_thickness:.4f},\n'
new_dict += f' "arrow_b_color": ({abc[0]:.4f}, {abc[1]:.4f}, {abc[2]:.4f}, {abc[3]:.4f}),\n'
new_dict += f' "arrow_b_thickness": {props.arrow_b_thickness:.4f},\n'
new_dict += f' "arrow_mx_color": ({amxc[0]:.4f}, {amxc[1]:.4f}, {amxc[2]:.4f}, {amxc[3]:.4f}),\n'
new_dict += f' "arrow_mx_thickness": {props.arrow_mx_thickness:.4f},\n'
new_dict += f' "arrow_my_color": ({amyc[0]:.4f}, {amyc[1]:.4f}, {amyc[2]:.4f}, {amyc[3]:.4f}),\n'
new_dict += f' "arrow_my_thickness": {props.arrow_my_thickness:.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
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 with absolute safety!")
except Exception as e:
self.report({'ERROR'}, f"Copy failed: {e}")
return {'CANCELLED'}
return {'FINISHED'}
class OT_CopyAngleInfo(Operator):
bl_idname = f"{OP_PREFIX}.copy_angle_info"
bl_label = "Copy Angle Info"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return {'CANCELLED'}
O = Vector(props.origin_pt).copy()
A = Vector(props.pt_a).copy()
B = Vector(props.pt_b).copy()
Mx = calc_mirror_x_pos(A, props.v_return)
My = calc_mirror_y_pos(B, props.v_return, props.v_return_y)
app_vx = 1.0 - (props.v_return - 1.0)
vec_a = A - O
vec_b = B - O
def get_angles(v):
if v.length < 1e-6: return 0.0, 0.0, 0.0
try:
vx = math.degrees(v.angle(Vector((1,0,0))))
vy = math.degrees(v.angle(Vector((0,1,0))))
vz = math.degrees(v.angle(Vector((0,0,1))))
except: return 0.0, 0.0, 0.0
return vx, vy, vz
ax, ay, az = get_angles(vec_a)
bx, by, bz = get_angles(vec_b)
angle_ab = 0.0
if vec_a.length > 1e-6 and vec_b.length > 1e-6:
try: angle_ab = math.degrees(vec_a.angle(vec_b))
except: pass
plane_str = get_plane_equation_str(O, A, B)
text = (
f"【角度・平面・反射鏡 情報】\n"
f"■基点(Orig): ({O.x:.4f}, {O.y:.4f}, {O.z:.4f})\n"
f"■点A: ({A.x:.4f}, {A.y:.4f}, {A.z:.4f})\n"
f" - 距離: {vec_a.length:.4f}\n"
f" - X軸との角度: {ax:.2f}°\n"
f" - Y軸との角度: {ay:.2f}°\n"
f" - Z軸との角度: {az:.2f}°\n"
f"■点B: ({B.x:.4f}, {B.y:.4f}, {B.z:.4f})\n"
f" - 距離: {vec_b.length:.4f}\n"
f" - X軸との角度: {bx:.2f}°\n"
f" - Y軸との角度: {by:.2f}°\n"
f" - Z軸との角度: {bz:.2f}°\n"
f"■2つの矢印のなす角 (A - Orig - B): {angle_ab:.2f}°\n"
f"■平面方程式 (ax + by + cz + d = 0): \n"
f" {plane_str}\n"
f"■過去からのx反射鏡: ({Mx.x:.4f}, {Mx.y:.4f}, {Mx.z:.4f})\n"
f"■過去からのy反射鏡: ({My.x:.4f}, {My.y:.4f}, {My.z:.4f})\n"
f" - y反射鏡 見かけ速度 x成分: {app_vx:.4f}\n"
f" - 速度Vreturn (X): {props.v_return:.4f}\n"
f" - 速度Vreturn (Y): {props.v_return_y:.4f}\n"
)
context.window_manager.clipboard = text
self.report({'INFO'}, "Copied Angle & Mirror Info to clipboard")
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 = "Sphere Angle Tool"
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
# -----------------------------
# 1. 共通ツール
# -----------------------------
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')
# -----------------------------
# 2. 座標設定と角度計算表示
# -----------------------------
box_a = layout.box()
box_a.label(text="Coordinates & Math", icon='DRIVER_TRANSFORM')
box_a.prop(props, "origin_pt", text="Origin (O)")
box_a.prop(props, "pt_a", text="Point A")
box_a.prop(props, "pt_b", text="Point B")
box_a.separator()
box_a.prop(props, "v_return", text="速度Vreturn (X)")
box_a.prop(props, "v_return_y", text="速度Vreturn (Y)")
Mx = calc_mirror_x_pos(props.pt_a, props.v_return)
My = calc_mirror_y_pos(props.pt_b, props.v_return, props.v_return_y)
app_vx = 1.0 - (props.v_return - 1.0)
O = Vector(props.origin_pt).copy()
A = Vector(props.pt_a).copy()
B = Vector(props.pt_b).copy()
vec_a = A - O
vec_b = B - O
col_a = box_a.column(align=True)
col_a.label(text=f"過去のx反射鏡: ({Mx.x:.2f}, {Mx.y:.2f}, {Mx.z:.2f})", icon='MESH_UVSPHERE')
col_a.label(text=f"過去のy反射鏡: ({My.x:.2f}, {My.y:.2f}, {My.z:.2f})", icon='MESH_UVSPHERE')
col_a.label(text=f"y反射鏡 見かけ速度 x成分: {app_vx:.4f}", icon='DRIVER_DISTANCE')
angle_ab = 0.0
if vec_a.length > 1e-6 and vec_b.length > 1e-6:
try: angle_ab = math.degrees(vec_a.angle(vec_b))
except: pass
col_a.label(text=f"Angle (A-O-B): {angle_ab:.2f}°", icon='DRIVER_ROTATIONAL_DIFFERENCE')
plane_str = get_plane_equation_str(O, A, B)
col_a.label(text=f"Plane Eq: {plane_str}", icon='MESH_PLANE')
box_a.separator()
box_a.operator(OT_CopyAngleInfo.bl_idname, icon='COPYDOWN', text="Copy Full Angle Info")
# -----------------------------
# 3. 色とサイズ(1行ずつ)
# -----------------------------
if props.show_preview:
b2 = layout.box()
b2.label(text="Size & Color Settings", icon='COLOR')
b2.label(text="Origin Sphere", icon='DOT')
r1 = b2.row(align=True)
r1.prop(props, "origin_radius", text="Radius")
r1.prop(props, "origin_color", text="")
b2.label(text="Point A Sphere", icon='DOT')
r2 = b2.row(align=True)
r2.prop(props, "pt_a_radius", text="Radius")
r2.prop(props, "pt_a_color", text="")
b2.label(text="Point B Sphere", icon='DOT')
r3 = b2.row(align=True)
r3.prop(props, "pt_b_radius", text="Radius")
r3.prop(props, "pt_b_color", text="")
b2.label(text="過去のx反射鏡 (Mirror X)", icon='DOT')
rmx = b2.row(align=True)
rmx.prop(props, "mirror_x_radius", text="Radius")
rmx.prop(props, "mirror_x_color", text="")
b2.label(text="過去のy反射鏡 (Mirror Y)", icon='DOT')
rmy = b2.row(align=True)
rmy.prop(props, "mirror_y_radius", text="Radius")
rmy.prop(props, "mirror_y_color", text="")
b2.separator()
b2.label(text="Arrow A (Origin -> A)", icon='FORWARD')
r4 = b2.row(align=True)
r4.prop(props, "arrow_a_thickness", text="Thickness")
r4.prop(props, "arrow_a_color", text="")
b2.label(text="Arrow B (Origin -> B)", icon='FORWARD')
r5 = b2.row(align=True)
r5.prop(props, "arrow_b_thickness", text="Thickness")
r5.prop(props, "arrow_b_color", text="")
b2.label(text="Arrow MX (Mirror X -> Origin)", icon='BACK')
r6 = b2.row(align=True)
r6.prop(props, "arrow_mx_thickness", text="Thickness")
r6.prop(props, "arrow_mx_color", text="")
b2.label(text="Arrow MY (Mirror Y -> Origin)", icon='BACK')
r7 = b2.row(align=True)
r7.prop(props, "arrow_my_thickness", text="Thickness")
r7.prop(props, "arrow_my_color", text="")
# -----------------------------
# 4. 実体化ボタン
# -----------------------------
layout.separator()
col_exec = layout.column()
col_exec.scale_y = 1.5
col_exec.operator(OT_CreateAngleObjects.bl_idname, icon='MESH_UVSPHERE', text="Create 5 Spheres & 4 Arrows")
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_SphereProps, OT_CreateAngleObjects, OT_CopyFullScript, OT_CopyAngleInfo,
OT_OpenUrl, OT_RemoveAddon,
PT_MainPanel, PT_LinksPanel, PT_RemovePanel
)
def auto_open_sidebar():
try:
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
if not space.show_region_ui:
space.show_region_ui = True
except: pass
return None
def register():
for c in classes:
try: bpy.utils.register_class(c)
except: pass
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_SphereProps))
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: pass
_timer = None
cleanup_preview_data()
try:
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
except: pass
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except: pass
if __name__ == "__main__":
register()
# Copied: 2026-04-05 08:04:03
import bpy
import bmesh
import webbrowser
import math
import mathutils
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime
# ==============================================================================
# 設定エリア & ID管理
# ==============================================================================
PREFIX = "Sphere20260227"
TAB_NAME = " [ Sphere Angle ] "
# ★ このスクリプト自身のID (絶対に消さないこと)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SPHERE_2026_02_27_FIXED ###"
bl_info = {
"name": f"zionad 520 [ Sphere Angle ] {PREFIX}",
"author": "zionadchat",
"version": (5, 0, 0), # ★ 完全安定版 v5.0.0
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": "3 Spheres & 2 Arrows Angle Tool (Stable Edition)",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props_v11"
ADDON_LINKS = (
{"label": "角度情報 20260405", "url": "<https://www.notion.so/20260405-338f5dacaf4380afa9a9f565e52f966a>"},
{"label": "Code Copy Template", "url": "<https://www.notion.so/Code-copy-20260221>"},
{"label": "Theory Background", "url": "<https://www.notion.so/Einstein-from-20260119>"},
)
# ==============================================================================
# デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"show_preview": True,
"origin_pt": (0.0000, 0.0000, 0.0000),
"pt_a": (10.0000, 0.0000, 0.0000),
"pt_b": (0.0000, 10.0000, 0.0000),
"v_return": 1.6000,
"v_return_y": 0.8000,
"origin_color": (1.0000, 1.0000, 1.0000, 1.0000),
"origin_radius": 0.5000,
"pt_a_color": (1.0000, 0.1000, 0.1000, 1.0000),
"pt_a_radius": 0.5000,
"pt_b_color": (0.1000, 0.3000, 1.0000, 1.0000),
"pt_b_radius": 0.5000,
"mirror_x_color": (1.0000, 0.5000, 0.5000, 1.0000),
"mirror_x_radius": 0.5000,
"mirror_y_color": (0.5000, 0.5000, 1.0000, 1.0000),
"mirror_y_radius": 0.5000,
"arrow_a_color": (1.0000, 0.5000, 0.0000, 1.0000),
"arrow_a_thickness": 0.1500,
"arrow_b_color": (0.0000, 0.8000, 1.0000, 1.0000),
"arrow_b_thickness": 0.1500,
"arrow_mx_color": (1.0000, 0.2000, 0.2000, 1.0000),
"arrow_mx_thickness": 0.1500,
"arrow_my_color": (0.2000, 0.2000, 1.0000, 1.0000),
"arrow_my_thickness": 0.1500,
}
# <END_DICT>
# ==============================================================================
# データ クリーンアップ管理 (安全化適用)
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_MATS = [
f"PreviewMat_Origin_{PREFIX}",
f"PreviewMat_PtA_{PREFIX}",
f"PreviewMat_PtB_{PREFIX}",
f"PreviewMat_ArrowA_{PREFIX}",
f"PreviewMat_ArrowB_{PREFIX}"
]
def safe_remove_object(obj):
if not obj: return
mesh = obj.data
try: bpy.data.objects.remove(obj, do_unlink=True)
except: pass
if mesh and mesh.users == 0:
try: bpy.data.meshes.remove(mesh)
except: pass
def cleanup_preview_data():
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if col:
for obj in list(col.objects):
safe_remove_object(obj)
try: bpy.data.collections.remove(col)
except: pass
for mat_name in PREVIEW_MATS:
mat = bpy.data.materials.get(mat_name)
if mat and mat.users == 0:
try: bpy.data.materials.remove(mat)
except: pass
# ==============================================================================
# 数学計算用関数
# ==============================================================================
def get_plane_equation_str(O, A, B):
try:
vec_a = A - O
vec_b = B - O
n = vec_a.cross(vec_b)
if n.length > 1e-6:
n.normalize()
d = -n.dot(O)
def fmt(val, is_first=False):
if abs(val) < 1e-5: val = 0.0
if is_first: return f"{val:.3f}"
return f"+ {val:.3f}" if val >= 0 else f"- {abs(val):.3f}"
return f"{fmt(n.x, True)}x {fmt(n.y)}y {fmt(n.z)}z {fmt(d)} = 0"
except: pass
return "Undefined (Collinear)"
# ==============================================================================
# マテリアル作成ロジック (ノード非破壊化 適用)
# ==============================================================================
def ensure_bsdf(mat):
nodes = mat.node_tree.nodes
links = mat.node_tree.links
bsdf = next((n for n in nodes if n.type == 'BSDF_PRINCIPLED'), None)
out = next((n for n in nodes if n.type == 'OUTPUT_MATERIAL'), None)
if not bsdf: bsdf = nodes.new("ShaderNodeBsdfPrincipled")
if not out: out = nodes.new("ShaderNodeOutputMaterial")
if not bsdf.outputs[0].is_linked:
links.new(bsdf.outputs[0], out.inputs[0])
return bsdf
def create_unique_material(color, name_prefix="Mat"):
timestamp = datetime.now().strftime('%M%S%f')[:5]
mat_name = f"{name_prefix}_{timestamp}"
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
mat.diffuse_color = color
if mat.use_nodes:
bsdf = ensure_bsdf(mat)
if "Base Color" in bsdf.inputs: bsdf.inputs['Base Color'].default_value = color
if "Alpha" in bsdf.inputs: bsdf.inputs['Alpha'].default_value = color[3]
return mat
def get_or_create_preview_material(mat_name):
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
return mat
def update_preview_material(mat, color):
mat.diffuse_color = color
if mat.use_nodes:
bsdf = ensure_bsdf(mat)
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 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)
# ★ Collection存在チェック強化
if col.name not in [c.name for c in context.scene.collection.children]:
context.scene.collection.children.link(col)
for obj in list(col.objects):
safe_remove_object(obj)
if not props.show_preview: return
def create_prev_obj(name_suffix, bm, mat_name, color):
mesh = bpy.data.meshes.new(f"PreviewMesh_{name_suffix}")
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(f"[Preview] {name_suffix}", mesh)
col.objects.link(obj)
mat = get_or_create_preview_material(mat_name)
update_preview_material(mat, color)
obj.data.materials.append(mat)
# ★ Vector最適化適用
O_vec = Vector(props.origin_pt).copy()
A_vec = Vector(props.pt_a).copy()
B_vec = Vector(props.pt_b).copy()
# 1. 3つのSpheres
spheres_data = [
(O_vec, props.origin_radius, props.origin_color, "Origin", PREVIEW_MATS[0]),
(A_vec, props.pt_a_radius, props.pt_a_color, "PtA", PREVIEW_MATS[1]),
(B_vec, props.pt_b_radius, props.pt_b_color, "PtB", PREVIEW_MATS[2]),
]
for pt, r, c, name, mat_name in spheres_data:
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=max(r, 0.001))
bmesh.ops.translate(bm, vec=pt, verts=bm.verts)
create_prev_obj(name, bm, mat_name, c)
# 2. 2つのArrows
arrows_data = [
(A_vec, props.arrow_a_thickness, props.arrow_a_color, "ArrowA", PREVIEW_MATS[3]),
(B_vec, props.arrow_b_thickness, props.arrow_b_color, "ArrowB", PREVIEW_MATS[4]),
]
for pt, thick, c, name, mat_name in arrows_data:
bm = bmesh.new()
vec = pt - O_vec
length = vec.length
# ★ NaN対策 (回転安全化)
if length > 1e-6:
Z = Vector((0,0,1))
try: rot = Z.rotation_difference(vec).to_matrix().to_4x4()
except: rot = mathutils.Matrix.Identity(4)
else:
rot = mathutils.Matrix.Identity(4)
head_len = min(length * 0.2, thick * 6)
body_len = length - head_len
if body_len > 0:
c_pos = O_vec + vec.normalized() * (body_len / 2)
mat_body = mathutils.Matrix.Translation(c_pos) @ rot
geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=16, radius1=thick, radius2=thick, depth=body_len)
bmesh.ops.transform(bm, matrix=mat_body, verts=geom['verts'])
if length > 1e-6:
c_pos_head = O_vec + vec.normalized() * (body_len + head_len / 2)
mat_head = mathutils.Matrix.Translation(c_pos_head) @ rot
geom_head = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=16, radius1=thick*2.5, radius2=0, depth=head_len)
bmesh.ops.transform(bm, matrix=mat_head, verts=geom_head['verts'])
create_prev_obj(name, bm, mat_name, c)
# ==============================================================================
# タイマー管理 (暴走防止適用)
# ==============================================================================
_timer = None
def delayed_update_safe():
global _timer
if not bpy.context or not getattr(bpy.context, "scene", None):
_timer = None
return None
try:
update_preview_geometry(bpy.context)
except Exception as e:
print("Preview update error:", e)
_timer = None
return None
def on_update(self, context):
global _timer
if _timer:
try: bpy.app.timers.unregister(_timer)
except: pass
_timer = None
_timer = bpy.app.timers.register(delayed_update_safe, first_interval=0.05)
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_SphereProps(PropertyGroup):
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
origin_pt: FloatVectorProperty(name="Origin", size=3, default=CURRENT_DEFAULTS['origin_pt'], update=on_update)
pt_a: FloatVectorProperty(name="Point A", size=3, default=CURRENT_DEFAULTS['pt_a'], update=on_update)
pt_b: FloatVectorProperty(name="Point B", size=3, default=CURRENT_DEFAULTS['pt_b'], update=on_update)
origin_color: FloatVectorProperty(name="Origin Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['origin_color'], update=on_update)
origin_radius: FloatProperty(name="Origin Radius", default=CURRENT_DEFAULTS['origin_radius'], min=0.01, update=on_update)
pt_a_color: FloatVectorProperty(name="Point A Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['pt_a_color'], update=on_update)
pt_a_radius: FloatProperty(name="Point A Radius", default=CURRENT_DEFAULTS['pt_a_radius'], min=0.01, update=on_update)
pt_b_color: FloatVectorProperty(name="Point B Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['pt_b_color'], update=on_update)
pt_b_radius: FloatProperty(name="Point B Radius", default=CURRENT_DEFAULTS['pt_b_radius'], min=0.01, update=on_update)
arrow_a_color: FloatVectorProperty(name="Arrow A Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['arrow_a_color'], update=on_update)
arrow_a_thickness: FloatProperty(name="Arrow A Thick", default=CURRENT_DEFAULTS['arrow_a_thickness'], min=0.001, update=on_update)
arrow_b_color: FloatVectorProperty(name="Arrow B Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['arrow_b_color'], update=on_update)
arrow_b_thickness: FloatProperty(name="Arrow B Thick", default=CURRENT_DEFAULTS['arrow_b_thickness'], min=0.001, update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CreateAngleObjects(Operator):
bl_idname = f"{OP_PREFIX}.create_objects"
bl_label = "Create Objects"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
timestamp = datetime.now().strftime('%H%M%S')
O_vec = Vector(props.origin_pt).copy()
def create_sphere_obj(name, loc, radius, color):
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=max(radius, 0.001))
bmesh.ops.translate(bm, vec=loc, verts=bm.verts)
mesh = bpy.data.meshes.new(f"{name}_Mesh")
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(f"{name}_{timestamp}", mesh)
if context.collection: context.collection.objects.link(obj)
else: context.scene.collection.objects.link(obj)
mat = create_unique_material(color, f"Mat_{name}")
obj.data.materials.append(mat)
return obj
def create_arrow_obj(name, loc_from, loc_to, thick, color):
bm = bmesh.new()
vec = loc_to - loc_from
length = vec.length
if length > 1e-6:
Z = Vector((0,0,1))
try: rot = Z.rotation_difference(vec).to_matrix().to_4x4()
except: rot = mathutils.Matrix.Identity(4)
else:
rot = mathutils.Matrix.Identity(4)
head_len = min(length * 0.2, thick * 6)
body_len = length - head_len
if body_len > 0:
c_pos = loc_from + vec.normalized() * (body_len / 2)
mat_body = mathutils.Matrix.Translation(c_pos) @ rot
geom = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=16, radius1=thick, radius2=thick, depth=body_len)
bmesh.ops.transform(bm, matrix=mat_body, verts=geom['verts'])
if length > 1e-6:
c_pos_head = loc_from + vec.normalized() * (body_len + head_len / 2)
mat_head = mathutils.Matrix.Translation(c_pos_head) @ rot
geom_head = bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=16, radius1=thick*2.5, radius2=0, depth=head_len)
bmesh.ops.transform(bm, matrix=mat_head, verts=geom_head['verts'])
mesh = bpy.data.meshes.new(f"{name}_Mesh")
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(f"{name}_{timestamp}", mesh)
if context.collection: context.collection.objects.link(obj)
else: context.scene.collection.objects.link(obj)
mat = create_unique_material(color, f"Mat_{name}")
obj.data.materials.append(mat)
return obj
bpy.ops.object.select_all(action='DESELECT')
objs = []
objs.append(create_sphere_obj("Origin", O_vec, props.origin_radius, props.origin_color))
objs.append(create_sphere_obj("PointA", Vector(props.pt_a).copy(), props.pt_a_radius, props.pt_a_color))
objs.append(create_sphere_obj("PointB", Vector(props.pt_b).copy(), props.pt_b_radius, props.pt_b_color))
objs.append(create_arrow_obj("ArrowA", O_vec, Vector(props.pt_a).copy(), props.arrow_a_thickness, props.arrow_a_color))
objs.append(create_arrow_obj("ArrowB", O_vec, Vector(props.pt_b).copy(), props.arrow_b_thickness, props.arrow_b_color))
for o in objs: o.select_set(True)
if objs: context.view_layer.objects.active = objs[-1]
self.report({'INFO'}, "Created Spheres & Arrows with Unique Materials!")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string(): target_text = t; break
if not target_text:
self.report({'WARNING'}, "Source script not found in Text Editor.")
return {'CANCELLED'}
code = target_text.as_string()
oc, ac, bc = props.origin_color, props.pt_a_color, props.pt_b_color
aac, abc = props.arrow_a_color, props.arrow_b_color
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "origin_pt": ({props.origin_pt[0]:.4f}, {props.origin_pt[1]:.4f}, {props.origin_pt[2]:.4f}),\n'
new_dict += f' "pt_a": ({props.pt_a[0]:.4f}, {props.pt_a[1]:.4f}, {props.pt_a[2]:.4f}),\n'
new_dict += f' "pt_b": ({props.pt_b[0]:.4f}, {props.pt_b[1]:.4f}, {props.pt_b[2]:.4f}),\n'
new_dict += f' "origin_color": ({oc[0]:.4f}, {oc[1]:.4f}, {oc[2]:.4f}, {oc[3]:.4f}),\n'
new_dict += f' "origin_radius": {props.origin_radius:.4f},\n'
new_dict += f' "pt_a_color": ({ac[0]:.4f}, {ac[1]:.4f}, {ac[2]:.4f}, {ac[3]:.4f}),\n'
new_dict += f' "pt_a_radius": {props.pt_a_radius:.4f},\n'
new_dict += f' "pt_b_color": ({bc[0]:.4f}, {bc[1]:.4f}, {bc[2]:.4f}, {bc[3]:.4f}),\n'
new_dict += f' "pt_b_radius": {props.pt_b_radius:.4f},\n'
new_dict += f' "arrow_a_color": ({aac[0]:.4f}, {aac[1]:.4f}, {aac[2]:.4f}, {aac[3]:.4f}),\n'
new_dict += f' "arrow_a_thickness": {props.arrow_a_thickness:.4f},\n'
new_dict += f' "arrow_b_color": ({abc[0]:.4f}, {abc[1]:.4f}, {abc[2]:.4f}, {abc[3]:.4f}),\n'
new_dict += f' "arrow_b_thickness": {props.arrow_b_thickness:.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
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 with absolute safety!")
except Exception as e:
self.report({'ERROR'}, f"Copy failed: {e}")
return {'CANCELLED'}
return {'FINISHED'}
class OT_CopyAngleInfo(Operator):
bl_idname = f"{OP_PREFIX}.copy_angle_info"
bl_label = "Copy Angle Info"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return {'CANCELLED'}
O = Vector(props.origin_pt).copy()
A = Vector(props.pt_a).copy()
B = Vector(props.pt_b).copy()
vec_a = A - O
vec_b = B - O
def get_angles(v):
if v.length < 1e-6: return 0.0, 0.0, 0.0
try:
vx = math.degrees(v.angle(Vector((1,0,0))))
vy = math.degrees(v.angle(Vector((0,1,0))))
vz = math.degrees(v.angle(Vector((0,0,1))))
except: return 0.0, 0.0, 0.0
return vx, vy, vz
ax, ay, az = get_angles(vec_a)
bx, by, bz = get_angles(vec_b)
angle_ab = 0.0
if vec_a.length > 1e-6 and vec_b.length > 1e-6:
try: angle_ab = math.degrees(vec_a.angle(vec_b))
except: pass
plane_str = get_plane_equation_str(O, A, B)
text = (
f"【角度・平面情報】\n"
f"■基点(Orig): ({O.x:.4f}, {O.y:.4f}, {O.z:.4f})\n"
f"■点A: ({A.x:.4f}, {A.y:.4f}, {A.z:.4f})\n"
f" - 距離: {vec_a.length:.4f}\n"
f" - X軸との角度: {ax:.2f}°\n"
f" - Y軸との角度: {ay:.2f}°\n"
f" - Z軸との角度: {az:.2f}°\n"
f"■点B: ({B.x:.4f}, {B.y:.4f}, {B.z:.4f})\n"
f" - 距離: {vec_b.length:.4f}\n"
f" - X軸との角度: {bx:.2f}°\n"
f" - Y軸との角度: {by:.2f}°\n"
f" - Z軸との角度: {bz:.2f}°\n"
f"■2つの矢印のなす角 (A - Orig - B): {angle_ab:.2f}°\n"
f"■平面方程式 (ax + by + cz + d = 0): \n"
f" {plane_str}\n"
)
context.window_manager.clipboard = text
self.report({'INFO'}, "Copied Angle & Plane Info to clipboard")
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 = "Sphere Angle Tool"
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_a = layout.box()
box_a.label(text="Coordinates", icon='DRIVER_TRANSFORM')
box_a.prop(props, "origin_pt", text="Origin (O)")
box_a.prop(props, "pt_a", text="Point A")
box_a.prop(props, "pt_b", text="Point B")
box_a.separator()
O = Vector(props.origin_pt).copy()
A = Vector(props.pt_a).copy()
B = Vector(props.pt_b).copy()
vec_a = A - O
vec_b = B - O
col_a = box_a.column(align=True)
angle_ab = 0.0
if vec_a.length > 1e-6 and vec_b.length > 1e-6:
try: angle_ab = math.degrees(vec_a.angle(vec_b))
except: pass
col_a.label(text=f"Angle (A-O-B): {angle_ab:.2f}°", icon='DRIVER_ROTATIONAL_DIFFERENCE')
plane_str = get_plane_equation_str(O, A, B)
col_a.label(text=f"Plane Eq: {plane_str}", icon='MESH_PLANE')
box_a.separator()
box_a.operator(OT_CopyAngleInfo.bl_idname, icon='COPYDOWN', text="Copy Full Angle Info")
if props.show_preview:
b2 = layout.box()
b2.label(text="Size & Color Settings", icon='COLOR')
b2.label(text="Origin Sphere", icon='DOT')
r1 = b2.row(align=True)
r1.prop(props, "origin_radius", text="Radius")
r1.prop(props, "origin_color", text="")
b2.label(text="Point A Sphere", icon='DOT')
r2 = b2.row(align=True)
r2.prop(props, "pt_a_radius", text="Radius")
r2.prop(props, "pt_a_color", text="")
b2.label(text="Point B Sphere", icon='DOT')
r3 = b2.row(align=True)
r3.prop(props, "pt_b_radius", text="Radius")
r3.prop(props, "pt_b_color", text="")
b2.separator()
b2.label(text="Arrow A (Origin -> A)", icon='FORWARD')
r4 = b2.row(align=True)
r4.prop(props, "arrow_a_thickness", text="Thickness")
r4.prop(props, "arrow_a_color", text="")
b2.label(text="Arrow B (Origin -> B)", icon='FORWARD')
r5 = b2.row(align=True)
r5.prop(props, "arrow_b_thickness", text="Thickness")
r5.prop(props, "arrow_b_color", text="")
layout.separator()
col_exec = layout.column()
col_exec.scale_y = 1.5
col_exec.operator(OT_CreateAngleObjects.bl_idname, icon='MESH_UVSPHERE', text="Create 3 Spheres & 2 Arrows")
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 (Unregister クリーン化適用)
# ==============================================================================
classes = (
PG_SphereProps, OT_CreateAngleObjects, OT_CopyFullScript, OT_CopyAngleInfo,
OT_OpenUrl, OT_RemoveAddon,
PT_MainPanel, PT_LinksPanel, PT_RemovePanel
)
def auto_open_sidebar():
try:
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
if not space.show_region_ui:
space.show_region_ui = True
except: pass
return None
def register():
for c in classes:
try: bpy.utils.register_class(c)
except: pass
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_SphereProps))
bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)
def unregister():
global _timer
if _timer:
try: bpy.app.timers.unregister(_timer)
except: pass
_timer = None
cleanup_preview_data()
try:
if hasattr(bpy.types.Scene, PROPS_NAME):
delattr(bpy.types.Scene, PROPS_NAME)
except: pass
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except: pass
if __name__ == "__main__":
register()