blender Million 2026
あっさり干渉計 20260312版
### [2026-03-12 17:00:00] Interferometer Generator (Ray Target 指定版)
円錐矢印(Bottom Rays)の「目指す点(収束点)」を自由に指定できるようにしました!
Bottom Rays Settings パネル内に **Target XYZ** というパラメータを追加しました。デフォルトはこれまで通り `(0, 0, 0)`(球体の中心)ですが、この数値を変更することで、指定した座標に向かってすべての矢印の向きと長さが自動計算されます。
以下のコードで更新してください。
```python
# Copied: 17:00:01
import bpy
import bmesh
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, IntProperty, EnumProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime
# ==============================================================================
# 設定エリア & ID管理
# ==============================================================================
PREFIX = "Interferometer20260312_v13"
TAB_NAME = "[ Interferometer ] "
# ### ZIONAD_SOURCE_ID: INTERFEROMETER_2026_03_12_V13 ###
bl_info = {
"name": f"zionad 520 [ Interferometer Gen ] {PREFIX}",
"author": "zionadchat",
"version": (5, 1, 4),
"blender": (5, 0, 0),
"location": "3D View > Sidebar",
"description": "5-Part Interferometer Sphere Generator (Custom Ray Target)",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: INTERFEROMETER_2026_03_12_V13 ###"
ADDON_LINKS = (
{"label": "あっさり干渉計 20260312版", "url": "<https://www.notion.so/20260312-320f5dacaf438031a63dd9fc00edc049>"},
{"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,
"sphere_loc": (0.0000, 0.0000, 0.0000),
"sphere_radius": 30.0000,
"cone_angle": 90.0000,
"split_offset": 5.0000,
"show_top_cap": True,
"cap_top_cap": True,
"color_front_top_cap": (0.8000, 0.2000, 0.2000, 0.8000),
"color_back_top_cap": (0.4000, 0.1000, 0.1000, 0.8000),
"show_top_cone": True,
"cap_top_cone": True,
"color_front_top_cone": (0.8000, 0.5000, 0.2000, 0.8000),
"color_back_top_cone": (0.4000, 0.2500, 0.1000, 0.8000),
"show_mid": True,
"cap_mid": True,
"color_front_mid": (0.2000, 0.8000, 0.2000, 0.8000),
"color_back_mid": (0.1000, 0.4000, 0.1000, 0.8000),
"show_bot_cone": True,
"cap_bot_cone": True,
"color_front_bot_cone": (0.2000, 0.5000, 0.8000, 0.8000),
"color_back_bot_cone": (0.1000, 0.2500, 0.4000, 0.8000),
"show_bot_cap": True,
"cap_bot_cap": True,
"color_front_bot_cap": (0.5000, 0.2000, 0.8000, 0.8000),
"color_back_bot_cap": (0.2500, 0.1000, 0.4000, 0.8000),
"show_bot_rays": True,
"ray_count": 36,
"ray_thickness": 0.5000,
"ray_offset": 10.0000,
"ray_target": (0.0000, 0.0000, 0.0000),
"color_front_bot_rays": (0.9000, 0.9000, 0.2000, 0.8000),
"color_back_bot_rays": (0.5000, 0.5000, 0.1000, 0.8000),
"show_disc": True,
"disc_z": 0.0000,
"disc_thickness": 1.0000,
"disc_radius_mode": "RELATIVE",
"disc_radius_pct": 50.0000,
"disc_manual_radius": 20.0000,
"color_front_disc": (0.2000, 0.9000, 0.9000, 0.8000),
"color_back_disc": (0.1000, 0.5000, 0.5000, 0.8000),
}
# <END_DICT>
# ==============================================================================
# マテリアル作成ロジック
# ==============================================================================
def apply_material_color(mat, front_color, back_color):
mat.use_nodes = True
f_col = list(front_color)
b_col = list(back_color)
tree = mat.node_tree
tree.nodes.clear()
try:
bsdf_front = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf_front.location = (-200, 150)
bsdf_front.inputs["Base Color"].default_value = f_col
bsdf_front.inputs["Alpha"].default_value = f_col[3]
bsdf_back = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf_back.location = (-200, -150)
bsdf_back.inputs["Base Color"].default_value = b_col
bsdf_back.inputs["Alpha"].default_value = b_col[3]
mix_shader = tree.nodes.new("ShaderNodeMixShader")
mix_shader.location = (100, 0)
geom = tree.nodes.new("ShaderNodeNewGeometry")
geom.location = (-400, 300)
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (300, 0)
tree.links.new(geom.outputs["Backfacing"], mix_shader.inputs[0])
tree.links.new(bsdf_front.outputs["BSDF"], mix_shader.inputs[1])
tree.links.new(bsdf_back.outputs["BSDF"], mix_shader.inputs[2])
tree.links.new(mix_shader.outputs["Shader"], out.inputs["Surface"])
except Exception as e:
print("Material Error 5.0+:", e)
mat.diffuse_color = f_col
def create_unique_material(front_color, back_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)
apply_material_color(mat, front_color, back_color)
return mat
def get_preview_material(front_color, back_color, name="Mat_Prev"):
mat = bpy.data.materials.get(name)
if not mat: mat = bpy.data.materials.new(name=name)
apply_material_color(mat, front_color, back_color)
return mat
# ==============================================================================
# ジオメトリ構築ロジック
# ==============================================================================
def build_base_meshes(props, prefix):
meshes = {}
R = props.sphere_radius
theta = math.radians(props.cone_angle / 2)
H = R * math.cos(theta)
r_base = R * math.sin(theta)
def make_mesh(name, bm):
mesh = bpy.data.meshes.get(name)
if not mesh: mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
return mesh
# Base Spheres & Cones
for key in ['base_top', 'base_mid', 'base_bot']:
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=R)
meshes[key] = make_mesh(f"{prefix}_{key}", bm)
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=props.cap_top_cone, cap_tris=True, segments=64, radius1=0.0, radius2=r_base, depth=H)
bmesh.ops.translate(bm, vec=(0, 0, H/2), verts=bm.verts)
meshes['cone_top'] = make_mesh(f"{prefix}_ConeTop", bm)
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=props.cap_bot_cone, cap_tris=True, segments=64, radius1=r_base, radius2=0.0, depth=H)
bmesh.ops.translate(bm, vec=(0, 0, -H/2), verts=bm.verts)
meshes['cone_bot'] = make_mesh(f"{prefix}_ConeBot", bm)
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=(R*4, R*4, R*4), verts=bm.verts)
bmesh.ops.translate(bm, vec=(0, 0, H + 2*R), verts=bm.verts)
meshes['box_top'] = make_mesh(f"{prefix}_BoxTop", bm)
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=(R*4, R*4, R*4), verts=bm.verts)
bmesh.ops.translate(bm, vec=(0, 0, -H - 2*R), verts=bm.verts)
meshes['box_bot'] = make_mesh(f"{prefix}_BoxBot", bm)
# --- Bottom Rays ---
bm_rays = bmesh.new()
count = props.ray_count
offset = props.ray_offset
thickness = props.ray_thickness
target = Vector(props.ray_target) # 新規追加:目標座標
z_min, z_max = -R, -H
golden_ratio = (1 + 5 ** 0.5) / 2
for i in range(count):
z = z_min + (z_max - z_min) * ((i + 0.5) / count) if count > 0 else z_min
r = math.sqrt(max(0, R**2 - z**2))
phi = 2 * math.pi * i / golden_ratio
x, y = r * math.cos(phi), r * math.sin(phi)
P = Vector((x, y, z)) # 表面の座標
vec = target - P # ターゲットへのベクトル
dist = vec.length
depth = dist - offset
if depth <= 0.1: continue
dir_vec = vec.normalized()
A = target - dir_vec * offset # オフセットを加味した頂点の座標
geom = bmesh.ops.create_cone(bm_rays, cap_ends=True, cap_tris=True, segments=12, radius1=thickness, radius2=0, depth=depth)
verts = [v for v in geom['verts'] if isinstance(v, bmesh.types.BMVert)]
rot = Vector((0,0,1)).rotation_difference(dir_vec)
bmesh.ops.rotate(bm_rays, cent=(0,0,0), matrix=rot.to_matrix(), verts=verts)
mid = (P + A) / 2
bmesh.ops.translate(bm_rays, vec=mid, verts=verts)
meshes['bot_rays'] = make_mesh(f"{prefix}_BotRays", bm_rays)
# --- Slice Disc ---
bm_disc = bmesh.new()
d_z = props.disc_z
if props.disc_radius_mode == 'RELATIVE':
r_slice = math.sqrt(max(0, R**2 - d_z**2))
d_rad = r_slice * (props.disc_radius_pct / 100.0)
else:
d_rad = props.disc_manual_radius
bmesh.ops.create_cone(bm_disc, cap_ends=True, cap_tris=True, segments=64, radius1=d_rad, radius2=d_rad, depth=props.disc_thickness)
meshes['disc'] = make_mesh(f"{prefix}_Disc", bm_disc)
return meshes
def setup_cutters(meshes, loc, offset_v, collection, is_preview=False):
cutters = {}
names = [('box_top', "Cut_BoxTop"), ('box_bot', "Cut_BoxBot")]
for key, name in names:
obj = bpy.data.objects.new(name, meshes[key])
obj.display_type = 'BOUNDS'
obj.hide_viewport = True; obj.hide_render = True
if is_preview: obj[PREVIEW_TAG] = True
collection.objects.link(obj)
cutters[key] = obj
cutters['box_top'].location = loc + Vector((0, 0, offset_v * 2))
cutters['box_bot'].location = loc + Vector((0, 0, -offset_v * 2))
return cutters
def setup_parts(meshes, cutters, loc, offset_v, collection, is_preview=False):
parts = {}
parts['top_cap'] = bpy.data.objects.new("Part_TopCap", meshes['base_top'])
parts['top_cone'] = bpy.data.objects.new("Part_TopCone", meshes['cone_top'])
parts['mid'] = bpy.data.objects.new("Part_Middle", meshes['base_mid'])
parts['bot_cone'] = bpy.data.objects.new("Part_BotCone", meshes['cone_bot'])
parts['bot_cap'] = bpy.data.objects.new("Part_BotCap", meshes['base_bot'])
parts['bot_rays'] = bpy.data.objects.new("Part_BotRays", meshes['bot_rays'])
parts['disc'] = bpy.data.objects.new("Part_Disc", meshes['disc'])
for obj in parts.values():
if is_preview: obj[PREVIEW_TAG] = True
collection.objects.link(obj)
parts['top_cap'].location = loc + Vector((0, 0, offset_v * 2))
parts['top_cone'].location = loc + Vector((0, 0, offset_v * 1))
parts['mid'].location = loc
parts['bot_cone'].location = loc + Vector((0, 0, -offset_v * 1))
parts['bot_cap'].location = loc + Vector((0, 0, -offset_v * 2))
parts['bot_rays'].location = loc + Vector((0, 0, -offset_v * 2))
parts['disc'].location = loc + Vector((0, 0, getattr(bpy.context.scene, PROPS_NAME).disc_z))
mod = parts['top_cap'].modifiers.new("Bool", 'BOOLEAN')
mod.operation = 'INTERSECT'; mod.object = cutters['box_top']; mod.solver = 'EXACT'
mod = parts['bot_cap'].modifiers.new("Bool", 'BOOLEAN')
mod.operation = 'INTERSECT'; mod.object = cutters['box_bot']; mod.solver = 'EXACT'
mod = parts['mid'].modifiers.new("Bool1", 'BOOLEAN')
mod.operation = 'DIFFERENCE'; mod.object = cutters['box_top']; mod.solver = 'EXACT'
mod = parts['mid'].modifiers.new("Bool2", 'BOOLEAN')
mod.operation = 'DIFFERENCE'; mod.object = cutters['box_bot']; mod.solver = 'EXACT'
return parts
def remove_flat_faces(mesh, normal_z_targets, threshold=0.01):
bm = bmesh.new()
bm.from_mesh(mesh)
faces_to_remove = []
for f in bm.faces:
for nz in normal_z_targets:
if abs(f.normal.z - nz) < threshold:
faces_to_remove.append(f)
break
if faces_to_remove:
bmesh.ops.delete(bm, geom=faces_to_remove, context='FACES')
bm.to_mesh(mesh)
bm.free()
def apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=False):
context.view_layer.update()
dg = context.evaluated_depsgraph_get()
for key in ['top_cap', 'mid', 'bot_cap']:
obj = parts.get(key)
if not obj: continue
try:
eval_obj = obj.evaluated_get(dg)
new_mesh = bpy.data.meshes.new_from_object(eval_obj)
if is_preview: new_mesh.name = f"Prev_Applied_{key}"
old_mesh = obj.data
obj.modifiers.clear()
obj.data = new_mesh
if old_mesh.users == 0: bpy.data.meshes.remove(old_mesh)
if key == 'top_cap' and not getattr(props, 'cap_top_cap', True):
remove_flat_faces(new_mesh, [-1.0])
elif key == 'bot_cap' and not getattr(props, 'cap_bot_cap', True):
remove_flat_faces(new_mesh, [1.0])
elif key == 'mid' and not getattr(props, 'cap_mid', True):
remove_flat_faces(new_mesh, [1.0, -1.0])
except Exception as e:
print(f"Mod Apply Error [{key}]:", e)
for obj in cutters.values():
m = obj.data
try:
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0: bpy.data.meshes.remove(m)
except: pass
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_TAG = f"{PREFIX}_preview_tag"
def clear_preview_data(prefix):
col = bpy.data.collections.get(f"{prefix}_Preview_Zone")
if col:
for o in list(col.objects):
m = o.data
bpy.data.objects.remove(o, do_unlink=True)
if m and getattr(m, "users", 0) == 0: bpy.data.meshes.remove(m)
for m in list(bpy.data.meshes):
if (m.name.startswith(f"Prev_{prefix}_") or m.name.startswith("Prev_Applied_")) and m.users == 0:
bpy.data.meshes.remove(m)
def update_preview_geometry(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
clear_preview_data(PREFIX)
if not props.show_preview:
context.view_layer.update(); return
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_NAME)
context.scene.collection.children.link(col)
meshes = build_base_meshes(props, f"Prev_{PREFIX}")
loc = Vector(props.sphere_loc)
offset_v = props.split_offset
cutters = setup_cutters(meshes, loc, offset_v, col, is_preview=True)
parts = setup_parts(meshes, cutters, loc, offset_v, col, is_preview=True)
apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=True)
parts_config = [
('top_cap', props.show_top_cap, props.color_front_top_cap, props.color_back_top_cap, "TCap"),
('top_cone', props.show_top_cone, props.color_front_top_cone, props.color_back_top_cone, "TCone"),
('mid', props.show_mid, props.color_front_mid, props.color_back_mid, "Mid"),
('bot_cone', props.show_bot_cone, props.color_front_bot_cone, props.color_back_bot_cone, "BCone"),
('bot_cap', props.show_bot_cap, props.color_front_bot_cap, props.color_back_bot_cap, "BCap"),
('bot_rays', props.show_bot_rays, props.color_front_bot_rays, props.color_back_bot_rays, "BRays"),
('disc', props.show_disc, props.color_front_disc, props.color_back_disc, "Disc"),
]
for key, is_show, c_front, c_back, mat_name in parts_config:
obj = parts.get(key)
if not obj: continue
try:
if is_show:
mat = get_preview_material(c_front, c_back, f"Mat_Prev_{mat_name}")
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
else:
m = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0: bpy.data.meshes.remove(m)
except Exception as e: print(f"Visibility Error[{key}]:", e)
context.view_layer.update()
_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)
sphere_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['sphere_loc'], update=on_update)
sphere_radius: FloatProperty(name="Sphere Radius", default=CURRENT_DEFAULTS['sphere_radius'], min=0.01, update=on_update)
cone_angle: FloatProperty(name="Cone Angle", default=CURRENT_DEFAULTS['cone_angle'], min=1.0, max=179.0, update=on_update)
split_offset: FloatProperty(name="Split Z-Offset", default=CURRENT_DEFAULTS['split_offset'], min=0.0, update=on_update)
show_top_cap: BoolProperty(name="Top Cap", default=CURRENT_DEFAULTS['show_top_cap'], update=on_update)
cap_top_cap: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_top_cap'], update=on_update)
color_front_top_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_top_cap'], update=on_update)
color_back_top_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_top_cap'], update=on_update)
show_top_cone: BoolProperty(name="Top Cone", default=CURRENT_DEFAULTS['show_top_cone'], update=on_update)
cap_top_cone: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_top_cone'], update=on_update)
color_front_top_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_top_cone'], update=on_update)
color_back_top_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_top_cone'], update=on_update)
show_mid: BoolProperty(name="Middle Sphere", default=CURRENT_DEFAULTS['show_mid'], update=on_update)
cap_mid: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_mid'], update=on_update)
color_front_mid: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_mid'], update=on_update)
color_back_mid: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_mid'], update=on_update)
show_bot_cone: BoolProperty(name="Bottom Cone", default=CURRENT_DEFAULTS['show_bot_cone'], update=on_update)
cap_bot_cone: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_bot_cone'], update=on_update)
color_front_bot_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_bot_cone'], update=on_update)
color_back_bot_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_bot_cone'], update=on_update)
show_bot_cap: BoolProperty(name="Bottom Cap", default=CURRENT_DEFAULTS['show_bot_cap'], update=on_update)
cap_bot_cap: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_bot_cap'], update=on_update)
color_front_bot_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_bot_cap'], update=on_update)
color_back_bot_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_bot_cap'], update=on_update)
show_bot_rays: BoolProperty(name="Bottom Rays", default=CURRENT_DEFAULTS['show_bot_rays'], update=on_update)
ray_count: IntProperty(name="Ray Count", default=CURRENT_DEFAULTS['ray_count'], min=1, max=72, update=on_update)
ray_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['ray_thickness'], min=0.01, update=on_update)
ray_offset: FloatProperty(name="Apex Offset", default=CURRENT_DEFAULTS['ray_offset'], min=0.0, update=on_update)
ray_target: FloatVectorProperty(name="Target XYZ", size=3, default=CURRENT_DEFAULTS['ray_target'], update=on_update)
color_front_bot_rays: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_bot_rays'], update=on_update)
color_back_bot_rays: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_bot_rays'], update=on_update)
show_disc: BoolProperty(name="Slice Disc", default=CURRENT_DEFAULTS['show_disc'], update=on_update)
disc_z: FloatProperty(name="Z Position", default=CURRENT_DEFAULTS['disc_z'], update=on_update)
disc_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['disc_thickness'], min=0.01, update=on_update)
disc_radius_mode: EnumProperty(
name="Radius Mode",
items=[('RELATIVE', "Auto (%)", "Percentage of the sphere cross-section radius"),
('ABSOLUTE', "Manual", "Input radius manually")],
default=CURRENT_DEFAULTS['disc_radius_mode'], update=on_update
)
disc_radius_pct: FloatProperty(name="Radius %", default=CURRENT_DEFAULTS['disc_radius_pct'], min=0.0, max=500.0, update=on_update)
disc_manual_radius: FloatProperty(name="Absolute Radius", default=CURRENT_DEFAULTS['disc_manual_radius'], min=0.01, update=on_update)
color_front_disc: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_disc'], update=on_update)
color_back_disc: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_disc'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CreateSphere(Operator):
bl_idname = f"{OP_PREFIX}.create_sphere"
bl_label = "Create Selected Parts"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
timestamp = datetime.now().strftime('%H%M%S')
meshes = build_base_meshes(props, f"Temp_{timestamp}")
loc = Vector(props.sphere_loc)
offset_v = props.split_offset
col = context.collection if context.collection else context.scene.collection
cutters = setup_cutters(meshes, loc, offset_v, col, is_preview=False)
parts = setup_parts(meshes, cutters, loc, offset_v, col, is_preview=False)
apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=False)
parts_config = [
('top_cap', props.show_top_cap, props.color_front_top_cap, props.color_back_top_cap, "TopCap"),
('top_cone', props.show_top_cone, props.color_front_top_cone, props.color_back_top_cone, "TopCone"),
('mid', props.show_mid, props.color_front_mid, props.color_back_mid, "Middle"),
('bot_cone', props.show_bot_cone, props.color_front_bot_cone, props.color_back_bot_cone, "BotCone"),
('bot_cap', props.show_bot_cap, props.color_front_bot_cap, props.color_back_bot_cap, "BotCap"),
('bot_rays', props.show_bot_rays, props.color_front_bot_rays, props.color_back_bot_rays, "BotRays"),
('disc', props.show_disc, props.color_front_disc, props.color_back_disc, "SliceDisc"),
]
active_obj = None
bpy.ops.object.select_all(action='DESELECT')
for key, is_show, c_front, c_back, mat_name in parts_config:
obj = parts.get(key)
if not obj: continue
if is_show:
obj.data.materials.append(create_unique_material(c_front, c_back, f"Mat_{mat_name}"))
obj.name = f"Sphere_{mat_name}_{timestamp}"
obj.select_set(True)
active_obj = obj
else:
m = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0: bpy.data.meshes.remove(m)
for k, m in list(meshes.items()):
if m and m.users == 0: bpy.data.meshes.remove(m)
if active_obj: context.view_layer.objects.active = active_obj
self.report({'INFO'}, "Created Interferometer Parts")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string():
target_text = t; break
if not target_text:
self.report({'ERROR'}, "Script source not found.")
return {'CANCELLED'}
code = target_text.as_string()
l = props.sphere_loc
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "sphere_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
new_dict += f' "sphere_radius": {props.sphere_radius:.4f},\n'
new_dict += f' "cone_angle": {props.cone_angle:.4f},\n'
new_dict += f' "split_offset": {props.split_offset:.4f},\n'
for key in ["top_cap", "top_cone", "mid", "bot_cone", "bot_cap", "bot_rays"]:
s = getattr(props, f"show_{key}")
cf = getattr(props, f"color_front_{key}")
cb = getattr(props, f"color_back_{key}")
new_dict += f' "show_{key}": {s},\n'
if hasattr(props, f"cap_{key}"):
cap_s = getattr(props, f"cap_{key}")
new_dict += f' "cap_{key}": {cap_s},\n'
if key == "bot_rays":
new_dict += f' "ray_count": {props.ray_count},\n'
new_dict += f' "ray_thickness": {props.ray_thickness:.4f},\n'
new_dict += f' "ray_offset": {props.ray_offset:.4f},\n'
t = props.ray_target
new_dict += f' "ray_target": ({t[0]:.4f}, {t[1]:.4f}, {t[2]:.4f}),\n'
new_dict += f' "color_front_{key}": ({cf[0]:.4f}, {cf[1]:.4f}, {cf[2]:.4f}, {cf[3]:.4f}),\n'
new_dict += f' "color_back_{key}": ({cb[0]:.4f}, {cb[1]:.4f}, {cb[2]:.4f}, {cb[3]:.4f}),\n'
new_dict += f' "show_disc": {props.show_disc},\n'
new_dict += f' "disc_z": {props.disc_z:.4f},\n'
new_dict += f' "disc_thickness": {props.disc_thickness:.4f},\n'
new_dict += f' "disc_radius_mode": "{props.disc_radius_mode}",\n'
new_dict += f' "disc_radius_pct": {props.disc_radius_pct:.4f},\n'
new_dict += f' "disc_manual_radius": {props.disc_manual_radius:.4f},\n'
cf = props.color_front_disc
cb = props.color_back_disc
new_dict += f' "color_front_disc": ({cf[0]:.4f}, {cf[1]:.4f}, {cf[2]:.4f}, {cf[3]:.4f}),\n'
new_dict += f' "color_back_disc": ({cb[0]:.4f}, {cb[1]:.4f}, {cb[2]:.4f}, {cb[3]:.4f}),\n'
new_dict += "}\n"
try:
start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
pre, post = code.split(start)[0], code.split(end)[1]
final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
context.window_manager.clipboard = final
self.report({'INFO'}, "Code copied!")
except: return {'CANCELLED'}
return {'FINISHED'}
class OT_Reset(Operator):
bl_idname = f"{OP_PREFIX}.reset"
bl_label = "Reset"
def execute(self, context):
p = getattr(context.scene, PROPS_NAME)
p.sphere_loc = (0,0,0)
p.sphere_radius = 30.0; p.cone_angle = 90.0; p.split_offset = 5.0
p.show_top_cap = True; p.cap_top_cap = True; p.color_front_top_cap = (0.8, 0.2, 0.2, 0.8); p.color_back_top_cap = (0.4, 0.1, 0.1, 0.8)
p.show_top_cone = True; p.cap_top_cone = True; p.color_front_top_cone = (0.8, 0.5, 0.2, 0.8); p.color_back_top_cone = (0.4, 0.25, 0.1, 0.8)
p.show_mid = True; p.cap_mid = True; p.color_front_mid = (0.2, 0.8, 0.2, 0.8); p.color_back_mid = (0.1, 0.4, 0.1, 0.8)
p.show_bot_cone = True; p.cap_bot_cone = True; p.color_front_bot_cone = (0.2, 0.5, 0.8, 0.8); p.color_back_bot_cone = (0.1, 0.25, 0.4, 0.8)
p.show_bot_cap = True; p.cap_bot_cap = True; p.color_front_bot_cap = (0.5, 0.2, 0.8, 0.8); p.color_back_bot_cap = (0.25, 0.1, 0.4, 0.8)
p.show_bot_rays = True; p.ray_count = 36; p.ray_thickness = 0.5; p.ray_offset = 10.0; p.ray_target = (0,0,0)
p.color_front_bot_rays = (0.9, 0.9, 0.2, 0.8); p.color_back_bot_rays = (0.5, 0.5, 0.1, 0.8)
p.show_disc = True; p.disc_z = 0.0; p.disc_thickness = 1.0; p.disc_radius_mode = 'RELATIVE'; p.disc_radius_pct = 50.0; p.disc_manual_radius = 20.0
p.color_front_disc = (0.2, 0.9, 0.9, 0.8); p.color_back_disc = (0.1, 0.5, 0.5, 0.8)
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 = "Interferometer Generator"
bl_idname = f"{PREFIX}_PT_main"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: layout.label(text="Reload Script"); return
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
box = layout.box()
if not props.show_preview: box.label(text="Preview is Hidden", icon='INFO')
box.prop(props, "sphere_loc")
box.prop(props, "sphere_radius")
box.prop(props, "cone_angle")
box.prop(props, "split_offset")
layout.separator()
parts_box = layout.box()
parts_box.label(text="Parts Selection & Colors", icon='MATERIAL')
parts_ui = [
("top_cap", "Top Cap", "cap_top_cap"),
("top_cone", "Top Cone", "cap_top_cone"),
("mid", "Middle Sphere", "cap_mid"),
("bot_cone", "Bottom Cone", "cap_bot_cone"),
("bot_cap", "Bottom Cap", "cap_bot_cap"),
]
for key, label, cap_prop in parts_ui:
is_show = getattr(props, f"show_{key}")
icon = 'RESTRICT_VIEW_OFF' if is_show else 'RESTRICT_VIEW_ON'
p_box = parts_box.box()
row = p_box.row(align=True)
row.prop(props, f"show_{key}", icon=icon, text=label)
if cap_prop: row.prop(props, cap_prop, text="Base", toggle=True)
if is_show:
c_row = p_box.row(align=True)
c_row.prop(props, f"color_front_{key}", text="Front")
c_row.prop(props, f"color_back_{key}", text="Back")
layout.separator()
layout.operator(OT_Reset.bl_idname, icon='LOOP_BACK', text="Reset Values")
layout.separator()
col = layout.column()
col.scale_y = 1.5
col.operator(OT_CreateSphere.bl_idname, icon='MESH_UVSPHERE', text="Create Selected Parts")
class PT_RaysPanel(Panel):
bl_label = "Bottom Rays Settings"
bl_idname = f"{PREFIX}_PT_rays"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
box = layout.box()
icon = 'RESTRICT_VIEW_OFF' if props.show_bot_rays else 'RESTRICT_VIEW_ON'
box.prop(props, "show_bot_rays", icon=icon, text="Enable Bottom Rays")
if props.show_bot_rays:
col = box.column(align=True)
col.prop(props, "ray_count")
col.prop(props, "ray_thickness")
col.prop(props, "ray_offset")
col.prop(props, "ray_target") # 新しいターゲット入力項目
c_box = box.box()
c_row = c_box.row(align=True)
c_row.prop(props, "color_front_bot_rays", text="Front")
c_row.prop(props, "color_back_bot_rays", text="Back")
class PT_DiscPanel(Panel):
bl_label = "Slice Disc Settings"
bl_idname = f"{PREFIX}_PT_disc"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
box = layout.box()
icon = 'RESTRICT_VIEW_OFF' if props.show_disc else 'RESTRICT_VIEW_ON'
box.prop(props, "show_disc", icon=icon, text="Enable Slice Disc")
if props.show_disc:
col = box.column(align=True)
col.prop(props, "disc_z")
col.prop(props, "disc_thickness")
col.separator()
row = col.row(align=True)
row.prop(props, "disc_radius_mode", expand=True)
if props.disc_radius_mode == 'RELATIVE':
col.prop(props, "disc_radius_pct")
R = props.sphere_radius
d_z = props.disc_z
r_slice = math.sqrt(max(0, R**2 - d_z**2))
calc_rad = r_slice * (props.disc_radius_pct / 100.0)
col.label(text=f"Calculated Radius: {calc_rad:.2f}", icon='INFO')
else:
col.prop(props, "disc_manual_radius")
c_box = box.box()
c_row = c_box.row(align=True)
c_row.prop(props, "color_front_disc", text="Front")
c_row.prop(props, "color_back_disc", text="Back")
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_CreateSphere, OT_CopyFullScript, OT_Reset, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, PT_RaysPanel, PT_DiscPanel, PT_LinksPanel, PT_RemovePanel)
def init_preview():
if bpy.context and hasattr(bpy.context, 'scene'):
props = getattr(bpy.context.scene, PROPS_NAME, None)
if props and props.show_preview:
update_preview_geometry(bpy.context)
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(init_preview, first_interval=0.2)
def unregister():
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes): bpy.utils.unregister_class(c)
if __name__ == "__main__": register()
```jsx
# Copied: 04:02:56
# Copied: 16:00:01
import bpy
import bmesh
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime
# ==============================================================================
# 設定エリア & ID管理
# ==============================================================================
PREFIX = "Interferometer20260312_v10"
TAB_NAME = "[ Interferometer ] "
# ### ZIONAD_SOURCE_ID: INTERFEROMETER_2026_03_12_V10 ###
bl_info = {
"name": f"zionad 520 [ Interferometer Gen ] {PREFIX}",
"author": "zionadchat",
"version": (5, 1, 1),
"blender": (5, 0, 0),
"location": "3D View > Sidebar",
"description": "5-Part Interferometer Sphere Generator (Auto-Preview Fix)",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: INTERFEROMETER_2026_03_12_V10 ###"
ADDON_LINKS = (
{"label": "あっさり干渉計 20260312版", "url": "<https://www.notion.so/20260312-320f5dacaf438031a63dd9fc00edc049>"},
{"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,
"sphere_loc": (0.0000, 0.0000, 0.0000),
"sphere_radius": 30.0000,
"cone_angle": 178.8900,
"split_offset": 0.0000,
"show_top_cap": False,
"cap_top_cap": True,
"color_front_top_cap": (0.8000, 0.2000, 0.2000, 0.8000),
"color_back_top_cap": (0.4000, 0.1000, 0.1000, 0.8000),
"show_top_cone": False,
"cap_top_cone": True,
"color_front_top_cone": (0.8000, 0.5000, 0.2000, 0.8000),
"color_back_top_cone": (0.4000, 0.2500, 0.1000, 0.8000),
"show_mid": False,
"cap_mid": True,
"color_front_mid": (0.2000, 0.8000, 0.2000, 0.8000),
"color_back_mid": (0.1000, 0.4000, 0.1000, 0.8000),
"show_bot_cone": False,
"cap_bot_cone": True,
"color_front_bot_cone": (0.2000, 0.5000, 0.8000, 0.8000),
"color_back_bot_cone": (0.1000, 0.2500, 0.4000, 0.8000),
"show_bot_cap": True,
"cap_bot_cap": False,
"color_front_bot_cap": (0.8000, 0.0158, 0.7195, 1.0000),
"color_back_bot_cap": (0.0379, 0.0173, 0.0576, 1.0000),
"show_bot_rays": True,
"ray_count": 22,
"ray_thickness": 2.6000,
"ray_offset": 10.0000,
"color_front_bot_rays": (0.9000, 0.9000, 0.2000, 0.8000),
"color_back_bot_rays": (0.5000, 0.5000, 0.1000, 0.8000),
}
# <END_DICT>
# ==============================================================================
# マテリアル作成ロジック (Blender 5.0+ 完全対応版)
# ==============================================================================
def apply_material_color(mat, front_color, back_color):
mat.use_nodes = True
f_col = list(front_color)
b_col = list(back_color)
tree = mat.node_tree
tree.nodes.clear()
try:
bsdf_front = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf_front.location = (-200, 150)
bsdf_front.inputs["Base Color"].default_value = f_col
bsdf_front.inputs["Alpha"].default_value = f_col[3]
bsdf_back = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf_back.location = (-200, -150)
bsdf_back.inputs["Base Color"].default_value = b_col
bsdf_back.inputs["Alpha"].default_value = b_col[3]
mix_shader = tree.nodes.new("ShaderNodeMixShader")
mix_shader.location = (100, 0)
geom = tree.nodes.new("ShaderNodeNewGeometry")
geom.location = (-400, 300)
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (300, 0)
tree.links.new(geom.outputs["Backfacing"], mix_shader.inputs[0])
tree.links.new(bsdf_front.outputs["BSDF"], mix_shader.inputs[1])
tree.links.new(bsdf_back.outputs["BSDF"], mix_shader.inputs[2])
tree.links.new(mix_shader.outputs["Shader"], out.inputs["Surface"])
except Exception as e:
print("Material Error 5.0+:", e)
mat.diffuse_color = f_col
def create_unique_material(front_color, back_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)
apply_material_color(mat, front_color, back_color)
return mat
def get_preview_material(front_color, back_color, name="Mat_Prev"):
mat = bpy.data.materials.get(name)
if not mat: mat = bpy.data.materials.new(name=name)
apply_material_color(mat, front_color, back_color)
return mat
# ==============================================================================
# ジオメトリ構築ロジック
# ==============================================================================
def build_base_meshes(props, prefix):
meshes = {}
R = props.sphere_radius
theta = math.radians(props.cone_angle / 2)
H = R * math.cos(theta)
r_base = R * math.sin(theta)
def make_mesh(name, bm):
mesh = bpy.data.meshes.get(name)
if not mesh: mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
return mesh
# Base Spheres & Cones
for key in ['base_top', 'base_mid', 'base_bot']:
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=R)
meshes[key] = make_mesh(f"{prefix}_{key}", bm)
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=props.cap_top_cone, cap_tris=True, segments=64, radius1=0.0, radius2=r_base, depth=H)
bmesh.ops.translate(bm, vec=(0, 0, H/2), verts=bm.verts)
meshes['cone_top'] = make_mesh(f"{prefix}_ConeTop", bm)
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=props.cap_bot_cone, cap_tris=True, segments=64, radius1=r_base, radius2=0.0, depth=H)
bmesh.ops.translate(bm, vec=(0, 0, -H/2), verts=bm.verts)
meshes['cone_bot'] = make_mesh(f"{prefix}_ConeBot", bm)
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=(R*4, R*4, R*4), verts=bm.verts)
bmesh.ops.translate(bm, vec=(0, 0, H + 2*R), verts=bm.verts)
meshes['box_top'] = make_mesh(f"{prefix}_BoxTop", bm)
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=(R*4, R*4, R*4), verts=bm.verts)
bmesh.ops.translate(bm, vec=(0, 0, -H - 2*R), verts=bm.verts)
meshes['box_bot'] = make_mesh(f"{prefix}_BoxBot", bm)
# --- Bottom Rays Generation (Fibonacci Lattice) ---
bm_rays = bmesh.new()
count = props.ray_count
offset = props.ray_offset
thickness = props.ray_thickness
z_min = -R
z_max = -H
golden_ratio = (1 + 5 ** 0.5) / 2
for i in range(count):
# 等面積になるようにZ軸を分割
z = z_min + (z_max - z_min) * ((i + 0.5) / count) if count > 0 else z_min
r = math.sqrt(max(0, R**2 - z**2))
phi = 2 * math.pi * i / golden_ratio
x = r * math.cos(phi)
y = r * math.sin(phi)
u = Vector((x, y, z)).normalized()
P = u * R # 底面中心 (球体表面)
A = u * offset # 頂点 (中心からのオフセット)
depth = R - offset
if depth <= 0.1: continue
geom = bmesh.ops.create_cone(bm_rays, cap_ends=True, cap_tris=True, segments=12, radius1=thickness, radius2=0, depth=depth)
verts = [v for v in geom['verts'] if isinstance(v, bmesh.types.BMVert)]
rot = Vector((0,0,1)).rotation_difference(-u)
bmesh.ops.rotate(bm_rays, cent=(0,0,0), matrix=rot.to_matrix(), verts=verts)
mid = (P + A) / 2
bmesh.ops.translate(bm_rays, vec=mid, verts=verts)
meshes['bot_rays'] = make_mesh(f"{prefix}_BotRays", bm_rays)
return meshes
def setup_cutters(meshes, loc, offset_v, collection, is_preview=False):
cutters = {}
names = [('box_top', "Cut_BoxTop"), ('box_bot', "Cut_BoxBot")]
for key, name in names:
obj = bpy.data.objects.new(name, meshes[key])
obj.display_type = 'BOUNDS'
obj.hide_viewport = True; obj.hide_render = True
if is_preview: obj[PREVIEW_TAG] = True
collection.objects.link(obj)
cutters[key] = obj
cutters['box_top'].location = loc + Vector((0, 0, offset_v * 2))
cutters['box_bot'].location = loc + Vector((0, 0, -offset_v * 2))
return cutters
def setup_parts(meshes, cutters, loc, offset_v, collection, is_preview=False):
parts = {}
parts['top_cap'] = bpy.data.objects.new("Part_TopCap", meshes['base_top'])
parts['top_cone'] = bpy.data.objects.new("Part_TopCone", meshes['cone_top'])
parts['mid'] = bpy.data.objects.new("Part_Middle", meshes['base_mid'])
parts['bot_cone'] = bpy.data.objects.new("Part_BotCone", meshes['cone_bot'])
parts['bot_cap'] = bpy.data.objects.new("Part_BotCap", meshes['base_bot'])
parts['bot_rays'] = bpy.data.objects.new("Part_BotRays", meshes['bot_rays'])
for obj in parts.values():
if is_preview: obj[PREVIEW_TAG] = True
collection.objects.link(obj)
parts['top_cap'].location = loc + Vector((0, 0, offset_v * 2))
parts['top_cone'].location = loc + Vector((0, 0, offset_v * 1))
parts['mid'].location = loc
parts['bot_cone'].location = loc + Vector((0, 0, -offset_v * 1))
parts['bot_cap'].location = loc + Vector((0, 0, -offset_v * 2))
parts['bot_rays'].location = loc + Vector((0, 0, -offset_v * 2)) # Bottom Capと同じ座標系
mod = parts['top_cap'].modifiers.new("Bool", 'BOOLEAN')
mod.operation = 'INTERSECT'; mod.object = cutters['box_top']; mod.solver = 'EXACT'
mod = parts['bot_cap'].modifiers.new("Bool", 'BOOLEAN')
mod.operation = 'INTERSECT'; mod.object = cutters['box_bot']; mod.solver = 'EXACT'
mod = parts['mid'].modifiers.new("Bool1", 'BOOLEAN')
mod.operation = 'DIFFERENCE'; mod.object = cutters['box_top']; mod.solver = 'EXACT'
mod = parts['mid'].modifiers.new("Bool2", 'BOOLEAN')
mod.operation = 'DIFFERENCE'; mod.object = cutters['box_bot']; mod.solver = 'EXACT'
return parts
def remove_flat_faces(mesh, normal_z_targets, threshold=0.01):
bm = bmesh.new()
bm.from_mesh(mesh)
faces_to_remove = []
for f in bm.faces:
for nz in normal_z_targets:
if abs(f.normal.z - nz) < threshold:
faces_to_remove.append(f)
break
if faces_to_remove:
bmesh.ops.delete(bm, geom=faces_to_remove, context='FACES')
bm.to_mesh(mesh)
bm.free()
def apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=False):
context.view_layer.update()
dg = context.evaluated_depsgraph_get()
for key in ['top_cap', 'mid', 'bot_cap']:
obj = parts.get(key)
if not obj: continue
try:
eval_obj = obj.evaluated_get(dg)
new_mesh = bpy.data.meshes.new_from_object(eval_obj)
if is_preview: new_mesh.name = f"Prev_Applied_{key}"
old_mesh = obj.data
obj.modifiers.clear()
obj.data = new_mesh
if old_mesh.users == 0: bpy.data.meshes.remove(old_mesh)
if key == 'top_cap' and not getattr(props, 'cap_top_cap', True):
remove_flat_faces(new_mesh, [-1.0])
elif key == 'bot_cap' and not getattr(props, 'cap_bot_cap', True):
remove_flat_faces(new_mesh, [1.0])
elif key == 'mid' and not getattr(props, 'cap_mid', True):
remove_flat_faces(new_mesh, [1.0, -1.0])
except Exception as e:
print(f"Mod Apply Error [{key}]:", e)
for obj in cutters.values():
m = obj.data
try:
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0: bpy.data.meshes.remove(m)
except: pass
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_TAG = f"{PREFIX}_preview_tag"
def clear_preview_data(prefix):
col = bpy.data.collections.get(f"{prefix}_Preview_Zone")
if col:
for o in list(col.objects):
m = o.data
bpy.data.objects.remove(o, do_unlink=True)
if m and getattr(m, "users", 0) == 0: bpy.data.meshes.remove(m)
for m in list(bpy.data.meshes):
if (m.name.startswith(f"Prev_{prefix}_") or m.name.startswith("Prev_Applied_")) and m.users == 0:
bpy.data.meshes.remove(m)
def update_preview_geometry(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
clear_preview_data(PREFIX)
if not props.show_preview:
context.view_layer.update(); return
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_NAME)
context.scene.collection.children.link(col)
meshes = build_base_meshes(props, f"Prev_{PREFIX}")
loc = Vector(props.sphere_loc)
offset_v = props.split_offset
cutters = setup_cutters(meshes, loc, offset_v, col, is_preview=True)
parts = setup_parts(meshes, cutters, loc, offset_v, col, is_preview=True)
apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=True)
parts_config = [
('top_cap', props.show_top_cap, props.color_front_top_cap, props.color_back_top_cap, "TCap"),
('top_cone', props.show_top_cone, props.color_front_top_cone, props.color_back_top_cone, "TCone"),
('mid', props.show_mid, props.color_front_mid, props.color_back_mid, "Mid"),
('bot_cone', props.show_bot_cone, props.color_front_bot_cone, props.color_back_bot_cone, "BCone"),
('bot_cap', props.show_bot_cap, props.color_front_bot_cap, props.color_back_bot_cap, "BCap"),
('bot_rays', props.show_bot_rays, props.color_front_bot_rays, props.color_back_bot_rays, "BRays"),
]
for key, is_show, c_front, c_back, mat_name in parts_config:
obj = parts.get(key)
if not obj: continue
try:
if is_show:
mat = get_preview_material(c_front, c_back, f"Mat_Prev_{mat_name}")
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
else:
m = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0: bpy.data.meshes.remove(m)
except Exception as e: print(f"Visibility Error[{key}]:", e)
context.view_layer.update()
_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)
sphere_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['sphere_loc'], update=on_update)
sphere_radius: FloatProperty(name="Sphere Radius", default=CURRENT_DEFAULTS['sphere_radius'], min=0.01, update=on_update)
cone_angle: FloatProperty(name="Cone Angle", default=CURRENT_DEFAULTS['cone_angle'], min=1.0, max=179.0, update=on_update)
split_offset: FloatProperty(name="Split Z-Offset", default=CURRENT_DEFAULTS['split_offset'], min=0.0, update=on_update)
show_top_cap: BoolProperty(name="Top Cap", default=CURRENT_DEFAULTS['show_top_cap'], update=on_update)
cap_top_cap: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_top_cap'], update=on_update)
color_front_top_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_top_cap'], update=on_update)
color_back_top_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_top_cap'], update=on_update)
show_top_cone: BoolProperty(name="Top Cone", default=CURRENT_DEFAULTS['show_top_cone'], update=on_update)
cap_top_cone: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_top_cone'], update=on_update)
color_front_top_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_top_cone'], update=on_update)
color_back_top_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_top_cone'], update=on_update)
show_mid: BoolProperty(name="Middle Sphere", default=CURRENT_DEFAULTS['show_mid'], update=on_update)
cap_mid: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_mid'], update=on_update)
color_front_mid: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_mid'], update=on_update)
color_back_mid: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_mid'], update=on_update)
show_bot_cone: BoolProperty(name="Bottom Cone", default=CURRENT_DEFAULTS['show_bot_cone'], update=on_update)
cap_bot_cone: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_bot_cone'], update=on_update)
color_front_bot_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_bot_cone'], update=on_update)
color_back_bot_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_bot_cone'], update=on_update)
show_bot_cap: BoolProperty(name="Bottom Cap", default=CURRENT_DEFAULTS['show_bot_cap'], update=on_update)
cap_bot_cap: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_bot_cap'], update=on_update)
color_front_bot_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_bot_cap'], update=on_update)
color_back_bot_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_bot_cap'], update=on_update)
# --- New Rays Properties ---
show_bot_rays: BoolProperty(name="Bottom Rays", default=CURRENT_DEFAULTS['show_bot_rays'], update=on_update)
ray_count: IntProperty(name="Ray Count", default=CURRENT_DEFAULTS['ray_count'], min=1, max=72, update=on_update)
ray_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['ray_thickness'], min=0.01, update=on_update)
ray_offset: FloatProperty(name="Apex Offset", default=CURRENT_DEFAULTS['ray_offset'], min=0.0, update=on_update)
color_front_bot_rays: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_bot_rays'], update=on_update)
color_back_bot_rays: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_bot_rays'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CreateSphere(Operator):
bl_idname = f"{OP_PREFIX}.create_sphere"
bl_label = "Create Selected Parts"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
timestamp = datetime.now().strftime('%H%M%S')
meshes = build_base_meshes(props, f"Temp_{timestamp}")
loc = Vector(props.sphere_loc)
offset_v = props.split_offset
col = context.collection if context.collection else context.scene.collection
cutters = setup_cutters(meshes, loc, offset_v, col, is_preview=False)
parts = setup_parts(meshes, cutters, loc, offset_v, col, is_preview=False)
apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=False)
parts_config = [
('top_cap', props.show_top_cap, props.color_front_top_cap, props.color_back_top_cap, "TopCap"),
('top_cone', props.show_top_cone, props.color_front_top_cone, props.color_back_top_cone, "TopCone"),
('mid', props.show_mid, props.color_front_mid, props.color_back_mid, "Middle"),
('bot_cone', props.show_bot_cone, props.color_front_bot_cone, props.color_back_bot_cone, "BotCone"),
('bot_cap', props.show_bot_cap, props.color_front_bot_cap, props.color_back_bot_cap, "BotCap"),
('bot_rays', props.show_bot_rays, props.color_front_bot_rays, props.color_back_bot_rays, "BotRays"),
]
active_obj = None
bpy.ops.object.select_all(action='DESELECT')
for key, is_show, c_front, c_back, mat_name in parts_config:
obj = parts.get(key)
if not obj: continue
if is_show:
obj.data.materials.append(create_unique_material(c_front, c_back, f"Mat_{mat_name}"))
obj.name = f"Sphere_{mat_name}_{timestamp}"
obj.select_set(True)
active_obj = obj
else:
m = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0: bpy.data.meshes.remove(m)
for k, m in list(meshes.items()):
if m and m.users == 0: bpy.data.meshes.remove(m)
if active_obj: context.view_layer.objects.active = active_obj
self.report({'INFO'}, "Created Interferometer Parts")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string():
target_text = t; break
if not target_text:
self.report({'ERROR'}, "Script source not found.")
return {'CANCELLED'}
code = target_text.as_string()
l = props.sphere_loc
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "sphere_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
new_dict += f' "sphere_radius": {props.sphere_radius:.4f},\n'
new_dict += f' "cone_angle": {props.cone_angle:.4f},\n'
new_dict += f' "split_offset": {props.split_offset:.4f},\n'
for key in ["top_cap", "top_cone", "mid", "bot_cone", "bot_cap", "bot_rays"]:
s = getattr(props, f"show_{key}")
cf = getattr(props, f"color_front_{key}")
cb = getattr(props, f"color_back_{key}")
new_dict += f' "show_{key}": {s},\n'
if hasattr(props, f"cap_{key}"):
cap_s = getattr(props, f"cap_{key}")
new_dict += f' "cap_{key}": {cap_s},\n'
if key == "bot_rays":
new_dict += f' "ray_count": {props.ray_count},\n'
new_dict += f' "ray_thickness": {props.ray_thickness:.4f},\n'
new_dict += f' "ray_offset": {props.ray_offset:.4f},\n'
new_dict += f' "color_front_{key}": ({cf[0]:.4f}, {cf[1]:.4f}, {cf[2]:.4f}, {cf[3]:.4f}),\n'
new_dict += f' "color_back_{key}": ({cb[0]:.4f}, {cb[1]:.4f}, {cb[2]:.4f}, {cb[3]:.4f}),\n'
new_dict += "}\n"
try:
start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
pre, post = code.split(start)[0], code.split(end)[1]
final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
context.window_manager.clipboard = final
self.report({'INFO'}, "Code copied!")
except: return {'CANCELLED'}
return {'FINISHED'}
class OT_Reset(Operator):
bl_idname = f"{OP_PREFIX}.reset"
bl_label = "Reset"
def execute(self, context):
p = getattr(context.scene, PROPS_NAME)
p.sphere_loc = (0,0,0)
p.sphere_radius = 30.0; p.cone_angle = 90.0; p.split_offset = 5.0
p.show_top_cap = True; p.cap_top_cap = True; p.color_front_top_cap = (0.8, 0.2, 0.2, 0.8); p.color_back_top_cap = (0.4, 0.1, 0.1, 0.8)
p.show_top_cone = True; p.cap_top_cone = True; p.color_front_top_cone = (0.8, 0.5, 0.2, 0.8); p.color_back_top_cone = (0.4, 0.25, 0.1, 0.8)
p.show_mid = True; p.cap_mid = True; p.color_front_mid = (0.2, 0.8, 0.2, 0.8); p.color_back_mid = (0.1, 0.4, 0.1, 0.8)
p.show_bot_cone = True; p.cap_bot_cone = True; p.color_front_bot_cone = (0.2, 0.5, 0.8, 0.8); p.color_back_bot_cone = (0.1, 0.25, 0.4, 0.8)
p.show_bot_cap = True; p.cap_bot_cap = True; p.color_front_bot_cap = (0.5, 0.2, 0.8, 0.8); p.color_back_bot_cap = (0.25, 0.1, 0.4, 0.8)
p.show_bot_rays = True; p.ray_count = 36; p.ray_thickness = 0.5; p.ray_offset = 10.0
p.color_front_bot_rays = (0.9, 0.9, 0.2, 0.8); p.color_back_bot_rays = (0.5, 0.5, 0.1, 0.8)
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 = "Interferometer Generator"
bl_idname = f"{PREFIX}_PT_main"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: layout.label(text="Reload Script"); return
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
box = layout.box()
if not props.show_preview: box.label(text="Preview is Hidden", icon='INFO')
box.prop(props, "sphere_loc")
box.prop(props, "sphere_radius")
box.prop(props, "cone_angle")
box.prop(props, "split_offset")
layout.separator()
parts_box = layout.box()
parts_box.label(text="Parts Selection & Colors", icon='MATERIAL')
parts_ui = [
("top_cap", "Top Cap", "cap_top_cap"),
("top_cone", "Top Cone", "cap_top_cone"),
("mid", "Middle Sphere", "cap_mid"),
("bot_cone", "Bottom Cone", "cap_bot_cone"),
("bot_cap", "Bottom Cap", "cap_bot_cap"),
]
for key, label, cap_prop in parts_ui:
is_show = getattr(props, f"show_{key}")
icon = 'RESTRICT_VIEW_OFF' if is_show else 'RESTRICT_VIEW_ON'
p_box = parts_box.box()
row = p_box.row(align=True)
row.prop(props, f"show_{key}", icon=icon, text=label)
if cap_prop: row.prop(props, cap_prop, text="Base", toggle=True)
if is_show:
col = p_box.column(align=True)
col.prop(props, f"color_front_{key}", text="Front Color")
col.prop(props, f"color_back_{key}", text="Back Color")
layout.separator()
layout.operator(OT_Reset.bl_idname, icon='LOOP_BACK', text="Reset Values")
layout.separator()
col = layout.column()
col.scale_y = 1.5
col.operator(OT_CreateSphere.bl_idname, icon='MESH_UVSPHERE', text="Create Selected Parts")
class PT_RaysPanel(Panel):
bl_label = "Bottom Rays Settings"
bl_idname = f"{PREFIX}_PT_rays"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
box = layout.box()
icon = 'RESTRICT_VIEW_OFF' if props.show_bot_rays else 'RESTRICT_VIEW_ON'
box.prop(props, "show_bot_rays", icon=icon, text="Enable Bottom Rays")
if props.show_bot_rays:
col = box.column(align=True)
col.prop(props, "ray_count")
col.prop(props, "ray_thickness")
col.prop(props, "ray_offset")
c_box = box.box()
c_box.prop(props, "color_front_bot_rays", text="Front Color")
c_box.prop(props, "color_back_bot_rays", text="Back Color")
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_CreateSphere, OT_CopyFullScript, OT_Reset, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, PT_RaysPanel, PT_LinksPanel, PT_RemovePanel)
# 初期化用の関数を追加
def init_preview():
if bpy.context and hasattr(bpy.context, 'scene'):
props = getattr(bpy.context.scene, PROPS_NAME, None)
if props and props.show_preview:
update_preview_geometry(bpy.context)
return None
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_SphereProps))
# スクリプト実行後、自動で1回描画を走らせるタイマー (修正ポイント)
bpy.app.timers.register(init_preview, first_interval=0.2)
def unregister():
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes): bpy.utils.unregister_class(c)
if __name__ == "__main__": register()
# Copied: 03:54:43
# Copied: 03:52:01
# Copied: 15:45:01
import bpy
import bmesh
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime
# ==============================================================================
# 設定エリア & ID管理
# ==============================================================================
PREFIX = "Interferometer20260312_v9"
TAB_NAME = "[ Interferometer ] "
# ### ZIONAD_SOURCE_ID: INTERFEROMETER_2026_03_12_V9 ###
bl_info = {
"name": f"zionad 520 [ Interferometer Gen ] {PREFIX}",
"author": "zionadchat",
"version": (5, 1, 0),
"blender": (5, 0, 0),
"location": "3D View > Sidebar",
"description": "5-Part Interferometer Sphere Generator with Evenly Spaced Rays",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: INTERFEROMETER_2026_03_12_V9 ###"
ADDON_LINKS = (
{"label": "あっさり干渉計 20260312版", "url": "<https://www.notion.so/20260312-320f5dacaf438031a63dd9fc00edc049>"},
{"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,
"sphere_loc": (0.0000, 0.0000, 0.0000),
"sphere_radius": 30.0000,
"cone_angle": 178.8900,
"split_offset": 0.0000,
"show_top_cap": False,
"cap_top_cap": True,
"color_front_top_cap": (0.8000, 0.2000, 0.2000, 0.8000),
"color_back_top_cap": (0.4000, 0.1000, 0.1000, 0.8000),
"show_top_cone": False,
"cap_top_cone": True,
"color_front_top_cone": (0.8000, 0.5000, 0.2000, 0.8000),
"color_back_top_cone": (0.4000, 0.2500, 0.1000, 0.8000),
"show_mid": False,
"cap_mid": True,
"color_front_mid": (0.2000, 0.8000, 0.2000, 0.8000),
"color_back_mid": (0.1000, 0.4000, 0.1000, 0.8000),
"show_bot_cone": False,
"cap_bot_cone": True,
"color_front_bot_cone": (0.2000, 0.5000, 0.8000, 0.8000),
"color_back_bot_cone": (0.1000, 0.2500, 0.4000, 0.8000),
"show_bot_cap": True,
"cap_bot_cap": False,
"color_front_bot_cap": (0.8000, 0.0158, 0.7195, 1.0000),
"color_back_bot_cap": (0.0379, 0.0173, 0.0576, 1.0000),
"show_bot_rays": True,
"ray_count": 22,
"ray_thickness": 2.6000,
"ray_offset": 10.0000,
"color_front_bot_rays": (0.9000, 0.9000, 0.2000, 0.8000),
"color_back_bot_rays": (0.5000, 0.5000, 0.1000, 0.8000),
}
# <END_DICT>
# ==============================================================================
# マテリアル作成ロジック (Blender 5.0+ 完全対応版)
# ==============================================================================
def apply_material_color(mat, front_color, back_color):
mat.use_nodes = True
f_col = list(front_color)
b_col = list(back_color)
tree = mat.node_tree
tree.nodes.clear()
try:
bsdf_front = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf_front.location = (-200, 150)
bsdf_front.inputs["Base Color"].default_value = f_col
bsdf_front.inputs["Alpha"].default_value = f_col[3]
bsdf_back = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf_back.location = (-200, -150)
bsdf_back.inputs["Base Color"].default_value = b_col
bsdf_back.inputs["Alpha"].default_value = b_col[3]
mix_shader = tree.nodes.new("ShaderNodeMixShader")
mix_shader.location = (100, 0)
geom = tree.nodes.new("ShaderNodeNewGeometry")
geom.location = (-400, 300)
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (300, 0)
tree.links.new(geom.outputs["Backfacing"], mix_shader.inputs[0])
tree.links.new(bsdf_front.outputs["BSDF"], mix_shader.inputs[1])
tree.links.new(bsdf_back.outputs["BSDF"], mix_shader.inputs[2])
tree.links.new(mix_shader.outputs["Shader"], out.inputs["Surface"])
except Exception as e:
print("Material Error 5.0+:", e)
mat.diffuse_color = f_col
def create_unique_material(front_color, back_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)
apply_material_color(mat, front_color, back_color)
return mat
def get_preview_material(front_color, back_color, name="Mat_Prev"):
mat = bpy.data.materials.get(name)
if not mat: mat = bpy.data.materials.new(name=name)
apply_material_color(mat, front_color, back_color)
return mat
# ==============================================================================
# ジオメトリ構築ロジック
# ==============================================================================
def build_base_meshes(props, prefix):
meshes = {}
R = props.sphere_radius
theta = math.radians(props.cone_angle / 2)
H = R * math.cos(theta)
r_base = R * math.sin(theta)
def make_mesh(name, bm):
mesh = bpy.data.meshes.get(name)
if not mesh: mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
return mesh
# Base Spheres & Cones
for key in ['base_top', 'base_mid', 'base_bot']:
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=R)
meshes[key] = make_mesh(f"{prefix}_{key}", bm)
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=props.cap_top_cone, cap_tris=True, segments=64, radius1=0.0, radius2=r_base, depth=H)
bmesh.ops.translate(bm, vec=(0, 0, H/2), verts=bm.verts)
meshes['cone_top'] = make_mesh(f"{prefix}_ConeTop", bm)
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=props.cap_bot_cone, cap_tris=True, segments=64, radius1=r_base, radius2=0.0, depth=H)
bmesh.ops.translate(bm, vec=(0, 0, -H/2), verts=bm.verts)
meshes['cone_bot'] = make_mesh(f"{prefix}_ConeBot", bm)
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=(R*4, R*4, R*4), verts=bm.verts)
bmesh.ops.translate(bm, vec=(0, 0, H + 2*R), verts=bm.verts)
meshes['box_top'] = make_mesh(f"{prefix}_BoxTop", bm)
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=(R*4, R*4, R*4), verts=bm.verts)
bmesh.ops.translate(bm, vec=(0, 0, -H - 2*R), verts=bm.verts)
meshes['box_bot'] = make_mesh(f"{prefix}_BoxBot", bm)
# --- Bottom Rays Generation (Fibonacci Lattice) ---
bm_rays = bmesh.new()
count = props.ray_count
offset = props.ray_offset
thickness = props.ray_thickness
z_min = -R
z_max = -H
golden_ratio = (1 + 5 ** 0.5) / 2
for i in range(count):
# 等面積になるようにZ軸を分割
z = z_min + (z_max - z_min) * ((i + 0.5) / count) if count > 0 else z_min
r = math.sqrt(max(0, R**2 - z**2))
phi = 2 * math.pi * i / golden_ratio
x = r * math.cos(phi)
y = r * math.sin(phi)
u = Vector((x, y, z)).normalized()
P = u * R # 底面中心 (球体表面)
A = u * offset # 頂点 (中心からのオフセット)
depth = R - offset
if depth <= 0.1: continue
geom = bmesh.ops.create_cone(bm_rays, cap_ends=True, cap_tris=True, segments=12, radius1=thickness, radius2=0, depth=depth)
verts = [v for v in geom['verts'] if isinstance(v, bmesh.types.BMVert)]
rot = Vector((0,0,1)).rotation_difference(-u)
bmesh.ops.rotate(bm_rays, cent=(0,0,0), matrix=rot.to_matrix(), verts=verts)
mid = (P + A) / 2
bmesh.ops.translate(bm_rays, vec=mid, verts=verts)
meshes['bot_rays'] = make_mesh(f"{prefix}_BotRays", bm_rays)
return meshes
def setup_cutters(meshes, loc, offset_v, collection, is_preview=False):
cutters = {}
names = [('box_top', "Cut_BoxTop"), ('box_bot', "Cut_BoxBot")]
for key, name in names:
obj = bpy.data.objects.new(name, meshes[key])
obj.display_type = 'BOUNDS'
obj.hide_viewport = True; obj.hide_render = True
if is_preview: obj[PREVIEW_TAG] = True
collection.objects.link(obj)
cutters[key] = obj
cutters['box_top'].location = loc + Vector((0, 0, offset_v * 2))
cutters['box_bot'].location = loc + Vector((0, 0, -offset_v * 2))
return cutters
def setup_parts(meshes, cutters, loc, offset_v, collection, is_preview=False):
parts = {}
parts['top_cap'] = bpy.data.objects.new("Part_TopCap", meshes['base_top'])
parts['top_cone'] = bpy.data.objects.new("Part_TopCone", meshes['cone_top'])
parts['mid'] = bpy.data.objects.new("Part_Middle", meshes['base_mid'])
parts['bot_cone'] = bpy.data.objects.new("Part_BotCone", meshes['cone_bot'])
parts['bot_cap'] = bpy.data.objects.new("Part_BotCap", meshes['base_bot'])
parts['bot_rays'] = bpy.data.objects.new("Part_BotRays", meshes['bot_rays'])
for obj in parts.values():
if is_preview: obj[PREVIEW_TAG] = True
collection.objects.link(obj)
parts['top_cap'].location = loc + Vector((0, 0, offset_v * 2))
parts['top_cone'].location = loc + Vector((0, 0, offset_v * 1))
parts['mid'].location = loc
parts['bot_cone'].location = loc + Vector((0, 0, -offset_v * 1))
parts['bot_cap'].location = loc + Vector((0, 0, -offset_v * 2))
parts['bot_rays'].location = loc + Vector((0, 0, -offset_v * 2)) # Bottom Capと同じ座標系
mod = parts['top_cap'].modifiers.new("Bool", 'BOOLEAN')
mod.operation = 'INTERSECT'; mod.object = cutters['box_top']; mod.solver = 'EXACT'
mod = parts['bot_cap'].modifiers.new("Bool", 'BOOLEAN')
mod.operation = 'INTERSECT'; mod.object = cutters['box_bot']; mod.solver = 'EXACT'
mod = parts['mid'].modifiers.new("Bool1", 'BOOLEAN')
mod.operation = 'DIFFERENCE'; mod.object = cutters['box_top']; mod.solver = 'EXACT'
mod = parts['mid'].modifiers.new("Bool2", 'BOOLEAN')
mod.operation = 'DIFFERENCE'; mod.object = cutters['box_bot']; mod.solver = 'EXACT'
return parts
def remove_flat_faces(mesh, normal_z_targets, threshold=0.01):
bm = bmesh.new()
bm.from_mesh(mesh)
faces_to_remove = []
for f in bm.faces:
for nz in normal_z_targets:
if abs(f.normal.z - nz) < threshold:
faces_to_remove.append(f)
break
if faces_to_remove:
bmesh.ops.delete(bm, geom=faces_to_remove, context='FACES')
bm.to_mesh(mesh)
bm.free()
def apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=False):
context.view_layer.update()
dg = context.evaluated_depsgraph_get()
for key in ['top_cap', 'mid', 'bot_cap']:
obj = parts.get(key)
if not obj: continue
try:
eval_obj = obj.evaluated_get(dg)
new_mesh = bpy.data.meshes.new_from_object(eval_obj)
if is_preview: new_mesh.name = f"Prev_Applied_{key}"
old_mesh = obj.data
obj.modifiers.clear()
obj.data = new_mesh
if old_mesh.users == 0: bpy.data.meshes.remove(old_mesh)
if key == 'top_cap' and not getattr(props, 'cap_top_cap', True):
remove_flat_faces(new_mesh, [-1.0])
elif key == 'bot_cap' and not getattr(props, 'cap_bot_cap', True):
remove_flat_faces(new_mesh, [1.0])
elif key == 'mid' and not getattr(props, 'cap_mid', True):
remove_flat_faces(new_mesh, [1.0, -1.0])
except Exception as e:
print(f"Mod Apply Error [{key}]:", e)
for obj in cutters.values():
m = obj.data
try:
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0: bpy.data.meshes.remove(m)
except: pass
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_TAG = f"{PREFIX}_preview_tag"
def clear_preview_data(prefix):
col = bpy.data.collections.get(f"{prefix}_Preview_Zone")
if col:
for o in list(col.objects):
m = o.data
bpy.data.objects.remove(o, do_unlink=True)
if m and getattr(m, "users", 0) == 0: bpy.data.meshes.remove(m)
for m in list(bpy.data.meshes):
if (m.name.startswith(f"Prev_{prefix}_") or m.name.startswith("Prev_Applied_")) and m.users == 0:
bpy.data.meshes.remove(m)
def update_preview_geometry(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
clear_preview_data(PREFIX)
if not props.show_preview:
context.view_layer.update(); return
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_NAME)
context.scene.collection.children.link(col)
meshes = build_base_meshes(props, f"Prev_{PREFIX}")
loc = Vector(props.sphere_loc)
offset_v = props.split_offset
cutters = setup_cutters(meshes, loc, offset_v, col, is_preview=True)
parts = setup_parts(meshes, cutters, loc, offset_v, col, is_preview=True)
apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=True)
parts_config = [
('top_cap', props.show_top_cap, props.color_front_top_cap, props.color_back_top_cap, "TCap"),
('top_cone', props.show_top_cone, props.color_front_top_cone, props.color_back_top_cone, "TCone"),
('mid', props.show_mid, props.color_front_mid, props.color_back_mid, "Mid"),
('bot_cone', props.show_bot_cone, props.color_front_bot_cone, props.color_back_bot_cone, "BCone"),
('bot_cap', props.show_bot_cap, props.color_front_bot_cap, props.color_back_bot_cap, "BCap"),
('bot_rays', props.show_bot_rays, props.color_front_bot_rays, props.color_back_bot_rays, "BRays"),
]
for key, is_show, c_front, c_back, mat_name in parts_config:
obj = parts.get(key)
if not obj: continue
try:
if is_show:
mat = get_preview_material(c_front, c_back, f"Mat_Prev_{mat_name}")
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
else:
m = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0: bpy.data.meshes.remove(m)
except Exception as e: print(f"Visibility Error[{key}]:", e)
context.view_layer.update()
_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)
sphere_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['sphere_loc'], update=on_update)
sphere_radius: FloatProperty(name="Sphere Radius", default=CURRENT_DEFAULTS['sphere_radius'], min=0.01, update=on_update)
cone_angle: FloatProperty(name="Cone Angle", default=CURRENT_DEFAULTS['cone_angle'], min=1.0, max=179.0, update=on_update)
split_offset: FloatProperty(name="Split Z-Offset", default=CURRENT_DEFAULTS['split_offset'], min=0.0, update=on_update)
show_top_cap: BoolProperty(name="Top Cap", default=CURRENT_DEFAULTS['show_top_cap'], update=on_update)
cap_top_cap: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_top_cap'], update=on_update)
color_front_top_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_top_cap'], update=on_update)
color_back_top_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_top_cap'], update=on_update)
show_top_cone: BoolProperty(name="Top Cone", default=CURRENT_DEFAULTS['show_top_cone'], update=on_update)
cap_top_cone: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_top_cone'], update=on_update)
color_front_top_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_top_cone'], update=on_update)
color_back_top_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_top_cone'], update=on_update)
show_mid: BoolProperty(name="Middle Sphere", default=CURRENT_DEFAULTS['show_mid'], update=on_update)
cap_mid: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_mid'], update=on_update)
color_front_mid: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_mid'], update=on_update)
color_back_mid: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_mid'], update=on_update)
show_bot_cone: BoolProperty(name="Bottom Cone", default=CURRENT_DEFAULTS['show_bot_cone'], update=on_update)
cap_bot_cone: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_bot_cone'], update=on_update)
color_front_bot_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_bot_cone'], update=on_update)
color_back_bot_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_bot_cone'], update=on_update)
show_bot_cap: BoolProperty(name="Bottom Cap", default=CURRENT_DEFAULTS['show_bot_cap'], update=on_update)
cap_bot_cap: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_bot_cap'], update=on_update)
color_front_bot_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_bot_cap'], update=on_update)
color_back_bot_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_bot_cap'], update=on_update)
# --- New Rays Properties ---
show_bot_rays: BoolProperty(name="Bottom Rays", default=CURRENT_DEFAULTS['show_bot_rays'], update=on_update)
ray_count: IntProperty(name="Ray Count", default=CURRENT_DEFAULTS['ray_count'], min=1, max=72, update=on_update)
ray_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['ray_thickness'], min=0.01, update=on_update)
ray_offset: FloatProperty(name="Apex Offset", default=CURRENT_DEFAULTS['ray_offset'], min=0.0, update=on_update)
color_front_bot_rays: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_bot_rays'], update=on_update)
color_back_bot_rays: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_bot_rays'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CreateSphere(Operator):
bl_idname = f"{OP_PREFIX}.create_sphere"
bl_label = "Create Selected Parts"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
timestamp = datetime.now().strftime('%H%M%S')
meshes = build_base_meshes(props, f"Temp_{timestamp}")
loc = Vector(props.sphere_loc)
offset_v = props.split_offset
col = context.collection if context.collection else context.scene.collection
cutters = setup_cutters(meshes, loc, offset_v, col, is_preview=False)
parts = setup_parts(meshes, cutters, loc, offset_v, col, is_preview=False)
apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=False)
parts_config = [
('top_cap', props.show_top_cap, props.color_front_top_cap, props.color_back_top_cap, "TopCap"),
('top_cone', props.show_top_cone, props.color_front_top_cone, props.color_back_top_cone, "TopCone"),
('mid', props.show_mid, props.color_front_mid, props.color_back_mid, "Middle"),
('bot_cone', props.show_bot_cone, props.color_front_bot_cone, props.color_back_bot_cone, "BotCone"),
('bot_cap', props.show_bot_cap, props.color_front_bot_cap, props.color_back_bot_cap, "BotCap"),
('bot_rays', props.show_bot_rays, props.color_front_bot_rays, props.color_back_bot_rays, "BotRays"),
]
active_obj = None
bpy.ops.object.select_all(action='DESELECT')
for key, is_show, c_front, c_back, mat_name in parts_config:
obj = parts.get(key)
if not obj: continue
if is_show:
obj.data.materials.append(create_unique_material(c_front, c_back, f"Mat_{mat_name}"))
obj.name = f"Sphere_{mat_name}_{timestamp}"
obj.select_set(True)
active_obj = obj
else:
m = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0: bpy.data.meshes.remove(m)
for k, m in list(meshes.items()):
if m and m.users == 0: bpy.data.meshes.remove(m)
if active_obj: context.view_layer.objects.active = active_obj
self.report({'INFO'}, "Created Interferometer Parts")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string():
target_text = t; break
if not target_text:
self.report({'ERROR'}, "Script source not found.")
return {'CANCELLED'}
code = target_text.as_string()
l = props.sphere_loc
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "sphere_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
new_dict += f' "sphere_radius": {props.sphere_radius:.4f},\n'
new_dict += f' "cone_angle": {props.cone_angle:.4f},\n'
new_dict += f' "split_offset": {props.split_offset:.4f},\n'
for key in ["top_cap", "top_cone", "mid", "bot_cone", "bot_cap", "bot_rays"]:
s = getattr(props, f"show_{key}")
cf = getattr(props, f"color_front_{key}")
cb = getattr(props, f"color_back_{key}")
new_dict += f' "show_{key}": {s},\n'
if hasattr(props, f"cap_{key}"):
cap_s = getattr(props, f"cap_{key}")
new_dict += f' "cap_{key}": {cap_s},\n'
if key == "bot_rays":
new_dict += f' "ray_count": {props.ray_count},\n'
new_dict += f' "ray_thickness": {props.ray_thickness:.4f},\n'
new_dict += f' "ray_offset": {props.ray_offset:.4f},\n'
new_dict += f' "color_front_{key}": ({cf[0]:.4f}, {cf[1]:.4f}, {cf[2]:.4f}, {cf[3]:.4f}),\n'
new_dict += f' "color_back_{key}": ({cb[0]:.4f}, {cb[1]:.4f}, {cb[2]:.4f}, {cb[3]:.4f}),\n'
new_dict += "}\n"
try:
start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
pre, post = code.split(start)[0], code.split(end)[1]
final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
context.window_manager.clipboard = final
self.report({'INFO'}, "Code copied!")
except: return {'CANCELLED'}
return {'FINISHED'}
class OT_Reset(Operator):
bl_idname = f"{OP_PREFIX}.reset"
bl_label = "Reset"
def execute(self, context):
p = getattr(context.scene, PROPS_NAME)
p.sphere_loc = (0,0,0)
p.sphere_radius = 30.0; p.cone_angle = 90.0; p.split_offset = 5.0
p.show_top_cap = True; p.cap_top_cap = True; p.color_front_top_cap = (0.8, 0.2, 0.2, 0.8); p.color_back_top_cap = (0.4, 0.1, 0.1, 0.8)
p.show_top_cone = True; p.cap_top_cone = True; p.color_front_top_cone = (0.8, 0.5, 0.2, 0.8); p.color_back_top_cone = (0.4, 0.25, 0.1, 0.8)
p.show_mid = True; p.cap_mid = True; p.color_front_mid = (0.2, 0.8, 0.2, 0.8); p.color_back_mid = (0.1, 0.4, 0.1, 0.8)
p.show_bot_cone = True; p.cap_bot_cone = True; p.color_front_bot_cone = (0.2, 0.5, 0.8, 0.8); p.color_back_bot_cone = (0.1, 0.25, 0.4, 0.8)
p.show_bot_cap = True; p.cap_bot_cap = True; p.color_front_bot_cap = (0.5, 0.2, 0.8, 0.8); p.color_back_bot_cap = (0.25, 0.1, 0.4, 0.8)
p.show_bot_rays = True; p.ray_count = 36; p.ray_thickness = 0.5; p.ray_offset = 10.0
p.color_front_bot_rays = (0.9, 0.9, 0.2, 0.8); p.color_back_bot_rays = (0.5, 0.5, 0.1, 0.8)
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 = "Interferometer Generator"
bl_idname = f"{PREFIX}_PT_main"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: layout.label(text="Reload Script"); return
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
box = layout.box()
if not props.show_preview: box.label(text="Preview is Hidden", icon='INFO')
box.prop(props, "sphere_loc")
box.prop(props, "sphere_radius")
box.prop(props, "cone_angle")
box.prop(props, "split_offset")
layout.separator()
parts_box = layout.box()
parts_box.label(text="Parts Selection & Colors", icon='MATERIAL')
parts_ui = [
("top_cap", "Top Cap", "cap_top_cap"),
("top_cone", "Top Cone", "cap_top_cone"),
("mid", "Middle Sphere", "cap_mid"),
("bot_cone", "Bottom Cone", "cap_bot_cone"),
("bot_cap", "Bottom Cap", "cap_bot_cap"),
]
for key, label, cap_prop in parts_ui:
is_show = getattr(props, f"show_{key}")
icon = 'RESTRICT_VIEW_OFF' if is_show else 'RESTRICT_VIEW_ON'
p_box = parts_box.box()
row = p_box.row(align=True)
row.prop(props, f"show_{key}", icon=icon, text=label)
if cap_prop: row.prop(props, cap_prop, text="Base", toggle=True)
if is_show:
col = p_box.column(align=True)
col.prop(props, f"color_front_{key}", text="Front Color")
col.prop(props, f"color_back_{key}", text="Back Color")
layout.separator()
layout.operator(OT_Reset.bl_idname, icon='LOOP_BACK', text="Reset Values")
layout.separator()
col = layout.column()
col.scale_y = 1.5
col.operator(OT_CreateSphere.bl_idname, icon='MESH_UVSPHERE', text="Create Selected Parts")
class PT_RaysPanel(Panel):
bl_label = "Bottom Rays Settings"
bl_idname = f"{PREFIX}_PT_rays"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
box = layout.box()
icon = 'RESTRICT_VIEW_OFF' if props.show_bot_rays else 'RESTRICT_VIEW_ON'
box.prop(props, "show_bot_rays", icon=icon, text="Enable Bottom Rays")
if props.show_bot_rays:
col = box.column(align=True)
col.prop(props, "ray_count")
col.prop(props, "ray_thickness")
col.prop(props, "ray_offset")
c_box = box.box()
c_box.prop(props, "color_front_bot_rays", text="Front Color")
c_box.prop(props, "color_back_bot_rays", text="Back Color")
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_CreateSphere, OT_CopyFullScript, OT_Reset, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, PT_RaysPanel, PT_LinksPanel, PT_RemovePanel)
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_SphereProps))
def unregister():
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes): bpy.utils.unregister_class(c)
if __name__ == "__main__": register()
# Copied: 15:00:01
import bpy
import bmesh
import math
import webbrowser
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 = "Interferometer20260312_v8"
TAB_NAME = "[ Interferometer ] "
# ### ZIONAD_SOURCE_ID: INTERFEROMETER_2026_03_12_V8 ###
bl_info = {
"name": f"zionad 520 [ Interferometer Gen ] {PREFIX}",
"author": "zionadchat",
"version": (5, 0, 0),
"blender": (5, 0, 0), # 5.0+ 専用に指定
"location": "3D View > Sidebar",
"description": "5-Part Interferometer Sphere Generator (Safe Materials & 2-Line Color UI)",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: INTERFEROMETER_2026_03_12_V8 ###"
ADDON_LINKS = (
{"label": "あっさり干渉計 20260312版", "url": "<https://www.notion.so/20260312-320f5dacaf438031a63dd9fc00edc049>"},
{"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,
"sphere_loc": (0.0000, 0.0000, 0.0000),
"sphere_radius": 30.0000,
"cone_angle": 90.0000,
"split_offset": 5.0000,
"show_top_cap": True,
"cap_top_cap": True,
"color_front_top_cap": (0.8000, 0.2000, 0.2000, 0.8000),
"color_back_top_cap": (0.4000, 0.1000, 0.1000, 0.8000),
"show_top_cone": True,
"cap_top_cone": True,
"color_front_top_cone": (0.8000, 0.5000, 0.2000, 0.8000),
"color_back_top_cone": (0.4000, 0.2500, 0.1000, 0.8000),
"show_mid": True,
"cap_mid": True,
"color_front_mid": (0.2000, 0.8000, 0.2000, 0.8000),
"color_back_mid": (0.1000, 0.4000, 0.1000, 0.8000),
"show_bot_cone": True,
"cap_bot_cone": True,
"color_front_bot_cone": (0.2000, 0.5000, 0.8000, 0.8000),
"color_back_bot_cone": (0.1000, 0.2500, 0.4000, 0.8000),
"show_bot_cap": True,
"cap_bot_cap": True,
"color_front_bot_cap": (0.5000, 0.2000, 0.8000, 0.8000),
"color_back_bot_cap": (0.2500, 0.1000, 0.4000, 0.8000),
}
# <END_DICT>
# ==============================================================================
# マテリアル作成ロジック (Blender 5.0+ 完全対応版)
# ==============================================================================
def apply_material_color(mat, front_color, back_color):
mat.use_nodes = True
# 5.0+ (EEVEE Next等) では blend_method は非推奨・廃止のため削除
f_col = list(front_color)
b_col = list(back_color)
tree = mat.node_tree
tree.nodes.clear()
try:
# 前面用 BSDF
bsdf_front = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf_front.location = (-200, 150)
# 5.0仕様のソケット名を直接指定
bsdf_front.inputs["Base Color"].default_value = f_col
bsdf_front.inputs["Alpha"].default_value = f_col[3]
# 背面用 BSDF
bsdf_back = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf_back.location = (-200, -150)
bsdf_back.inputs["Base Color"].default_value = b_col
bsdf_back.inputs["Alpha"].default_value = b_col[3]
# Mix Shader
mix_shader = tree.nodes.new("ShaderNodeMixShader")
mix_shader.location = (100, 0)
# ジオメトリノード (Backfacing取得用)
geom = tree.nodes.new("ShaderNodeNewGeometry")
geom.location = (-400, 300)
# 出力ノード
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (300, 0)
# ノードリンクの構築 (名前で確実につなぐ)
tree.links.new(geom.outputs["Backfacing"], mix_shader.inputs[0]) # Factor
tree.links.new(bsdf_front.outputs["BSDF"], mix_shader.inputs[1]) # Shader 1
tree.links.new(bsdf_back.outputs["BSDF"], mix_shader.inputs[2]) # Shader 2
tree.links.new(mix_shader.outputs["Shader"], out.inputs["Surface"])
except Exception as e:
print("Material Error 5.0+:", e)
# ソリッドビューでの表示カラーを確保
mat.diffuse_color = f_col
def create_unique_material(front_color, back_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)
apply_material_color(mat, front_color, back_color)
return mat
def get_preview_material(front_color, back_color, name="Mat_Prev"):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name=name)
apply_material_color(mat, front_color, back_color)
return mat
# ==============================================================================
# ジオメトリ構築ロジック
# ==============================================================================
def build_base_meshes(props, prefix):
meshes = {}
R = props.sphere_radius
theta = math.radians(props.cone_angle / 2)
H = R * math.cos(theta)
r_base = R * math.sin(theta)
def make_mesh(name, bm):
mesh = bpy.data.meshes.get(name)
if not mesh: mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
return mesh
for key in ['base_top', 'base_mid', 'base_bot']:
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=R)
meshes[key] = make_mesh(f"{prefix}_{key}", bm)
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=props.cap_top_cone, cap_tris=True, segments=64, radius1=0.0, radius2=r_base, depth=H)
bmesh.ops.translate(bm, vec=(0, 0, H/2), verts=bm.verts)
meshes['cone_top'] = make_mesh(f"{prefix}_ConeTop", bm)
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=props.cap_bot_cone, cap_tris=True, segments=64, radius1=r_base, radius2=0.0, depth=H)
bmesh.ops.translate(bm, vec=(0, 0, -H/2), verts=bm.verts)
meshes['cone_bot'] = make_mesh(f"{prefix}_ConeBot", bm)
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=(R*4, R*4, R*4), verts=bm.verts)
bmesh.ops.translate(bm, vec=(0, 0, H + 2*R), verts=bm.verts)
meshes['box_top'] = make_mesh(f"{prefix}_BoxTop", bm)
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=(R*4, R*4, R*4), verts=bm.verts)
bmesh.ops.translate(bm, vec=(0, 0, -H - 2*R), verts=bm.verts)
meshes['box_bot'] = make_mesh(f"{prefix}_BoxBot", bm)
return meshes
def setup_cutters(meshes, loc, offset_v, collection, is_preview=False):
cutters = {}
names = [('box_top', "Cut_BoxTop"), ('box_bot', "Cut_BoxBot")]
for key, name in names:
obj = bpy.data.objects.new(name, meshes[key])
obj.display_type = 'BOUNDS'
obj.hide_viewport = True
obj.hide_render = True
if is_preview: obj[PREVIEW_TAG] = True
collection.objects.link(obj)
cutters[key] = obj
cutters['box_top'].location = loc + Vector((0, 0, offset_v * 2))
cutters['box_bot'].location = loc + Vector((0, 0, -offset_v * 2))
return cutters
def setup_parts(meshes, cutters, loc, offset_v, collection, is_preview=False):
parts = {}
parts['top_cap'] = bpy.data.objects.new("Part_TopCap", meshes['base_top'])
parts['top_cone'] = bpy.data.objects.new("Part_TopCone", meshes['cone_top'])
parts['mid'] = bpy.data.objects.new("Part_Middle", meshes['base_mid'])
parts['bot_cone'] = bpy.data.objects.new("Part_BotCone", meshes['cone_bot'])
parts['bot_cap'] = bpy.data.objects.new("Part_BotCap", meshes['base_bot'])
for obj in parts.values():
if is_preview: obj[PREVIEW_TAG] = True
collection.objects.link(obj)
parts['top_cap'].location = loc + Vector((0, 0, offset_v * 2))
parts['top_cone'].location = loc + Vector((0, 0, offset_v * 1))
parts['mid'].location = loc
parts['bot_cone'].location = loc + Vector((0, 0, -offset_v * 1))
parts['bot_cap'].location = loc + Vector((0, 0, -offset_v * 2))
mod = parts['top_cap'].modifiers.new("Bool", 'BOOLEAN')
mod.operation = 'INTERSECT'; mod.object = cutters['box_top']; mod.solver = 'EXACT'
mod = parts['bot_cap'].modifiers.new("Bool", 'BOOLEAN')
mod.operation = 'INTERSECT'; mod.object = cutters['box_bot']; mod.solver = 'EXACT'
mod = parts['mid'].modifiers.new("Bool1", 'BOOLEAN')
mod.operation = 'DIFFERENCE'; mod.object = cutters['box_top']; mod.solver = 'EXACT'
mod = parts['mid'].modifiers.new("Bool2", 'BOOLEAN')
mod.operation = 'DIFFERENCE'; mod.object = cutters['box_bot']; mod.solver = 'EXACT'
return parts
def remove_flat_faces(mesh, normal_z_targets, threshold=0.01):
bm = bmesh.new()
bm.from_mesh(mesh)
faces_to_remove = []
for f in bm.faces:
for nz in normal_z_targets:
if abs(f.normal.z - nz) < threshold:
faces_to_remove.append(f)
break
if faces_to_remove:
bmesh.ops.delete(bm, geom=faces_to_remove, context='FACES')
bm.to_mesh(mesh)
bm.free()
def apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=False):
context.view_layer.update()
dg = context.evaluated_depsgraph_get()
for key in ['top_cap', 'mid', 'bot_cap']:
obj = parts.get(key)
if not obj: continue
try:
eval_obj = obj.evaluated_get(dg)
new_mesh = bpy.data.meshes.new_from_object(eval_obj)
if is_preview: new_mesh.name = f"Prev_Applied_{key}"
old_mesh = obj.data
obj.modifiers.clear()
obj.data = new_mesh
if old_mesh.users == 0:
bpy.data.meshes.remove(old_mesh)
if key == 'top_cap' and not getattr(props, 'cap_top_cap', True):
remove_flat_faces(new_mesh, [-1.0])
elif key == 'bot_cap' and not getattr(props, 'cap_bot_cap', True):
remove_flat_faces(new_mesh, [1.0])
elif key == 'mid' and not getattr(props, 'cap_mid', True):
remove_flat_faces(new_mesh, [1.0, -1.0])
except Exception as e:
print(f"Mod Apply Error [{key}]:", e)
for obj in cutters.values():
m = obj.data
try:
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0:
bpy.data.meshes.remove(m)
except: pass
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_TAG = f"{PREFIX}_preview_tag"
def clear_preview_data(prefix):
col = bpy.data.collections.get(f"{prefix}_Preview_Zone")
if col:
for o in list(col.objects):
m = o.data
bpy.data.objects.remove(o, do_unlink=True)
if m and getattr(m, "users", 0) == 0:
bpy.data.meshes.remove(m)
for m in list(bpy.data.meshes):
if (m.name.startswith(f"Prev_{prefix}_") or m.name.startswith("Prev_Applied_")) and m.users == 0:
bpy.data.meshes.remove(m)
def update_preview_geometry(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
clear_preview_data(PREFIX)
if not props.show_preview:
context.view_layer.update()
return
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_NAME)
context.scene.collection.children.link(col)
meshes = build_base_meshes(props, f"Prev_{PREFIX}")
loc = Vector(props.sphere_loc)
offset_v = props.split_offset
cutters = setup_cutters(meshes, loc, offset_v, col, is_preview=True)
parts = setup_parts(meshes, cutters, loc, offset_v, col, is_preview=True)
apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=True)
parts_config = [
('top_cap', props.show_top_cap, props.color_front_top_cap, props.color_back_top_cap, "TCap"),
('top_cone', props.show_top_cone, props.color_front_top_cone, props.color_back_top_cone, "TCone"),
('mid', props.show_mid, props.color_front_mid, props.color_back_mid, "Mid"),
('bot_cone', props.show_bot_cone, props.color_front_bot_cone, props.color_back_bot_cone, "BCone"),
('bot_cap', props.show_bot_cap, props.color_front_bot_cap, props.color_back_bot_cap, "BCap"),
]
for key, is_show, c_front, c_back, mat_name in parts_config:
obj = parts.get(key)
if not obj: continue
try:
if is_show:
mat = get_preview_material(c_front, c_back, f"Mat_Prev_{mat_name}")
if not obj.data.materials:
obj.data.materials.append(mat)
else:
obj.data.materials[0] = mat
else:
m = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0:
bpy.data.meshes.remove(m)
except Exception as e:
print(f"Visibility Error[{key}]:", e)
context.view_layer.update()
_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)
sphere_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['sphere_loc'], update=on_update)
sphere_radius: FloatProperty(name="Sphere Radius", default=CURRENT_DEFAULTS['sphere_radius'], min=0.01, update=on_update)
cone_angle: FloatProperty(name="Cone Angle", default=CURRENT_DEFAULTS['cone_angle'], min=1.0, max=179.0, update=on_update)
split_offset: FloatProperty(name="Split Z-Offset", default=CURRENT_DEFAULTS['split_offset'], min=0.0, update=on_update)
show_top_cap: BoolProperty(name="Top Cap", default=CURRENT_DEFAULTS['show_top_cap'], update=on_update)
cap_top_cap: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_top_cap'], update=on_update)
color_front_top_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_top_cap'], update=on_update)
color_back_top_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_top_cap'], update=on_update)
show_top_cone: BoolProperty(name="Top Cone", default=CURRENT_DEFAULTS['show_top_cone'], update=on_update)
cap_top_cone: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_top_cone'], update=on_update)
color_front_top_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_top_cone'], update=on_update)
color_back_top_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_top_cone'], update=on_update)
show_mid: BoolProperty(name="Middle Sphere", default=CURRENT_DEFAULTS['show_mid'], update=on_update)
cap_mid: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_mid'], update=on_update)
color_front_mid: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_mid'], update=on_update)
color_back_mid: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_mid'], update=on_update)
show_bot_cone: BoolProperty(name="Bottom Cone", default=CURRENT_DEFAULTS['show_bot_cone'], update=on_update)
cap_bot_cone: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_bot_cone'], update=on_update)
color_front_bot_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_bot_cone'], update=on_update)
color_back_bot_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_bot_cone'], update=on_update)
show_bot_cap: BoolProperty(name="Bottom Cap", default=CURRENT_DEFAULTS['show_bot_cap'], update=on_update)
cap_bot_cap: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_bot_cap'], update=on_update)
color_front_bot_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_front_bot_cap'], update=on_update)
color_back_bot_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_back_bot_cap'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CreateSphere(Operator):
bl_idname = f"{OP_PREFIX}.create_sphere"
bl_label = "Create Selected Parts"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
timestamp = datetime.now().strftime('%H%M%S')
meshes = build_base_meshes(props, f"Temp_{timestamp}")
loc = Vector(props.sphere_loc)
offset_v = props.split_offset
col = context.collection if context.collection else context.scene.collection
cutters = setup_cutters(meshes, loc, offset_v, col, is_preview=False)
parts = setup_parts(meshes, cutters, loc, offset_v, col, is_preview=False)
apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=False)
parts_config = [
('top_cap', props.show_top_cap, props.color_front_top_cap, props.color_back_top_cap, "TopCap"),
('top_cone', props.show_top_cone, props.color_front_top_cone, props.color_back_top_cone, "TopCone"),
('mid', props.show_mid, props.color_front_mid, props.color_back_mid, "Middle"),
('bot_cone', props.show_bot_cone, props.color_front_bot_cone, props.color_back_bot_cone, "BotCone"),
('bot_cap', props.show_bot_cap, props.color_front_bot_cap, props.color_back_bot_cap, "BotCap"),
]
active_obj = None
bpy.ops.object.select_all(action='DESELECT')
for key, is_show, c_front, c_back, mat_name in parts_config:
obj = parts.get(key)
if not obj: continue
if is_show:
obj.data.materials.append(create_unique_material(c_front, c_back, f"Mat_{mat_name}"))
obj.name = f"Sphere_{mat_name}_{timestamp}"
obj.select_set(True)
active_obj = obj
else:
m = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0:
bpy.data.meshes.remove(m)
for k, m in list(meshes.items()):
if m and m.users == 0:
bpy.data.meshes.remove(m)
if active_obj:
context.view_layer.objects.active = active_obj
self.report({'INFO'}, "Created Interferometer Parts")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string():
target_text = t; break
if not target_text:
self.report({'ERROR'}, "Script source not found.")
return {'CANCELLED'}
code = target_text.as_string()
l = props.sphere_loc
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "sphere_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
new_dict += f' "sphere_radius": {props.sphere_radius:.4f},\n'
new_dict += f' "cone_angle": {props.cone_angle:.4f},\n'
new_dict += f' "split_offset": {props.split_offset:.4f},\n'
for key in ["top_cap", "top_cone", "mid", "bot_cone", "bot_cap"]:
s = getattr(props, f"show_{key}")
cf = getattr(props, f"color_front_{key}")
cb = getattr(props, f"color_back_{key}")
new_dict += f' "show_{key}": {s},\n'
if hasattr(props, f"cap_{key}"):
cap_s = getattr(props, f"cap_{key}")
new_dict += f' "cap_{key}": {cap_s},\n'
new_dict += f' "color_front_{key}": ({cf[0]:.4f}, {cf[1]:.4f}, {cf[2]:.4f}, {cf[3]:.4f}),\n'
new_dict += f' "color_back_{key}": ({cb[0]:.4f}, {cb[1]:.4f}, {cb[2]:.4f}, {cb[3]:.4f}),\n'
new_dict += "}\n"
try:
start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
pre, post = code.split(start)[0], code.split(end)[1]
final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
context.window_manager.clipboard = final
self.report({'INFO'}, "Code copied!")
except: return {'CANCELLED'}
return {'FINISHED'}
class OT_Reset(Operator):
bl_idname = f"{OP_PREFIX}.reset"
bl_label = "Reset"
def execute(self, context):
p = getattr(context.scene, PROPS_NAME)
p.sphere_loc = (0,0,0)
p.sphere_radius = 30.0
p.cone_angle = 90.0
p.split_offset = 5.0
p.show_top_cap = True; p.cap_top_cap = True; p.color_front_top_cap = (0.8, 0.2, 0.2, 0.8); p.color_back_top_cap = (0.4, 0.1, 0.1, 0.8)
p.show_top_cone = True; p.cap_top_cone = True; p.color_front_top_cone = (0.8, 0.5, 0.2, 0.8); p.color_back_top_cone = (0.4, 0.25, 0.1, 0.8)
p.show_mid = True; p.cap_mid = True; p.color_front_mid = (0.2, 0.8, 0.2, 0.8); p.color_back_mid = (0.1, 0.4, 0.1, 0.8)
p.show_bot_cone = True; p.cap_bot_cone = True; p.color_front_bot_cone = (0.2, 0.5, 0.8, 0.8); p.color_back_bot_cone = (0.1, 0.25, 0.4, 0.8)
p.show_bot_cap = True; p.cap_bot_cap = True; p.color_front_bot_cap = (0.5, 0.2, 0.8, 0.8); p.color_back_bot_cap = (0.25, 0.1, 0.4, 0.8)
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 = "Interferometer Generator"
bl_idname = f"{PREFIX}_PT_main"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: layout.label(text="Reload Script"); return
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
box = layout.box()
if not props.show_preview:
box.label(text="Preview is Hidden", icon='INFO')
box.prop(props, "sphere_loc")
box.prop(props, "sphere_radius")
box.prop(props, "cone_angle")
box.prop(props, "split_offset")
layout.separator()
# --- パーツごとの表示・色・底面設定 ---
parts_box = layout.box()
parts_box.label(text="Parts Selection & Colors", icon='MATERIAL')
parts_ui = [
("top_cap", "Top Cap", "cap_top_cap"),
("top_cone", "Top Cone", "cap_top_cone"),
("mid", "Middle Sphere", "cap_mid"),
("bot_cone", "Bottom Cone", "cap_bot_cone"),
("bot_cap", "Bottom Cap", "cap_bot_cap"),
]
for key, label, cap_prop in parts_ui:
show_prop = f"show_{key}"
front_prop = f"color_front_{key}"
back_prop = f"color_back_{key}"
is_show = getattr(props, show_prop)
icon = 'RESTRICT_VIEW_OFF' if is_show else 'RESTRICT_VIEW_ON'
p_box = parts_box.box()
row = p_box.row(align=True)
row.prop(props, show_prop, icon=icon, text=label)
if cap_prop:
row.prop(props, cap_prop, text="Base", toggle=True)
# ★ 色調整を2行(Column)で表示
if is_show:
col = p_box.column(align=True)
col.prop(props, front_prop, text="Front Color")
col.prop(props, back_prop, text="Back Color")
layout.separator()
layout.operator(OT_Reset.bl_idname, icon='LOOP_BACK', text="Reset Values")
layout.separator()
col = layout.column()
col.scale_y = 1.5
col.operator(OT_CreateSphere.bl_idname, icon='MESH_UVSPHERE', text="Create Selected Parts")
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_CreateSphere, OT_CopyFullScript, OT_Reset, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, PT_LinksPanel, PT_RemovePanel)
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_SphereProps))
def unregister():
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes): bpy.utils.unregister_class(c)
if __name__ == "__main__": register()
# Copied: 15:00:01
import bpy
import bmesh
import math
import webbrowser
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 = "Interferometer20260312_v4"
TAB_NAME = "[ Interferometer ] "
# ### ZIONAD_SOURCE_ID: INTERFEROMETER_2026_03_12_V4 ###
bl_info = {
"name": f"zionad 520 [ Interferometer Gen ] {PREFIX}",
"author": "zionadchat",
"version": (3, 6, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": "5-Part Interferometer Sphere Generator (Fixed Middle Sphere)",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: INTERFEROMETER_2026_03_12_V4 ###"
ADDON_LINKS = (
{"label": "あっさり干渉計 20260312版", "url": "<https://www.notion.so/20260312-320f5dacaf438031a63dd9fc00edc049>"},
{"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,
"sphere_loc": (0.0000, 0.0000, 0.0000),
"sphere_radius": 30.0000,
"cone_angle": 90.0000,
"split_offset": 5.0000,
"show_top_cap": True,
"cap_top_cap": True,
"color_top_cap": (0.8000, 0.2000, 0.2000, 0.8000),
"show_top_cone": True,
"cap_top_cone": True,
"color_top_cone": (0.8000, 0.5000, 0.2000, 0.8000),
"show_mid": True,
"cap_mid": True,
"color_mid": (0.2000, 0.8000, 0.2000, 0.8000),
"show_bot_cone": True,
"cap_bot_cone": True,
"color_bot_cone": (0.2000, 0.5000, 0.8000, 0.8000),
"show_bot_cap": True,
"cap_bot_cap": True,
"color_bot_cap": (0.5000, 0.2000, 0.8000, 0.8000),
}
# <END_DICT>
# ==============================================================================
# マテリアル作成ロジック
# ==============================================================================
def apply_material_color(mat, color):
mat.diffuse_color = color
if mat.use_nodes:
tree = mat.node_tree
bsdf = tree.nodes.get("Principled BSDF")
if not bsdf:
for node in tree.nodes:
if node.type == 'BSDF_PRINCIPLED':
bsdf = node
break
if bsdf:
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 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'
apply_material_color(mat, color)
return mat
def get_preview_material(color, name="Mat_Prev"):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
apply_material_color(mat, color)
return mat
# ==============================================================================
# ジオメトリ構築ロジック
# ==============================================================================
def build_base_meshes(props, prefix):
meshes = {}
R = props.sphere_radius
theta = math.radians(props.cone_angle / 2)
H = R * math.cos(theta)
r_base = R * math.sin(theta)
def make_mesh(name, bm):
mesh = bpy.data.meshes.get(name)
if not mesh: mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
return mesh
# 1. Base Sphere (色競合を防ぐため3つ独立して作成)
for key in['base_top', 'base_mid', 'base_bot']:
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=R)
meshes[key] = make_mesh(f"{prefix}_{key}", bm)
# 2. Top Cone (キャップ制御対応)
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=props.cap_top_cone, cap_tris=True, segments=64, radius1=0.0, radius2=r_base, depth=H)
bmesh.ops.translate(bm, vec=(0, 0, H/2), verts=bm.verts)
meshes['cone_top'] = make_mesh(f"{prefix}_ConeTop", bm)
# 3. Bottom Cone (キャップ制御対応)
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=props.cap_bot_cone, cap_tris=True, segments=64, radius1=r_base, radius2=0.0, depth=H)
bmesh.ops.translate(bm, vec=(0, 0, -H/2), verts=bm.verts)
meshes['cone_bot'] = make_mesh(f"{prefix}_ConeBot", bm)
# 4. Cutter Box Top (Z > H を切り出す)
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=(R*4, R*4, R*4), verts=bm.verts)
bmesh.ops.translate(bm, vec=(0, 0, H + 2*R), verts=bm.verts)
meshes['box_top'] = make_mesh(f"{prefix}_BoxTop", bm)
# 5. Cutter Box Bottom (Z < -H を切り出す)
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=(R*4, R*4, R*4), verts=bm.verts)
bmesh.ops.translate(bm, vec=(0, 0, -H - 2*R), verts=bm.verts)
meshes['box_bot'] = make_mesh(f"{prefix}_BoxBot", bm)
return meshes
def setup_cutters(meshes, loc, offset_v, collection, is_preview=False):
cutters = {}
names =[('box_top', "Cut_BoxTop"), ('box_bot', "Cut_BoxBot")]
for key, name in names:
obj = bpy.data.objects.new(name, meshes[key])
obj.display_type = 'BOUNDS'
obj.hide_viewport = True
obj.hide_render = True
if is_preview: obj[PREVIEW_TAG] = True
collection.objects.link(obj)
cutters[key] = obj
cutters['box_top'].location = loc + Vector((0, 0, offset_v * 2))
cutters['box_bot'].location = loc + Vector((0, 0, -offset_v * 2))
return cutters
def setup_parts(meshes, cutters, loc, offset_v, collection, is_preview=False):
parts = {}
parts['top_cap'] = bpy.data.objects.new("Part_TopCap", meshes['base_top'])
parts['top_cone'] = bpy.data.objects.new("Part_TopCone", meshes['cone_top'])
parts['mid'] = bpy.data.objects.new("Part_Middle", meshes['base_mid'])
parts['bot_cone'] = bpy.data.objects.new("Part_BotCone", meshes['cone_bot'])
parts['bot_cap'] = bpy.data.objects.new("Part_BotCap", meshes['base_bot'])
for obj in parts.values():
if is_preview: obj[PREVIEW_TAG] = True
collection.objects.link(obj)
parts['top_cap'].location = loc + Vector((0, 0, offset_v * 2))
parts['top_cone'].location = loc + Vector((0, 0, offset_v * 1))
parts['mid'].location = loc
parts['bot_cone'].location = loc + Vector((0, 0, -offset_v * 1))
parts['bot_cap'].location = loc + Vector((0, 0, -offset_v * 2))
mod = parts['top_cap'].modifiers.new("Bool", 'BOOLEAN')
mod.operation = 'INTERSECT'; mod.object = cutters['box_top']; mod.solver = 'EXACT'
mod = parts['bot_cap'].modifiers.new("Bool", 'BOOLEAN')
mod.operation = 'INTERSECT'; mod.object = cutters['box_bot']; mod.solver = 'EXACT'
# 【修正】Middle Sphere はカッターで円錐をくり抜かず、単に上下のBoxで切り落として樽型にする
mod = parts['mid'].modifiers.new("Bool1", 'BOOLEAN')
mod.operation = 'DIFFERENCE'; mod.object = cutters['box_top']; mod.solver = 'EXACT'
mod = parts['mid'].modifiers.new("Bool2", 'BOOLEAN')
mod.operation = 'DIFFERENCE'; mod.object = cutters['box_bot']; mod.solver = 'EXACT'
return parts
# 底面の面(指定した法線方向を持つ面)を削除する関数
def remove_flat_faces(mesh, normal_z_targets, threshold=0.01):
bm = bmesh.new()
bm.from_mesh(mesh)
faces_to_remove =[]
for f in bm.faces:
for nz in normal_z_targets:
if abs(f.normal.z - nz) < threshold:
faces_to_remove.append(f)
break
if faces_to_remove:
bmesh.ops.delete(bm, geom=faces_to_remove, context='FACES')
bm.to_mesh(mesh)
bm.free()
def apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=False):
dg = context.evaluated_depsgraph_get()
# モディファイア適用 & 底面の削除
for key in['top_cap', 'mid', 'bot_cap']:
obj = parts[key]
eval_obj = obj.evaluated_get(dg)
new_mesh = bpy.data.meshes.new_from_object(eval_obj)
if is_preview: new_mesh.name = f"Prev_Applied_{key}"
old_mesh = obj.data
obj.modifiers.clear()
obj.data = new_mesh
if old_mesh.users == 0:
bpy.data.meshes.remove(old_mesh)
# UI設定による切断面(底面)の削除
if key == 'top_cap' and not props.cap_top_cap:
remove_flat_faces(new_mesh, normal_z_targets=[-1.0])
elif key == 'bot_cap' and not props.cap_bot_cap:
remove_flat_faces(new_mesh, normal_z_targets=[1.0])
elif key == 'mid' and not props.cap_mid:
remove_flat_faces(new_mesh, normal_z_targets=[1.0, -1.0])
# カッター用オブジェクトの削除
for obj in cutters.values():
m = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0:
bpy.data.meshes.remove(m)
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_TAG = f"{PREFIX}_preview_tag"
def clear_preview_data(prefix):
col = bpy.data.collections.get(f"{prefix}_Preview_Zone")
if col:
for o in list(col.objects):
m = o.data
bpy.data.objects.remove(o, do_unlink=True)
if m and getattr(m, "users", 0) == 0:
bpy.data.meshes.remove(m)
for m in list(bpy.data.meshes):
if m.name.startswith(f"Prev_{prefix}_") and m.users == 0:
bpy.data.meshes.remove(m)
def update_preview_geometry(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
clear_preview_data(PREFIX)
if not props.show_preview: return
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_NAME)
context.scene.collection.children.link(col)
meshes = build_base_meshes(props, f"Prev_{PREFIX}")
loc = Vector(props.sphere_loc)
offset_v = props.split_offset
cutters = setup_cutters(meshes, loc, offset_v, col, is_preview=True)
parts = setup_parts(meshes, cutters, loc, offset_v, col, is_preview=True)
apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=True)
parts_config =[
('top_cap', props.show_top_cap, props.color_top_cap, "TCap"),
('top_cone', props.show_top_cone, props.color_top_cone, "TCone"),
('mid', props.show_mid, props.color_mid, "Mid"),
('bot_cone', props.show_bot_cone, props.color_bot_cone, "BCone"),
('bot_cap', props.show_bot_cap, props.color_bot_cap, "BCap"),
]
for key, is_show, color, mat_name in parts_config:
obj = parts[key]
if is_show:
obj.data.materials.append(get_preview_material(color, f"Mat_Prev_{mat_name}"))
else:
m = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0:
bpy.data.meshes.remove(m)
_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)
sphere_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['sphere_loc'], update=on_update)
sphere_radius: FloatProperty(name="Sphere Radius", default=CURRENT_DEFAULTS['sphere_radius'], min=0.01, update=on_update)
cone_angle: FloatProperty(name="Cone Angle", default=CURRENT_DEFAULTS['cone_angle'], min=1.0, max=179.0, update=on_update)
split_offset: FloatProperty(name="Split Z-Offset", default=CURRENT_DEFAULTS['split_offset'], min=0.0, update=on_update)
show_top_cap: BoolProperty(name="Top Cap", default=CURRENT_DEFAULTS['show_top_cap'], update=on_update)
cap_top_cap: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_top_cap'], update=on_update)
color_top_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_top_cap'], update=on_update)
show_top_cone: BoolProperty(name="Top Cone", default=CURRENT_DEFAULTS['show_top_cone'], update=on_update)
cap_top_cone: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_top_cone'], update=on_update)
color_top_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_top_cone'], update=on_update)
show_mid: BoolProperty(name="Middle Sphere", default=CURRENT_DEFAULTS['show_mid'], update=on_update)
cap_mid: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_mid'], update=on_update)
color_mid: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_mid'], update=on_update)
show_bot_cone: BoolProperty(name="Bottom Cone", default=CURRENT_DEFAULTS['show_bot_cone'], update=on_update)
cap_bot_cone: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_bot_cone'], update=on_update)
color_bot_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_bot_cone'], update=on_update)
show_bot_cap: BoolProperty(name="Bottom Cap", default=CURRENT_DEFAULTS['show_bot_cap'], update=on_update)
cap_bot_cap: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_bot_cap'], update=on_update)
color_bot_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_bot_cap'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CreateSphere(Operator):
bl_idname = f"{OP_PREFIX}.create_sphere"
bl_label = "Create Selected Parts"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
timestamp = datetime.now().strftime('%H%M%S')
meshes = build_base_meshes(props, f"Temp_{timestamp}")
loc = Vector(props.sphere_loc)
offset_v = props.split_offset
col = context.collection if context.collection else context.scene.collection
cutters = setup_cutters(meshes, loc, offset_v, col, is_preview=False)
parts = setup_parts(meshes, cutters, loc, offset_v, col, is_preview=False)
apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=False)
parts_config =[
('top_cap', props.show_top_cap, props.color_top_cap, "TopCap"),
('top_cone', props.show_top_cone, props.color_top_cone, "TopCone"),
('mid', props.show_mid, props.color_mid, "Middle"),
('bot_cone', props.show_bot_cone, props.color_bot_cone, "BotCone"),
('bot_cap', props.show_bot_cap, props.color_bot_cap, "BotCap"),
]
active_obj = None
bpy.ops.object.select_all(action='DESELECT')
for key, is_show, color, mat_name in parts_config:
obj = parts[key]
if is_show:
obj.data.materials.append(create_unique_material(color, f"Mat_{mat_name}"))
obj.name = f"Sphere_{mat_name}_{timestamp}"
obj.select_set(True)
active_obj = obj
else:
m = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0:
bpy.data.meshes.remove(m)
# テンポラリメッシュの削除
for k, m in list(meshes.items()):
if m and m.users == 0:
bpy.data.meshes.remove(m)
if active_obj:
context.view_layer.objects.active = active_obj
self.report({'INFO'}, "Created Interferometer Parts")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string():
target_text = t; break
if not target_text:
self.report({'ERROR'}, "Script source not found.")
return {'CANCELLED'}
code = target_text.as_string()
l = props.sphere_loc
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "sphere_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
new_dict += f' "sphere_radius": {props.sphere_radius:.4f},\n'
new_dict += f' "cone_angle": {props.cone_angle:.4f},\n'
new_dict += f' "split_offset": {props.split_offset:.4f},\n'
for key in["top_cap", "top_cone", "mid", "bot_cone", "bot_cap"]:
s = getattr(props, f"show_{key}")
c = getattr(props, f"color_{key}")
new_dict += f' "show_{key}": {s},\n'
if hasattr(props, f"cap_{key}"):
cap_s = getattr(props, f"cap_{key}")
new_dict += f' "cap_{key}": {cap_s},\n'
new_dict += f' "color_{key}": ({c[0]:.4f}, {c[1]:.4f}, {c[2]:.4f}, {c[3]:.4f}),\n'
new_dict += "}\n"
try:
start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
pre, post = code.split(start)[0], code.split(end)[1]
final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
context.window_manager.clipboard = final
self.report({'INFO'}, "Code copied!")
except: return {'CANCELLED'}
return {'FINISHED'}
class OT_Reset(Operator):
bl_idname = f"{OP_PREFIX}.reset"
bl_label = "Reset"
def execute(self, context):
p = getattr(context.scene, PROPS_NAME)
p.sphere_loc = (0,0,0)
p.sphere_radius = 30.0
p.cone_angle = 90.0
p.split_offset = 5.0
p.show_top_cap = True; p.cap_top_cap = True; p.color_top_cap = (0.8, 0.2, 0.2, 0.8)
p.show_top_cone = True; p.cap_top_cone = True; p.color_top_cone = (0.8, 0.5, 0.2, 0.8)
p.show_mid = True; p.cap_mid = True; p.color_mid = (0.2, 0.8, 0.2, 0.8)
p.show_bot_cone = True; p.cap_bot_cone = True; p.color_bot_cone = (0.2, 0.5, 0.8, 0.8)
p.show_bot_cap = True; p.cap_bot_cap = True; p.color_bot_cap = (0.5, 0.2, 0.8, 0.8)
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 = "Interferometer Generator"
bl_idname = f"{PREFIX}_PT_main"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: layout.label(text="Reload Script"); return
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
box = layout.box()
if not props.show_preview:
box.label(text="Preview is Hidden", icon='INFO')
box.prop(props, "sphere_loc")
box.prop(props, "sphere_radius")
box.prop(props, "cone_angle")
box.prop(props, "split_offset")
layout.separator()
# --- パーツごとの表示・色・底面設定 ---
parts_box = layout.box()
parts_box.label(text="Parts Selection & Colors", icon='MATERIAL')
parts_ui =[
("top_cap", "Top Cap", "cap_top_cap"),
("top_cone", "Top Cone", "cap_top_cone"),
("mid", "Middle Sphere", "cap_mid"),
("bot_cone", "Bottom Cone", "cap_bot_cone"),
("bot_cap", "Bottom Cap", "cap_bot_cap"),
]
for key, label, cap_prop in parts_ui:
row = parts_box.row(align=True)
show_prop = f"show_{key}"
color_prop = f"color_{key}"
is_show = getattr(props, show_prop)
icon = 'RESTRICT_VIEW_OFF' if is_show else 'RESTRICT_VIEW_ON'
row.prop(props, show_prop, icon=icon, text="")
sub = row.row()
sub.active = is_show
sub.prop(props, color_prop, text=label)
if cap_prop:
sub2 = row.row(align=True)
sub2.active = is_show
sub2.prop(props, cap_prop, text="Base", toggle=True)
layout.separator()
layout.operator(OT_Reset.bl_idname, icon='LOOP_BACK', text="Reset Values")
layout.separator()
col = layout.column()
col.scale_y = 1.5
col.operator(OT_CreateSphere.bl_idname, icon='MESH_UVSPHERE', text="Create Selected Parts")
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_CreateSphere, OT_CopyFullScript, OT_Reset, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, PT_LinksPanel, PT_RemovePanel)
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_SphereProps))
def unregister():
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes): bpy.utils.unregister_class(c)
if __name__ == "__main__": register()
# Copied: 15:00:01
import bpy
import bmesh
import math
import webbrowser
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 = "Interferometer20260312_v3"
TAB_NAME = "[ Interferometer ] "
# ### ZIONAD_SOURCE_ID: INTERFEROMETER_2026_03_12_V3 ###
bl_info = {
"name": f"zionad 520 [ Interferometer Gen ] {PREFIX}",
"author": "zionadchat",
"version": (3, 5, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": "5-Part Interferometer Sphere Generator (Individual Colors & Bases)",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: INTERFEROMETER_2026_03_12_V3 ###"
ADDON_LINKS = (
{"label": "あっさり干渉計 20260312版", "url": "<https://www.notion.so/20260312-320f5dacaf438031a63dd9fc00edc049>"},
{"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,
"sphere_loc": (0.0000, 0.0000, 0.0000),
"sphere_radius": 30.0000,
"cone_angle": 90.0000,
"split_offset": 5.0000,
"show_top_cap": True,
"cap_top_cap": True,
"color_top_cap": (0.8000, 0.2000, 0.2000, 0.8000),
"show_top_cone": True,
"cap_top_cone": True,
"color_top_cone": (0.8000, 0.5000, 0.2000, 0.8000),
"show_mid": True,
"color_mid": (0.2000, 0.8000, 0.2000, 0.8000),
"show_bot_cone": True,
"cap_bot_cone": True,
"color_bot_cone": (0.2000, 0.5000, 0.8000, 0.8000),
"show_bot_cap": True,
"cap_bot_cap": True,
"color_bot_cap": (0.5000, 0.2000, 0.8000, 0.8000),
}
# <END_DICT>
# ==============================================================================
# マテリアル作成ロジック
# ==============================================================================
def apply_material_color(mat, color):
mat.diffuse_color = color
if mat.use_nodes:
tree = mat.node_tree
bsdf = tree.nodes.get("Principled BSDF")
if not bsdf:
for node in tree.nodes:
if node.type == 'BSDF_PRINCIPLED':
bsdf = node
break
if bsdf:
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 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'
apply_material_color(mat, color)
return mat
def get_preview_material(color, name="Mat_Prev"):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
apply_material_color(mat, color)
return mat
# ==============================================================================
# ジオメトリ構築ロジック
# ==============================================================================
def build_base_meshes(props, prefix):
meshes = {}
R = props.sphere_radius
theta = math.radians(props.cone_angle / 2)
H = R * math.cos(theta)
r_base = R * math.sin(theta)
depth = R * 2.5
r_cut = depth * math.tan(theta)
def make_mesh(name, bm):
mesh = bpy.data.meshes.get(name)
if not mesh: mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
return mesh
# 1. Base Sphere (色競合を防ぐため3つ独立して作成)
for key in['base_top', 'base_mid', 'base_bot']:
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=R)
meshes[key] = make_mesh(f"{prefix}_{key}", bm)
# 2. Top Cone (キャップ制御対応)
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=props.cap_top_cone, cap_tris=True, segments=64, radius1=0.0, radius2=r_base, depth=H)
bmesh.ops.translate(bm, vec=(0, 0, H/2), verts=bm.verts)
meshes['cone_top'] = make_mesh(f"{prefix}_ConeTop", bm)
# 3. Bottom Cone (キャップ制御対応)
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=props.cap_bot_cone, cap_tris=True, segments=64, radius1=r_base, radius2=0.0, depth=H)
bmesh.ops.translate(bm, vec=(0, 0, -H/2), verts=bm.verts)
meshes['cone_bot'] = make_mesh(f"{prefix}_ConeBot", bm)
# 4. Cutter Cone Top
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=64, radius1=0.0, radius2=r_cut, depth=depth)
bmesh.ops.translate(bm, vec=(0, 0, depth/2), verts=bm.verts)
meshes['cut_top'] = make_mesh(f"{prefix}_CutTop", bm)
# 5. Cutter Cone Bottom
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=64, radius1=r_cut, radius2=0.0, depth=depth)
bmesh.ops.translate(bm, vec=(0, 0, -depth/2), verts=bm.verts)
meshes['cut_bot'] = make_mesh(f"{prefix}_CutBot", bm)
# 6. Cutter Box Top
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=(R*4, R*4, R*4), verts=bm.verts)
bmesh.ops.translate(bm, vec=(0, 0, H + 2*R), verts=bm.verts)
meshes['box_top'] = make_mesh(f"{prefix}_BoxTop", bm)
# 7. Cutter Box Bottom
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=(R*4, R*4, R*4), verts=bm.verts)
bmesh.ops.translate(bm, vec=(0, 0, -H - 2*R), verts=bm.verts)
meshes['box_bot'] = make_mesh(f"{prefix}_BoxBot", bm)
return meshes
def setup_cutters(meshes, loc, offset_v, collection, is_preview=False):
cutters = {}
names =[('cut_top', "Cut_ConeTop"), ('cut_bot', "Cut_ConeBot"),
('box_top', "Cut_BoxTop"), ('box_bot', "Cut_BoxBot")]
for key, name in names:
obj = bpy.data.objects.new(name, meshes[key])
obj.display_type = 'BOUNDS'
obj.hide_viewport = True
obj.hide_render = True
if is_preview: obj[PREVIEW_TAG] = True
collection.objects.link(obj)
cutters[key] = obj
cutters['cut_top'].location = loc
cutters['cut_bot'].location = loc
cutters['box_top'].location = loc + Vector((0, 0, offset_v * 2))
cutters['box_bot'].location = loc + Vector((0, 0, -offset_v * 2))
return cutters
def setup_parts(meshes, cutters, loc, offset_v, collection, is_preview=False):
parts = {}
parts['top_cap'] = bpy.data.objects.new("Part_TopCap", meshes['base_top'])
parts['top_cone'] = bpy.data.objects.new("Part_TopCone", meshes['cone_top'])
parts['mid'] = bpy.data.objects.new("Part_Middle", meshes['base_mid'])
parts['bot_cone'] = bpy.data.objects.new("Part_BotCone", meshes['cone_bot'])
parts['bot_cap'] = bpy.data.objects.new("Part_BotCap", meshes['base_bot'])
for obj in parts.values():
if is_preview: obj[PREVIEW_TAG] = True
collection.objects.link(obj)
parts['top_cap'].location = loc + Vector((0, 0, offset_v * 2))
parts['top_cone'].location = loc + Vector((0, 0, offset_v * 1))
parts['mid'].location = loc
parts['bot_cone'].location = loc + Vector((0, 0, -offset_v * 1))
parts['bot_cap'].location = loc + Vector((0, 0, -offset_v * 2))
mod = parts['top_cap'].modifiers.new("Bool", 'BOOLEAN')
mod.operation = 'INTERSECT'; mod.object = cutters['box_top']; mod.solver = 'EXACT'
mod = parts['bot_cap'].modifiers.new("Bool", 'BOOLEAN')
mod.operation = 'INTERSECT'; mod.object = cutters['box_bot']; mod.solver = 'EXACT'
mod = parts['mid'].modifiers.new("Bool1", 'BOOLEAN')
mod.operation = 'DIFFERENCE'; mod.object = cutters['cut_top']; mod.solver = 'EXACT'
mod = parts['mid'].modifiers.new("Bool2", 'BOOLEAN')
mod.operation = 'DIFFERENCE'; mod.object = cutters['cut_bot']; mod.solver = 'EXACT'
return parts
# 底面の面(指定した法線方向を持つ面)を削除する関数
def remove_flat_faces(mesh, normal_z_target, threshold=0.01):
bm = bmesh.new()
bm.from_mesh(mesh)
faces_to_remove =[]
for f in bm.faces:
if abs(f.normal.z - normal_z_target) < threshold:
faces_to_remove.append(f)
if faces_to_remove:
bmesh.ops.delete(bm, geom=faces_to_remove, context='FACES')
bm.to_mesh(mesh)
bm.free()
def apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=False):
dg = context.evaluated_depsgraph_get()
# モディファイア適用 & 底面の削除(Spherical Cap用)
for key in['top_cap', 'mid', 'bot_cap']:
obj = parts[key]
eval_obj = obj.evaluated_get(dg)
new_mesh = bpy.data.meshes.new_from_object(eval_obj)
if is_preview: new_mesh.name = f"Prev_Applied_{key}"
old_mesh = obj.data
obj.modifiers.clear()
obj.data = new_mesh
if old_mesh.users == 0:
bpy.data.meshes.remove(old_mesh)
# Spherical Cap の切断面(底面)の削除
if key == 'top_cap' and not props.cap_top_cap:
remove_flat_faces(new_mesh, normal_z_target=-1.0)
elif key == 'bot_cap' and not props.cap_bot_cap:
remove_flat_faces(new_mesh, normal_z_target=1.0)
# カッター用オブジェクトの削除
for obj in cutters.values():
m = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0:
bpy.data.meshes.remove(m)
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_TAG = f"{PREFIX}_preview_tag"
def clear_preview_data(prefix):
col = bpy.data.collections.get(f"{prefix}_Preview_Zone")
if col:
for o in list(col.objects):
m = o.data
bpy.data.objects.remove(o, do_unlink=True)
if m and getattr(m, "users", 0) == 0:
bpy.data.meshes.remove(m)
for m in list(bpy.data.meshes):
if m.name.startswith(f"Prev_{prefix}_") and m.users == 0:
bpy.data.meshes.remove(m)
def update_preview_geometry(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
clear_preview_data(PREFIX)
if not props.show_preview: return
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_NAME)
context.scene.collection.children.link(col)
meshes = build_base_meshes(props, f"Prev_{PREFIX}")
loc = Vector(props.sphere_loc)
offset_v = props.split_offset
cutters = setup_cutters(meshes, loc, offset_v, col, is_preview=True)
parts = setup_parts(meshes, cutters, loc, offset_v, col, is_preview=True)
apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=True)
parts_config =[
('top_cap', props.show_top_cap, props.color_top_cap, "TCap"),
('top_cone', props.show_top_cone, props.color_top_cone, "TCone"),
('mid', props.show_mid, props.color_mid, "Mid"),
('bot_cone', props.show_bot_cone, props.color_bot_cone, "BCone"),
('bot_cap', props.show_bot_cap, props.color_bot_cap, "BCap"),
]
for key, is_show, color, mat_name in parts_config:
obj = parts[key]
if is_show:
obj.data.materials.append(get_preview_material(color, f"Mat_Prev_{mat_name}"))
else:
m = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0:
bpy.data.meshes.remove(m)
_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)
sphere_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['sphere_loc'], update=on_update)
sphere_radius: FloatProperty(name="Sphere Radius", default=CURRENT_DEFAULTS['sphere_radius'], min=0.01, update=on_update)
cone_angle: FloatProperty(name="Cone Angle", default=CURRENT_DEFAULTS['cone_angle'], min=1.0, max=179.0, update=on_update)
split_offset: FloatProperty(name="Split Z-Offset", default=CURRENT_DEFAULTS['split_offset'], min=0.0, update=on_update)
show_top_cap: BoolProperty(name="Top Cap", default=CURRENT_DEFAULTS['show_top_cap'], update=on_update)
cap_top_cap: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_top_cap'], update=on_update)
color_top_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_top_cap'], update=on_update)
show_top_cone: BoolProperty(name="Top Cone", default=CURRENT_DEFAULTS['show_top_cone'], update=on_update)
cap_top_cone: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_top_cone'], update=on_update)
color_top_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_top_cone'], update=on_update)
show_mid: BoolProperty(name="Middle Sphere", default=CURRENT_DEFAULTS['show_mid'], update=on_update)
color_mid: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_mid'], update=on_update)
show_bot_cone: BoolProperty(name="Bottom Cone", default=CURRENT_DEFAULTS['show_bot_cone'], update=on_update)
cap_bot_cone: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_bot_cone'], update=on_update)
color_bot_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_bot_cone'], update=on_update)
show_bot_cap: BoolProperty(name="Bottom Cap", default=CURRENT_DEFAULTS['show_bot_cap'], update=on_update)
cap_bot_cap: BoolProperty(name="Base", default=CURRENT_DEFAULTS['cap_bot_cap'], update=on_update)
color_bot_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_bot_cap'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CreateSphere(Operator):
bl_idname = f"{OP_PREFIX}.create_sphere"
bl_label = "Create Selected Parts"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
timestamp = datetime.now().strftime('%H%M%S')
meshes = build_base_meshes(props, f"Temp_{timestamp}")
loc = Vector(props.sphere_loc)
offset_v = props.split_offset
col = context.collection if context.collection else context.scene.collection
cutters = setup_cutters(meshes, loc, offset_v, col, is_preview=False)
parts = setup_parts(meshes, cutters, loc, offset_v, col, is_preview=False)
apply_modifiers_and_cleanup(parts, cutters, context, props, is_preview=False)
parts_config =[
('top_cap', props.show_top_cap, props.color_top_cap, "TopCap"),
('top_cone', props.show_top_cone, props.color_top_cone, "TopCone"),
('mid', props.show_mid, props.color_mid, "Middle"),
('bot_cone', props.show_bot_cone, props.color_bot_cone, "BotCone"),
('bot_cap', props.show_bot_cap, props.color_bot_cap, "BotCap"),
]
active_obj = None
bpy.ops.object.select_all(action='DESELECT')
for key, is_show, color, mat_name in parts_config:
obj = parts[key]
if is_show:
obj.data.materials.append(create_unique_material(color, f"Mat_{mat_name}"))
obj.name = f"Sphere_{mat_name}_{timestamp}"
obj.select_set(True)
active_obj = obj
else:
m = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if m and m.users == 0:
bpy.data.meshes.remove(m)
if active_obj:
context.view_layer.objects.active = active_obj
self.report({'INFO'}, "Created Interferometer Parts")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string():
target_text = t; break
if not target_text:
self.report({'ERROR'}, "Script source not found.")
return {'CANCELLED'}
code = target_text.as_string()
l = props.sphere_loc
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "sphere_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
new_dict += f' "sphere_radius": {props.sphere_radius:.4f},\n'
new_dict += f' "cone_angle": {props.cone_angle:.4f},\n'
new_dict += f' "split_offset": {props.split_offset:.4f},\n'
for key in["top_cap", "top_cone", "mid", "bot_cone", "bot_cap"]:
s = getattr(props, f"show_{key}")
c = getattr(props, f"color_{key}")
new_dict += f' "show_{key}": {s},\n'
# Cap (底面) の設定が存在すればコピー
if hasattr(props, f"cap_{key}"):
cap_s = getattr(props, f"cap_{key}")
new_dict += f' "cap_{key}": {cap_s},\n'
new_dict += f' "color_{key}": ({c[0]:.4f}, {c[1]:.4f}, {c[2]:.4f}, {c[3]:.4f}),\n'
new_dict += "}\n"
try:
start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
pre, post = code.split(start)[0], code.split(end)[1]
final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
context.window_manager.clipboard = final
self.report({'INFO'}, "Code copied!")
except: return {'CANCELLED'}
return {'FINISHED'}
class OT_Reset(Operator):
bl_idname = f"{OP_PREFIX}.reset"
bl_label = "Reset"
def execute(self, context):
p = getattr(context.scene, PROPS_NAME)
p.sphere_loc = (0,0,0)
p.sphere_radius = 30.0
p.cone_angle = 90.0
p.split_offset = 5.0
p.show_top_cap = True; p.cap_top_cap = True; p.color_top_cap = (0.8, 0.2, 0.2, 0.8)
p.show_top_cone = True; p.cap_top_cone = True; p.color_top_cone = (0.8, 0.5, 0.2, 0.8)
p.show_mid = True; p.color_mid = (0.2, 0.8, 0.2, 0.8)
p.show_bot_cone = True; p.cap_bot_cone = True; p.color_bot_cone = (0.2, 0.5, 0.8, 0.8)
p.show_bot_cap = True; p.cap_bot_cap = True; p.color_bot_cap = (0.5, 0.2, 0.8, 0.8)
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 = "Interferometer Generator"
bl_idname = f"{PREFIX}_PT_main"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: layout.label(text="Reload Script"); return
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
box = layout.box()
if not props.show_preview:
box.label(text="Preview is Hidden", icon='INFO')
box.prop(props, "sphere_loc")
box.prop(props, "sphere_radius")
box.prop(props, "cone_angle")
box.prop(props, "split_offset")
layout.separator()
# --- パーツごとの表示・色・底面設定 ---
parts_box = layout.box()
parts_box.label(text="Parts Selection & Colors", icon='MATERIAL')
parts_ui =[
("top_cap", "Top Cap", "cap_top_cap"),
("top_cone", "Top Cone", "cap_top_cone"),
("mid", "Middle Sphere", None),
("bot_cone", "Bottom Cone", "cap_bot_cone"),
("bot_cap", "Bottom Cap", "cap_bot_cap"),
]
for key, label, cap_prop in parts_ui:
row = parts_box.row(align=True)
show_prop = f"show_{key}"
color_prop = f"color_{key}"
is_show = getattr(props, show_prop)
icon = 'RESTRICT_VIEW_OFF' if is_show else 'RESTRICT_VIEW_ON'
row.prop(props, show_prop, icon=icon, text="")
sub = row.row()
sub.active = is_show
sub.prop(props, color_prop, text=label)
if cap_prop:
sub2 = row.row(align=True)
sub2.active = is_show
# トグルボタン化
sub2.prop(props, cap_prop, text="Base", toggle=True)
layout.separator()
layout.operator(OT_Reset.bl_idname, icon='LOOP_BACK', text="Reset Values")
layout.separator()
col = layout.column()
col.scale_y = 1.5
col.operator(OT_CreateSphere.bl_idname, icon='MESH_UVSPHERE', text="Create Selected Parts")
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_CreateSphere, OT_CopyFullScript, OT_Reset, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, PT_LinksPanel, PT_RemovePanel)
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_SphereProps))
def unregister():
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes): bpy.utils.unregister_class(c)
if __name__ == "__main__": register()
# Copied: 15:00:01
import bpy
import bmesh
import math
import webbrowser
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 = "Interferometer20260312_v2"
TAB_NAME = "[ Interferometer ] "
# ### ZIONAD_SOURCE_ID: INTERFEROMETER_2026_03_12_V2 ###
bl_info = {
"name": f"zionad 520 [ Interferometer Gen ] {PREFIX}",
"author": "zionadchat",
"version": (3, 4, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": "5-Part Interferometer Sphere Generator (Individual Colors & Toggles)",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: INTERFEROMETER_2026_03_12_V2 ###"
ADDON_LINKS = (
{"label": "あっさり干渉計 20260312版", "url": "<https://www.notion.so/20260312-320f5dacaf438031a63dd9fc00edc049>"},
{"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,
"sphere_loc": (0.0000, 0.0000, 0.0000),
"sphere_radius": 30.0000,
"cone_angle": 90.0000,
"split_offset": 5.0000,
"show_top_cap": True,
"color_top_cap": (0.8000, 0.2000, 0.2000, 0.8000),
"show_top_cone": True,
"color_top_cone": (0.8000, 0.5000, 0.2000, 0.8000),
"show_mid": True,
"color_mid": (0.2000, 0.8000, 0.2000, 0.8000),
"show_bot_cone": True,
"color_bot_cone": (0.2000, 0.5000, 0.8000, 0.8000),
"show_bot_cap": True,
"color_bot_cap": (0.5000, 0.2000, 0.8000, 0.8000),
}
# <END_DICT>
# ==============================================================================
# マテリアル作成ロジック
# ==============================================================================
def apply_material_color(mat, color):
mat.diffuse_color = color
if mat.use_nodes:
tree = mat.node_tree
bsdf = tree.nodes.get("Principled BSDF")
if not bsdf:
for node in tree.nodes:
if node.type == 'BSDF_PRINCIPLED':
bsdf = node
break
if bsdf:
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 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'
apply_material_color(mat, color)
return mat
def get_preview_material(color, name="Mat_Prev"):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
apply_material_color(mat, color)
return mat
# ==============================================================================
# ジオメトリ構築ロジック
# ==============================================================================
def build_meshes(props, prefix):
meshes = {}
R = props.sphere_radius
theta = math.radians(props.cone_angle / 2)
H = R * math.cos(theta)
r_base = R * math.sin(theta)
depth = R * 2.5
r_cut = depth * math.tan(theta)
def make_mesh(name, bm):
mesh = bpy.data.meshes.get(name)
if not mesh: mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
return mesh
# 1. Base Sphere
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=R)
meshes['base'] = make_mesh(f"{prefix}_Base", bm)
# 2. Top Cone
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=64, radius1=0.0, radius2=r_base, depth=H)
bmesh.ops.translate(bm, vec=(0, 0, H/2), verts=bm.verts)
meshes['cone_top'] = make_mesh(f"{prefix}_ConeTop", bm)
# 3. Bottom Cone
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=64, radius1=r_base, radius2=0.0, depth=H)
bmesh.ops.translate(bm, vec=(0, 0, -H/2), verts=bm.verts)
meshes['cone_bot'] = make_mesh(f"{prefix}_ConeBot", bm)
# 4. Cutter Cone Top
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=64, radius1=0.0, radius2=r_cut, depth=depth)
bmesh.ops.translate(bm, vec=(0, 0, depth/2), verts=bm.verts)
meshes['cut_top'] = make_mesh(f"{prefix}_CutTop", bm)
# 5. Cutter Cone Bottom
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=64, radius1=r_cut, radius2=0.0, depth=depth)
bmesh.ops.translate(bm, vec=(0, 0, -depth/2), verts=bm.verts)
meshes['cut_bot'] = make_mesh(f"{prefix}_CutBot", bm)
# 6. Cutter Box Top
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=(R*4, R*4, R*4), verts=bm.verts)
bmesh.ops.translate(bm, vec=(0, 0, H + 2*R), verts=bm.verts)
meshes['box_top'] = make_mesh(f"{prefix}_BoxTop", bm)
# 7. Cutter Box Bottom
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=(R*4, R*4, R*4), verts=bm.verts)
bmesh.ops.translate(bm, vec=(0, 0, -H - 2*R), verts=bm.verts)
meshes['box_bot'] = make_mesh(f"{prefix}_BoxBot", bm)
return meshes
def setup_cutters(meshes, loc, offset_v, collection, is_preview=False):
cutters = {}
names =[('cut_top', "Cut_ConeTop"), ('cut_bot', "Cut_ConeBot"),
('box_top', "Cut_BoxTop"), ('box_bot', "Cut_BoxBot")]
for key, name in names:
obj = bpy.data.objects.new(name, meshes[key])
obj.display_type = 'BOUNDS'
obj.hide_viewport = True
obj.hide_render = True
if is_preview: obj[PREVIEW_TAG] = True
collection.objects.link(obj)
cutters[key] = obj
cutters['cut_top'].location = loc
cutters['cut_bot'].location = loc
cutters['box_top'].location = loc + Vector((0, 0, offset_v * 2))
cutters['box_bot'].location = loc + Vector((0, 0, -offset_v * 2))
return cutters
def setup_parts(meshes, cutters, loc, offset_v):
parts = {}
parts['top_cap'] = bpy.data.objects.new("Part_TopCap", meshes['base'])
parts['top_cone'] = bpy.data.objects.new("Part_TopCone", meshes['cone_top'])
parts['mid'] = bpy.data.objects.new("Part_Middle", meshes['base'])
parts['bot_cone'] = bpy.data.objects.new("Part_BotCone", meshes['cone_bot'])
parts['bot_cap'] = bpy.data.objects.new("Part_BotCap", meshes['base'])
parts['top_cap'].location = loc + Vector((0, 0, offset_v * 2))
parts['top_cone'].location = loc + Vector((0, 0, offset_v * 1))
parts['mid'].location = loc
parts['bot_cone'].location = loc + Vector((0, 0, -offset_v * 1))
parts['bot_cap'].location = loc + Vector((0, 0, -offset_v * 2))
mod = parts['top_cap'].modifiers.new("Bool", 'BOOLEAN')
mod.operation = 'INTERSECT'; mod.object = cutters['box_top']; mod.solver = 'EXACT'
mod = parts['bot_cap'].modifiers.new("Bool", 'BOOLEAN')
mod.operation = 'INTERSECT'; mod.object = cutters['box_bot']; mod.solver = 'EXACT'
mod = parts['mid'].modifiers.new("Bool1", 'BOOLEAN')
mod.operation = 'DIFFERENCE'; mod.object = cutters['cut_top']; mod.solver = 'EXACT'
mod = parts['mid'].modifiers.new("Bool2", 'BOOLEAN')
mod.operation = 'DIFFERENCE'; mod.object = cutters['cut_bot']; mod.solver = 'EXACT'
return parts
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_TAG = f"{PREFIX}_preview_tag"
def update_preview_geometry(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_NAME)
context.scene.collection.children.link(col)
for o in[o for o in col.objects if o.get(PREVIEW_TAG)]:
bpy.data.objects.remove(o, do_unlink=True)
if not props.show_preview: return
meshes = build_meshes(props, f"Prev_{PREFIX}")
loc = Vector(props.sphere_loc)
offset_v = props.split_offset
cutters = setup_cutters(meshes, loc, offset_v, col, is_preview=True)
parts = setup_parts(meshes, cutters, loc, offset_v)
parts_config =[
('top_cap', props.show_top_cap, props.color_top_cap, "TCap"),
('top_cone', props.show_top_cone, props.color_top_cone, "TCone"),
('mid', props.show_mid, props.color_mid, "Mid"),
('bot_cone', props.show_bot_cone, props.color_bot_cone, "BCone"),
('bot_cap', props.show_bot_cap, props.color_bot_cap, "BCap"),
]
for key, is_show, color, mat_name in parts_config:
obj = parts[key]
if is_show:
obj[PREVIEW_TAG] = True
col.objects.link(obj)
obj.data.materials.append(get_preview_material(color, f"Mat_Prev_{mat_name}"))
else:
bpy.data.objects.remove(obj, do_unlink=True)
_timer = None
def delayed_update():
global _timer
_timer = None
if bpy.context and bpy.context.scene:
update_preview_geometry(bpy.context)
return None
def on_update(self, context):
global _timer
if _timer:
try: bpy.app.timers.unregister(_timer)
except: pass
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_SphereProps(PropertyGroup):
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
sphere_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['sphere_loc'], update=on_update)
sphere_radius: FloatProperty(name="Sphere Radius", default=CURRENT_DEFAULTS['sphere_radius'], min=0.01, update=on_update)
cone_angle: FloatProperty(name="Cone Angle", default=CURRENT_DEFAULTS['cone_angle'], min=1.0, max=179.0, update=on_update)
split_offset: FloatProperty(name="Split Z-Offset", default=CURRENT_DEFAULTS['split_offset'], min=0.0, update=on_update)
show_top_cap: BoolProperty(name="Top Cap", default=CURRENT_DEFAULTS['show_top_cap'], update=on_update)
color_top_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_top_cap'], update=on_update)
show_top_cone: BoolProperty(name="Top Cone", default=CURRENT_DEFAULTS['show_top_cone'], update=on_update)
color_top_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_top_cone'], update=on_update)
show_mid: BoolProperty(name="Middle Sphere", default=CURRENT_DEFAULTS['show_mid'], update=on_update)
color_mid: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_mid'], update=on_update)
show_bot_cone: BoolProperty(name="Bottom Cone", default=CURRENT_DEFAULTS['show_bot_cone'], update=on_update)
color_bot_cone: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_bot_cone'], update=on_update)
show_bot_cap: BoolProperty(name="Bottom Cap", default=CURRENT_DEFAULTS['show_bot_cap'], update=on_update)
color_bot_cap: FloatVectorProperty(name="", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_bot_cap'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CreateSphere(Operator):
bl_idname = f"{OP_PREFIX}.create_sphere"
bl_label = "Create Selected Parts"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
timestamp = datetime.now().strftime('%H%M%S')
meshes = build_meshes(props, f"Temp_{timestamp}")
loc = Vector(props.sphere_loc)
offset_v = props.split_offset
col = context.collection if context.collection else context.scene.collection
cutters = setup_cutters(meshes, loc, offset_v, col, is_preview=False)
parts = setup_parts(meshes, cutters, loc, offset_v)
# モディファイアを評価するために一度シーンにリンク
for obj in parts.values():
col.objects.link(obj)
dg = context.evaluated_depsgraph_get()
for obj in parts.values():
if obj.modifiers:
eval_obj = obj.evaluated_get(dg)
mesh_applied = bpy.data.meshes.new_from_object(eval_obj)
obj.modifiers.clear()
obj.data = mesh_applied
# カッターの削除
for obj in cutters.values():
bpy.data.objects.remove(obj, do_unlink=True)
# UIの設定に従ってオブジェクトを残すか削除するかを判定
parts_config =[
('top_cap', props.show_top_cap, props.color_top_cap, "TopCap"),
('top_cone', props.show_top_cone, props.color_top_cone, "TopCone"),
('mid', props.show_mid, props.color_mid, "Middle"),
('bot_cone', props.show_bot_cone, props.color_bot_cone, "BotCone"),
('bot_cap', props.show_bot_cap, props.color_bot_cap, "BotCap"),
]
active_obj = None
bpy.ops.object.select_all(action='DESELECT')
for key, is_show, color, mat_name in parts_config:
obj = parts[key]
if is_show:
obj.data.materials.append(create_unique_material(color, f"Mat_{mat_name}"))
obj.name = f"Sphere_{mat_name}_{timestamp}"
obj.select_set(True)
active_obj = obj
else:
bpy.data.objects.remove(obj, do_unlink=True)
# テンポラリメッシュの削除
for k, m in meshes.items():
if k in ('base', 'cone_top', 'cone_bot', 'cut_top', 'cut_bot', 'box_top', 'box_bot'):
bpy.data.meshes.remove(m)
if active_obj:
context.view_layer.objects.active = active_obj
self.report({'INFO'}, "Created Interferometer Parts")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string():
target_text = t; break
if not target_text:
self.report({'ERROR'}, "Script source not found.")
return {'CANCELLED'}
code = target_text.as_string()
l = props.sphere_loc
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "sphere_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
new_dict += f' "sphere_radius": {props.sphere_radius:.4f},\n'
new_dict += f' "cone_angle": {props.cone_angle:.4f},\n'
new_dict += f' "split_offset": {props.split_offset:.4f},\n'
for key in["top_cap", "top_cone", "mid", "bot_cone", "bot_cap"]:
s = getattr(props, f"show_{key}")
c = getattr(props, f"color_{key}")
new_dict += f' "show_{key}": {s},\n'
new_dict += f' "color_{key}": ({c[0]:.4f}, {c[1]:.4f}, {c[2]:.4f}, {c[3]:.4f}),\n'
new_dict += "}\n"
try:
start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
pre, post = code.split(start)[0], code.split(end)[1]
final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
context.window_manager.clipboard = final
self.report({'INFO'}, "Code copied!")
except: return {'CANCELLED'}
return {'FINISHED'}
class OT_Reset(Operator):
bl_idname = f"{OP_PREFIX}.reset"
bl_label = "Reset"
def execute(self, context):
p = getattr(context.scene, PROPS_NAME)
p.sphere_loc = (0,0,0)
p.sphere_radius = 30.0
p.cone_angle = 90.0
p.split_offset = 5.0
p.show_top_cap = True; p.color_top_cap = (0.8, 0.2, 0.2, 0.8)
p.show_top_cone = True; p.color_top_cone = (0.8, 0.5, 0.2, 0.8)
p.show_mid = True; p.color_mid = (0.2, 0.8, 0.2, 0.8)
p.show_bot_cone = True; p.color_bot_cone = (0.2, 0.5, 0.8, 0.8)
p.show_bot_cap = True; p.color_bot_cap = (0.5, 0.2, 0.8, 0.8)
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 = "Interferometer Generator"
bl_idname = f"{PREFIX}_PT_main"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: layout.label(text="Reload Script"); return
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
box = layout.box()
if not props.show_preview:
box.label(text="Preview is Hidden", icon='INFO')
box.prop(props, "sphere_loc")
box.prop(props, "sphere_radius")
box.prop(props, "cone_angle")
box.prop(props, "split_offset")
layout.separator()
# --- パーツごとの表示・色設定 ---
parts_box = layout.box()
parts_box.label(text="Parts Selection & Colors", icon='MATERIAL')
parts_ui =[
("top_cap", "Top Cap"),
("top_cone", "Top Cone"),
("mid", "Middle Sphere"),
("bot_cone", "Bottom Cone"),
("bot_cap", "Bottom Cap"),
]
for key, label in parts_ui:
row = parts_box.row(align=True)
show_prop = f"show_{key}"
color_prop = f"color_{key}"
is_show = getattr(props, show_prop)
icon = 'RESTRICT_VIEW_OFF' if is_show else 'RESTRICT_VIEW_ON'
# 左側に目のアイコン(トグル)
row.prop(props, show_prop, icon=icon, text="")
# 右側にカラーピッカー(非表示の時はグレーアウト)
sub = row.row()
sub.active = is_show
sub.prop(props, color_prop, text=label)
layout.separator()
layout.operator(OT_Reset.bl_idname, icon='LOOP_BACK', text="Reset Values")
layout.separator()
col = layout.column()
col.scale_y = 1.5
col.operator(OT_CreateSphere.bl_idname, icon='MESH_UVSPHERE', text="Create Selected Parts")
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_CreateSphere, OT_CopyFullScript, OT_Reset, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, PT_LinksPanel, PT_RemovePanel)
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_SphereProps))
def unregister():
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes): bpy.utils.unregister_class(c)
if __name__ == "__main__": register()