https://posfie.com/@timekagura?sort=0&page=1


# 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()