blender Million 2026
角度情報 20260405
# Copied: 2026-04-05 12: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": (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": (5.0000, 0.0000, 0.0000),
"pt_b": (0.0000, 5.0000, 0.0000),
"origin_color": (1.0000, 1.0000, 1.0000, 0.8000),
"origin_radius": 0.5000,
"pt_a_color": (1.0000, 0.1000, 0.1000, 0.8000),
"pt_a_radius": 0.5000,
"pt_b_color": (0.1000, 0.3000, 1.0000, 0.8000),
"pt_b_radius": 0.5000,
"arrow_a_color": (1.0000, 0.5000, 0.0000, 0.8000),
"arrow_a_thickness": 0.1500,
"arrow_b_color": (0.0000, 0.8000, 1.0000, 0.8000),
"arrow_b_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()
# Copied: 2026-04-05 10: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": (4, 1, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": "3 Spheres & 2 Arrows Angle Tool",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props_v10"
# ★ リンクトップを更新しました
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": (5.0000, 0.0000, 0.0000),
"pt_b": (0.0000, 5.0000, 0.0000),
"origin_color": (1.0000, 1.0000, 1.0000, 0.8000),
"origin_radius": 0.5000,
"pt_a_color": (1.0000, 0.1000, 0.1000, 0.8000),
"pt_a_radius": 0.5000,
"pt_b_color": (0.1000, 0.3000, 1.0000, 0.8000),
"pt_b_radius": 0.5000,
"arrow_a_color": (1.0000, 0.5000, 0.0000, 0.8000),
"arrow_a_thickness": 0.1500,
"arrow_b_color": (0.0000, 0.8000, 1.0000, 0.8000),
"arrow_b_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 cleanup_preview_data():
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if col:
for obj in list(col.objects):
mesh = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if mesh and mesh.users == 0:
bpy.data.meshes.remove(mesh)
bpy.data.collections.remove(col)
for mat_name in PREVIEW_MATS:
mat = bpy.data.materials.get(mat_name)
if mat and mat.users == 0:
bpy.data.materials.remove(mat)
# ==============================================================================
# 数学計算用関数(平面方程式など)エラー保護付き
# ==============================================================================
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 create_unique_material(color, name_prefix="Mat"):
timestamp = datetime.now().strftime('%M%S%f')[:5]
mat_name = f"{name_prefix}_{timestamp}"
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
mat.diffuse_color = color
if mat.use_nodes:
tree = mat.node_tree
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (0, 0)
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (300, 0)
tree.links.new(bsdf.outputs[0], out.inputs[0])
if "Base Color" in bsdf.inputs: bsdf.inputs['Base Color'].default_value = color
if "Alpha" in bsdf.inputs: bsdf.inputs['Alpha'].default_value = color[3]
return mat
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 = 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 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)
# 古いプレビューを完全にリセット
for obj in list(col.objects):
mesh = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if mesh and mesh.users == 0:
bpy.data.meshes.remove(mesh)
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)
# 1. 3つのSpheres
spheres_data = [
(props.origin_pt, props.origin_radius, props.origin_color, "Origin", PREVIEW_MATS[0]),
(props.pt_a, props.pt_a_radius, props.pt_a_color, "PtA", PREVIEW_MATS[1]),
(props.pt_b, 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=Vector(pt), verts=bm.verts)
create_prev_obj(name, bm, mat_name, c)
# 2. 2つのArrows
O = Vector(props.origin_pt)
arrows_data = [
(props.pt_a, props.arrow_a_thickness, props.arrow_a_color, "ArrowA", PREVIEW_MATS[3]),
(props.pt_b, 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()
target = Vector(pt)
vec = target - O
length = vec.length
if length > 1e-4:
Z = Vector((0,0,1))
try: rot = Z.rotation_difference(vec).to_matrix().to_4x4()
except: 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.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'])
c_pos_head = O + 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():
global _timer
_timer = None
if bpy.context and bpy.context.scene:
update_preview_geometry(bpy.context)
return None
def on_update(self, context):
global _timer
if _timer:
try: bpy.app.timers.unregister(_timer)
except: pass
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_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')
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=Vector(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 = Vector(loc_to) - Vector(loc_from)
length = vec.length
if length > 1e-4:
Z = Vector((0,0,1))
try: rot = Z.rotation_difference(vec).to_matrix().to_4x4()
except: rot = mathutils.Matrix.Identity(4)
head_len = min(length * 0.2, thick * 6)
body_len = length - head_len
if body_len > 0:
c_pos = Vector(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'])
c_pos_head = Vector(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", props.origin_pt, props.origin_radius, props.origin_color))
objs.append(create_sphere_obj("PointA", props.pt_a, props.pt_a_radius, props.pt_a_color))
objs.append(create_sphere_obj("PointB", props.pt_b, props.pt_b_radius, props.pt_b_color))
objs.append(create_arrow_obj("ArrowA", props.origin_pt, props.pt_a, props.arrow_a_thickness, props.arrow_a_color))
objs.append(create_arrow_obj("ArrowB", props.origin_pt, props.pt_b, 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)
A = Vector(props.pt_a)
B = Vector(props.pt_b)
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 (すべて1つのパネルに集約)
# ==============================================================================
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", 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)
A = Vector(props.pt_a)
B = Vector(props.pt_b)
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")
# -----------------------------
# 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.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="")
# -----------------------------
# 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 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
# ==============================================================================
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: bpy.utils.register_class(c)
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()
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes): bpy.utils.unregister_class(c)
if __name__ == "__main__":
register()