blender Million 2026
# 2026-03-06 13:36:06 Session Template
# Blender 4.0+ Compatible
UNIQUE_SCRIPT_ID = "RELATIVITY_VISUALIZER_2026_03_06_V1"
SCRIPT_VERSION = 1
bl_info = {
"name": "Relativity Visualizer: Reality vs Image",
"author": "zionadchat Gemini",
"version": (6, 1),
"blender": (4, 0, 0),
"location": "View3D > Sidebar",
"description": "Maxwellの絶対静止系における「実体の位置」と「遅れて届く映像」のズレを可視化",
"category": "Physics",
}
import bpy
import bmesh
import math
import webbrowser
from mathutils import Vector
from datetime import datetime
# ==============================================================================
# DYNAMIC DEFAULTS (Saved State)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"velocity": 0.6000,
"radius": 10.0000,
"obs_time": 0.0000,
"show_rays": True,
"resolution": 12,
"ring_thick": 0.1000,
"ray_thick": 0.0500,
"obs_size": 0.3000,
}
# <END_DICT>
# ==============================================================================
# SYSTEM DEFAULTS (Factory Reset)
# ==============================================================================
SYSTEM_DEFAULTS = {
"velocity": 0.6,
"radius": 5.0,
"obs_time": 10.0,
"show_rays": True,
"resolution": 72,
"ring_thick": 0.1,
"ray_thick": 0.05,
"obs_size": 0.3,
}
TAB_NAME = "77Relativity_Visual"
COLLECTION_NAME = "Relativity_Visualizer_Output"
OBJECT_TAG = "relativity_visualizer_tag"
ADDON_LINKS = (
{"label": "Code Copy Template 20260221", "url": "<https://www.notion.so/Code-copy-20260221-30ef5dacaf4380f2984bd865b38b55b3>"},
{"label": "Theory Background: Notion Doc", "url": "<https://www.notion.so/Einstein-from-20260119-main-2edc563be1b080bb94d9f6e5b667fdec>"},
{"label": "Blender Simulation Guide", "url": "<https://www.notion.so/blender-deviationtokyo-30c293bfbb2980118c25dfc02259b096>"},
)
# ------------------------------------------------------------------------
# Material Setup (Improved Safety)
# ------------------------------------------------------------------------
def get_fixed_material(name, color):
mat = bpy.data.materials.get(name) or bpy.data.materials.new(name=name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
nodes = mat.node_tree.nodes
links = mat.node_tree.links
bsdf = nodes.get("Principled BSDF")
# If the user deleted the node manually, recreate it
if not bsdf:
nodes.clear()
bsdf = nodes.new("ShaderNodeBsdfPrincipled")
output = nodes.new("ShaderNodeOutputMaterial")
output.location = (300, 0)
links.new(bsdf.outputs[0], output.inputs[0])
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]
mat.diffuse_color = color
return mat
# ------------------------------------------------------------------------
# Object Creation Utilities
# ------------------------------------------------------------------------
def create_curve(col, name, points, thickness, mat_name, color, circular=False):
curve = bpy.data.curves.new(name, 'CURVE')
curve.dimensions = '3D'
obj = bpy.data.objects.new(name, curve)
obj[OBJECT_TAG] = True
col.objects.link(obj)
spline = curve.splines.new('POLY')
spline.use_cyclic_u = circular
spline.points.add(len(points) - 1)
for i, p in enumerate(points):
spline.points[i].co = (p.x, p.y, p.z, 1)
curve.bevel_depth = thickness
obj.data.materials.append(get_fixed_material(mat_name, color))
return obj
def create_sphere(col, name, location, radius, mat_name, color):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
obj[OBJECT_TAG] = True
col.objects.link(obj)
bm = bmesh.new()
try:
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=radius)
bmesh.ops.translate(bm, vec=Vector(location), verts=bm.verts)
bm.to_mesh(mesh)
except Exception as e:
print(f"BMesh Error: {e}")
finally:
bm.free()
obj.data.materials.append(get_fixed_material(mat_name, color))
return obj
# ------------------------------------------------------------------------
# Core Drawing Logic
# ------------------------------------------------------------------------
def draw_rel_visual_core(context):
if not context or not context.scene: return
p = context.scene.rel_visual
v = p.velocity
R = p.radius
t_obs = p.obs_time
res = p.resolution
# Safe Collection Linking
col = bpy.data.collections.get(COLLECTION_NAME)
if not col:
col = bpy.data.collections.new(COLLECTION_NAME)
if col.name not in context.scene.collection.children:
context.scene.collection.children.link(col)
# Clean up previous tagged objects
objects_to_remove =[obj for obj in col.objects if obj.get(OBJECT_TAG)]
for obj in objects_to_remove:
mesh_data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if mesh_data and mesh_data.users == 0:
if isinstance(mesh_data, bpy.types.Mesh):
bpy.data.meshes.remove(mesh_data)
elif isinstance(mesh_data, bpy.types.Curve):
bpy.data.curves.remove(mesh_data)
# 1. 観測者(現在位置)
center_pos_now = Vector((v * t_obs, 0, t_obs))
create_sphere(col, "Observer_Now", center_pos_now, p.obs_size, "Mat_Obs", (0.0, 0.5, 1.0, 1.0)) # 青
# 2. 実体のリング (Physical Ring - Green)
phys_points =[]
for i in range(res):
phi = math.radians(i * 360.0 / res)
x = v * t_obs + R * math.cos(phi)
y = R * math.sin(phi)
z = t_obs
phys_points.append(Vector((x, y, z)))
create_curve(col, "Physical_Ring", phys_points, p.ring_thick, "Mat_Phys", (0.0, 1.0, 0.0, 0.8), circular=True) # 緑
# 3. 映像のリング (Visual Ring - Red) & 光路
visual_points = []
ray_paths =[]
denom = 1.0 - v**2
if abs(denom) < 1e-9: denom = 1e-9 # v=1対策
for i in range(res):
phi = math.radians(i * 360.0 / res)
term_sqrt = math.sqrt(max(0.0, 1.0 - (v * math.sin(phi))**2))
term_vcos = v * math.cos(phi)
dt = (R * (term_sqrt - term_vcos)) / denom
t_emit = t_obs - dt
x_emit = v * t_emit + R * math.cos(phi)
y_emit = R * math.sin(phi)
z_emit = t_emit
emit_pos = Vector((x_emit, y_emit, z_emit))
visual_points.append(emit_pos)
if p.show_rays:
ray_paths.append([emit_pos, center_pos_now])
create_curve(col, "Visual_Ring", visual_points, p.ring_thick, "Mat_Vis", (1.0, 0.0, 0.0, 0.8), circular=True) # 赤
if p.show_rays:
for i, path in enumerate(ray_paths):
create_curve(col, f"Ray_{i}", path, p.ray_thick, "Mat_Ray", (1.0, 1.0, 0.0, 0.3)) # 黄色
# ------------------------------------------------------------------------
# Update Throttling (Debounce)
# ------------------------------------------------------------------------
_update_timer = None
def delayed_update_func():
global _update_timer
_update_timer = None
try:
if bpy.context and bpy.context.scene:
draw_rel_visual_core(bpy.context)
except Exception as e:
print(f"Update Error: {e}")
return None
def update_view(self, context):
global _update_timer
if _update_timer:
try:
bpy.app.timers.unregister(_update_timer)
except ValueError:
pass
_update_timer = bpy.app.timers.register(delayed_update_func, first_interval=0.05)
# ------------------------------------------------------------------------
# Properties
# ------------------------------------------------------------------------
class PG_RelativityVisual(bpy.types.PropertyGroup):
velocity: bpy.props.FloatProperty(
name="Velocity (v/c)", default=CURRENT_DEFAULTS["velocity"], min=0.0, max=0.99, update=update_view,
description="円の移動速度 (光速=1)"
)
radius: bpy.props.FloatProperty(
name="Radius (R)", default=CURRENT_DEFAULTS["radius"], min=0.1, update=update_view,
description="円の半径(剛体として計算)"
)
obs_time: bpy.props.FloatProperty(
name="Observation Time (t)", default=CURRENT_DEFAULTS["obs_time"], min=0.0, update=update_view,
description="観測者が「見た」時刻。この瞬間の実体と映像を計算します。"
)
show_rays: bpy.props.BoolProperty(name="Show Light Paths", default=CURRENT_DEFAULTS["show_rays"], update=update_view)
resolution: bpy.props.IntProperty(name="Resolution", default=CURRENT_DEFAULTS["resolution"], min=12, max=360, update=update_view)
ring_thick: bpy.props.FloatProperty(name="Ring Thick", default=CURRENT_DEFAULTS["ring_thick"], min=0.01, update=update_view)
ray_thick: bpy.props.FloatProperty(name="Ray Thick", default=CURRENT_DEFAULTS["ray_thick"], min=0.01, update=update_view)
obs_size: bpy.props.FloatProperty(name="Observer Size", default=CURRENT_DEFAULTS["obs_size"], min=0.01, update=update_view)
# ------------------------------------------------------------------------
# Operators
# ------------------------------------------------------------------------
class OBJECT_OT_DrawRelVisual(bpy.types.Operator):
bl_idname = "object.draw_rel_visual"
bl_label = "Force Refresh"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
draw_rel_visual_core(context)
return {'FINISHED'}
class OBJECT_OT_DetachVisual(bpy.types.Operator):
bl_idname = "object.detach_visual"
bl_label = "Detach & Keep"
bl_description = "Stop controlling the current visualization and keep it in the scene"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col = bpy.data.collections.get(COLLECTION_NAME)
if not col: return {'CANCELLED'}
targets =[obj for obj in col.objects if obj.get(OBJECT_TAG)]
if not targets:
self.report({'WARNING'}, "No active visualization to detach.")
return {'CANCELLED'}
scene_col = context.scene.collection
count = 0
timestamp = datetime.now().strftime('%H%M%S')
for obj in targets:
if OBJECT_TAG in obj: del obj[OBJECT_TAG]
orig_name = obj.name
obj.name = f"{orig_name}_Baked_{timestamp}"
# Independent Material Logic
if obj.data.materials:
old_mat = obj.data.materials[0]
new_mat = old_mat.copy()
new_mat.name = f"{old_mat.name}_Baked_{timestamp}"
obj.data.materials.clear()
obj.data.materials.append(new_mat)
if obj.name not in scene_col.objects:
scene_col.objects.link(obj)
col.objects.unlink(obj)
obj.select_set(True)
context.view_layer.objects.active = obj
count += 1
self.report({'INFO'}, f"Detached {count} object(s).")
return {'FINISHED'}
class WM_OT_ResetToDefaults(bpy.types.Operator):
bl_idname = "wm.reset_to_defaults"
bl_label = "Reset to Defaults"
bl_description = "Reset parameters to system defaults"
def execute(self, context):
p = context.scene.rel_visual
for key, val in SYSTEM_DEFAULTS.items():
setattr(p, key, val)
self.report({'INFO'}, "Parameters reset to defaults.")
return {'FINISHED'}
class WM_OT_RemoveAddon(bpy.types.Operator):
bl_idname = "wm.remove_addon"
bl_label = "Remove Addon"
def execute(self, context):
def cleanup_logic():
if __name__ == "__main__":
unregister()
print(f"[{bl_info['name']}] Unregistered from Script Mode.")
else:
import addon_utils
module_name = __package__ if __package__ else __name__
addon_utils.disable(module_name, default_set=True)
print(f"[{bl_info['name']}] Disabled from Addon Mode.")
for win in bpy.context.window_manager.windows:
for area in win.screen.areas:
area.tag_redraw()
bpy.app.timers.register(cleanup_logic, first_interval=0.1)
self.report({'INFO'}, "Removing Addon UI...")
return {'FINISHED'}
class WM_OT_CopyFullScript(bpy.types.Operator):
bl_idname = "wm.copy_full_script"
bl_label = "Copy Full Script"
def execute(self, context):
p = context.scene.rel_visual
M_START, M_END = "# <BEG" + "IN_DICT>", "# <EN" + "D_DICT>"
found_texts =[t for t in bpy.data.texts if UNIQUE_SCRIPT_ID in t.as_string()]
if not found_texts:
self.report({'ERROR'}, "Script source not found.")
return {'CANCELLED'}
target_text = found_texts[0]
code_str = target_text.as_string()
d_str = "CURRENT_DEFAULTS = {\n"
for k in CURRENT_DEFAULTS.keys():
val = getattr(p, k)
if isinstance(val, str): d_str += f' "{k}": "{val}",\n'
elif hasattr(val, "__len__") and not isinstance(val, str):
v_str = ", ".join([f"{v:.4f}" for v in val])
d_str += f' "{k}": ({v_str}),\n'
elif isinstance(val, float): d_str += f' "{k}": {val:.4f},\n'
else: d_str += f' "{k}": {val},\n'
d_str += "}\n"
try:
pre_dict = code_str.split(M_START)[0]; post_dict = code_str.split(M_END)[1]
new_code = pre_dict + M_START + "\n" + d_str + M_END + post_dict
context.window_manager.clipboard = f"# {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Session Template\n" + '\n'.join(new_code.split('\n')[1:])
self.report({'INFO'}, "Code copied with current values.")
except Exception as e:
self.report({'ERROR'}, f"Failed to parse: {e}")
return {'CANCELLED'}
return {'FINISHED'}
class WM_OT_OpenUrl(bpy.types.Operator):
bl_idname = "wm.open_url"
bl_label = "Open URL"
url: bpy.props.StringProperty()
def execute(self, context):
webbrowser.open(self.url)
return {'FINISHED'}
# ------------------------------------------------------------------------
# UI Panels
# ------------------------------------------------------------------------
class VIEW3D_PT_RelVisualPanel(bpy.types.Panel):
bl_label = "Relativity Visualizer"
bl_idname = "VIEW3D_PT_rel_visual"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
p = context.scene.rel_visual
row = layout.row()
row.operator("wm.copy_full_script", text="Copy Code with Values", icon='COPY_ID')
layout.separator()
box = layout.box()
box.label(text="Time Control (重要)", icon='TIME')
box.prop(p, "obs_time", text="Observation Time (t)")
box = layout.box()
box.label(text="Physics Parameters", icon='PHYSICS')
box.prop(p, "velocity")
box.prop(p, "radius")
box = layout.box()
box.label(text="Visual Settings", icon='MESH_GRID')
box.prop(p, "show_rays")
box.prop(p, "resolution")
box.prop(p, "ring_thick")
box.prop(p, "ray_thick")
box.prop(p, "obs_size")
layout.separator()
row = layout.row()
row.label(text="Green: Physical Reality (Now)", icon='FILE_3D')
row = layout.row()
row.label(text="Red: Visual Image (Past)", icon='IMAGE_RGB')
layout.separator()
layout.operator("wm.reset_to_defaults", text="Reset to Defaults", icon='LOOP_BACK')
row = layout.row()
row.scale_y = 1.5
row.operator("object.detach_visual", text="Detach & Keep", icon='PINNED')
layout.operator("object.draw_rel_visual", text="Force Refresh", icon='FILE_REFRESH')
class VIEW3D_PT_Links(bpy.types.Panel):
bl_label = "Theory Links"
bl_idname = "VIEW3D_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:
op = self.layout.operator("wm.open_url", text=l["label"])
op.url = l["url"]
class VIEW3D_PT_System(bpy.types.Panel):
bl_label = "System"
bl_idname = "VIEW3D_PT_system"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
self.layout.operator("wm.remove_addon", icon='CANCEL', text="Remove Addon")
# ------------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------------
classes = (
PG_RelativityVisual,
OBJECT_OT_DrawRelVisual,
OBJECT_OT_DetachVisual,
WM_OT_ResetToDefaults,
WM_OT_RemoveAddon,
WM_OT_CopyFullScript,
WM_OT_OpenUrl,
VIEW3D_PT_RelVisualPanel,
VIEW3D_PT_Links,
VIEW3D_PT_System,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
if not hasattr(bpy.types.Scene, "rel_visual"):
bpy.types.Scene.rel_visual = bpy.props.PointerProperty(type=PG_RelativityVisual)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if hasattr(bpy.types.Scene, "rel_visual"):
del bpy.types.Scene.rel_visual
if __name__ == "__main__":
register()
# 2026-03-06 Session Template
# Blender 4.0+ Compatible
UNIQUE_SCRIPT_ID = "RELATIVITY_VISUALIZER_2026_03_06_V1"
SCRIPT_VERSION = 1
bl_info = {
"name": "Relativity Visualizer: Reality vs Image",
"author": "zionadchat Gemini",
"version": (6, 1),
"blender": (4, 0, 0),
"location": "View3D > Sidebar",
"description": "Maxwellの絶対静止系における「実体の位置」と「遅れて届く映像」のズレを可視化",
"category": "Physics",
}
import bpy
import bmesh
import math
import webbrowser
from mathutils import Vector
from datetime import datetime
# ==============================================================================
# DYNAMIC DEFAULTS (Saved State)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"velocity": 0.6000,
"radius": 5.0000,
"obs_time": 10.0000,
"show_rays": True,
"resolution": 72,
"ring_thick": 0.1000,
"ray_thick": 0.0500,
"obs_size": 0.3000,
}
# <END_DICT>
# ==============================================================================
# SYSTEM DEFAULTS (Factory Reset)
# ==============================================================================
SYSTEM_DEFAULTS = {
"velocity": 0.6,
"radius": 5.0,
"obs_time": 10.0,
"show_rays": True,
"resolution": 72,
"ring_thick": 0.1,
"ray_thick": 0.05,
"obs_size": 0.3,
}
TAB_NAME = "Relativity_Visual"
COLLECTION_NAME = "Relativity_Visualizer_Output"
OBJECT_TAG = "relativity_visualizer_tag"
ADDON_LINKS = (
{"label": "Code Copy Template 20260221", "url": "<https://www.notion.so/Code-copy-20260221-30ef5dacaf4380f2984bd865b38b55b3>"},
{"label": "Theory Background: Notion Doc", "url": "<https://www.notion.so/Einstein-from-20260119-main-2edc563be1b080bb94d9f6e5b667fdec>"},
{"label": "Blender Simulation Guide", "url": "<https://www.notion.so/blender-deviationtokyo-30c293bfbb2980118c25dfc02259b096>"},
)
# ------------------------------------------------------------------------
# Material Setup (Improved Safety)
# ------------------------------------------------------------------------
def get_fixed_material(name, color):
mat = bpy.data.materials.get(name) or bpy.data.materials.new(name=name)
mat.use_nodes = True
mat.blend_method = 'BLEND'
nodes = mat.node_tree.nodes
links = mat.node_tree.links
bsdf = nodes.get("Principled BSDF")
# If the user deleted the node manually, recreate it
if not bsdf:
nodes.clear()
bsdf = nodes.new("ShaderNodeBsdfPrincipled")
output = nodes.new("ShaderNodeOutputMaterial")
output.location = (300, 0)
links.new(bsdf.outputs[0], output.inputs[0])
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]
mat.diffuse_color = color
return mat
# ------------------------------------------------------------------------
# Object Creation Utilities
# ------------------------------------------------------------------------
def create_curve(col, name, points, thickness, mat_name, color, circular=False):
curve = bpy.data.curves.new(name, 'CURVE')
curve.dimensions = '3D'
obj = bpy.data.objects.new(name, curve)
obj[OBJECT_TAG] = True
col.objects.link(obj)
spline = curve.splines.new('POLY')
spline.use_cyclic_u = circular
spline.points.add(len(points) - 1)
for i, p in enumerate(points):
spline.points[i].co = (p.x, p.y, p.z, 1)
curve.bevel_depth = thickness
obj.data.materials.append(get_fixed_material(mat_name, color))
return obj
def create_sphere(col, name, location, radius, mat_name, color):
mesh = bpy.data.meshes.new(name)
obj = bpy.data.objects.new(name, mesh)
obj[OBJECT_TAG] = True
col.objects.link(obj)
bm = bmesh.new()
try:
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=radius)
bmesh.ops.translate(bm, vec=Vector(location), verts=bm.verts)
bm.to_mesh(mesh)
except Exception as e:
print(f"BMesh Error: {e}")
finally:
bm.free()
obj.data.materials.append(get_fixed_material(mat_name, color))
return obj
# ------------------------------------------------------------------------
# Core Drawing Logic
# ------------------------------------------------------------------------
def draw_rel_visual_core(context):
if not context or not context.scene: return
p = context.scene.rel_visual
v = p.velocity
R = p.radius
t_obs = p.obs_time
res = p.resolution
# Safe Collection Linking
col = bpy.data.collections.get(COLLECTION_NAME)
if not col:
col = bpy.data.collections.new(COLLECTION_NAME)
if col.name not in context.scene.collection.children:
context.scene.collection.children.link(col)
# Clean up previous tagged objects
objects_to_remove =[obj for obj in col.objects if obj.get(OBJECT_TAG)]
for obj in objects_to_remove:
mesh_data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if mesh_data and mesh_data.users == 0:
if isinstance(mesh_data, bpy.types.Mesh):
bpy.data.meshes.remove(mesh_data)
elif isinstance(mesh_data, bpy.types.Curve):
bpy.data.curves.remove(mesh_data)
# 1. 観測者(現在位置)
center_pos_now = Vector((v * t_obs, 0, t_obs))
create_sphere(col, "Observer_Now", center_pos_now, p.obs_size, "Mat_Obs", (0.0, 0.5, 1.0, 1.0)) # 青
# 2. 実体のリング (Physical Ring - Green)
phys_points =[]
for i in range(res):
phi = math.radians(i * 360.0 / res)
x = v * t_obs + R * math.cos(phi)
y = R * math.sin(phi)
z = t_obs
phys_points.append(Vector((x, y, z)))
create_curve(col, "Physical_Ring", phys_points, p.ring_thick, "Mat_Phys", (0.0, 1.0, 0.0, 0.8), circular=True) # 緑
# 3. 映像のリング (Visual Ring - Red) & 光路
visual_points = []
ray_paths =[]
denom = 1.0 - v**2
if abs(denom) < 1e-9: denom = 1e-9 # v=1対策
for i in range(res):
phi = math.radians(i * 360.0 / res)
term_sqrt = math.sqrt(max(0.0, 1.0 - (v * math.sin(phi))**2))
term_vcos = v * math.cos(phi)
dt = (R * (term_sqrt - term_vcos)) / denom
t_emit = t_obs - dt
x_emit = v * t_emit + R * math.cos(phi)
y_emit = R * math.sin(phi)
z_emit = t_emit
emit_pos = Vector((x_emit, y_emit, z_emit))
visual_points.append(emit_pos)
if p.show_rays:
ray_paths.append([emit_pos, center_pos_now])
create_curve(col, "Visual_Ring", visual_points, p.ring_thick, "Mat_Vis", (1.0, 0.0, 0.0, 0.8), circular=True) # 赤
if p.show_rays:
for i, path in enumerate(ray_paths):
create_curve(col, f"Ray_{i}", path, p.ray_thick, "Mat_Ray", (1.0, 1.0, 0.0, 0.3)) # 黄色
# ------------------------------------------------------------------------
# Update Throttling (Debounce)
# ------------------------------------------------------------------------
_update_timer = None
def delayed_update_func():
global _update_timer
_update_timer = None
try:
if bpy.context and bpy.context.scene:
draw_rel_visual_core(bpy.context)
except Exception as e:
print(f"Update Error: {e}")
return None
def update_view(self, context):
global _update_timer
if _update_timer:
try:
bpy.app.timers.unregister(_update_timer)
except ValueError:
pass
_update_timer = bpy.app.timers.register(delayed_update_func, first_interval=0.05)
# ------------------------------------------------------------------------
# Properties
# ------------------------------------------------------------------------
class PG_RelativityVisual(bpy.types.PropertyGroup):
velocity: bpy.props.FloatProperty(
name="Velocity (v/c)", default=CURRENT_DEFAULTS["velocity"], min=0.0, max=0.99, update=update_view,
description="円の移動速度 (光速=1)"
)
radius: bpy.props.FloatProperty(
name="Radius (R)", default=CURRENT_DEFAULTS["radius"], min=0.1, update=update_view,
description="円の半径(剛体として計算)"
)
obs_time: bpy.props.FloatProperty(
name="Observation Time (t)", default=CURRENT_DEFAULTS["obs_time"], min=0.0, update=update_view,
description="観測者が「見た」時刻。この瞬間の実体と映像を計算します。"
)
show_rays: bpy.props.BoolProperty(name="Show Light Paths", default=CURRENT_DEFAULTS["show_rays"], update=update_view)
resolution: bpy.props.IntProperty(name="Resolution", default=CURRENT_DEFAULTS["resolution"], min=12, max=360, update=update_view)
ring_thick: bpy.props.FloatProperty(name="Ring Thick", default=CURRENT_DEFAULTS["ring_thick"], min=0.01, update=update_view)
ray_thick: bpy.props.FloatProperty(name="Ray Thick", default=CURRENT_DEFAULTS["ray_thick"], min=0.01, update=update_view)
obs_size: bpy.props.FloatProperty(name="Observer Size", default=CURRENT_DEFAULTS["obs_size"], min=0.01, update=update_view)
# ------------------------------------------------------------------------
# Operators
# ------------------------------------------------------------------------
class OBJECT_OT_DrawRelVisual(bpy.types.Operator):
bl_idname = "object.draw_rel_visual"
bl_label = "Force Refresh"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
draw_rel_visual_core(context)
return {'FINISHED'}
class OBJECT_OT_DetachVisual(bpy.types.Operator):
bl_idname = "object.detach_visual"
bl_label = "Detach & Keep"
bl_description = "Stop controlling the current visualization and keep it in the scene"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col = bpy.data.collections.get(COLLECTION_NAME)
if not col: return {'CANCELLED'}
targets =[obj for obj in col.objects if obj.get(OBJECT_TAG)]
if not targets:
self.report({'WARNING'}, "No active visualization to detach.")
return {'CANCELLED'}
scene_col = context.scene.collection
count = 0
timestamp = datetime.now().strftime('%H%M%S')
for obj in targets:
if OBJECT_TAG in obj: del obj[OBJECT_TAG]
orig_name = obj.name
obj.name = f"{orig_name}_Baked_{timestamp}"
# Independent Material Logic
if obj.data.materials:
old_mat = obj.data.materials[0]
new_mat = old_mat.copy()
new_mat.name = f"{old_mat.name}_Baked_{timestamp}"
obj.data.materials.clear()
obj.data.materials.append(new_mat)
if obj.name not in scene_col.objects:
scene_col.objects.link(obj)
col.objects.unlink(obj)
obj.select_set(True)
context.view_layer.objects.active = obj
count += 1
self.report({'INFO'}, f"Detached {count} object(s).")
return {'FINISHED'}
class WM_OT_ResetToDefaults(bpy.types.Operator):
bl_idname = "wm.reset_to_defaults"
bl_label = "Reset to Defaults"
bl_description = "Reset parameters to system defaults"
def execute(self, context):
p = context.scene.rel_visual
for key, val in SYSTEM_DEFAULTS.items():
setattr(p, key, val)
self.report({'INFO'}, "Parameters reset to defaults.")
return {'FINISHED'}
class WM_OT_RemoveAddon(bpy.types.Operator):
bl_idname = "wm.remove_addon"
bl_label = "Remove Addon"
def execute(self, context):
def cleanup_logic():
if __name__ == "__main__":
unregister()
print(f"[{bl_info['name']}] Unregistered from Script Mode.")
else:
import addon_utils
module_name = __package__ if __package__ else __name__
addon_utils.disable(module_name, default_set=True)
print(f"[{bl_info['name']}] Disabled from Addon Mode.")
for win in bpy.context.window_manager.windows:
for area in win.screen.areas:
area.tag_redraw()
bpy.app.timers.register(cleanup_logic, first_interval=0.1)
self.report({'INFO'}, "Removing Addon UI...")
return {'FINISHED'}
class WM_OT_CopyFullScript(bpy.types.Operator):
bl_idname = "wm.copy_full_script"
bl_label = "Copy Full Script"
def execute(self, context):
p = context.scene.rel_visual
M_START, M_END = "# <BEG" + "IN_DICT>", "# <EN" + "D_DICT>"
found_texts =[t for t in bpy.data.texts if UNIQUE_SCRIPT_ID in t.as_string()]
if not found_texts:
self.report({'ERROR'}, "Script source not found.")
return {'CANCELLED'}
target_text = found_texts[0]
code_str = target_text.as_string()
d_str = "CURRENT_DEFAULTS = {\n"
for k in CURRENT_DEFAULTS.keys():
val = getattr(p, k)
if isinstance(val, str): d_str += f' "{k}": "{val}",\n'
elif hasattr(val, "__len__") and not isinstance(val, str):
v_str = ", ".join([f"{v:.4f}" for v in val])
d_str += f' "{k}": ({v_str}),\n'
elif isinstance(val, float): d_str += f' "{k}": {val:.4f},\n'
else: d_str += f' "{k}": {val},\n'
d_str += "}\n"
try:
pre_dict = code_str.split(M_START)[0]; post_dict = code_str.split(M_END)[1]
new_code = pre_dict + M_START + "\n" + d_str + M_END + post_dict
context.window_manager.clipboard = f"# {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Session Template\n" + '\n'.join(new_code.split('\n')[1:])
self.report({'INFO'}, "Code copied with current values.")
except Exception as e:
self.report({'ERROR'}, f"Failed to parse: {e}")
return {'CANCELLED'}
return {'FINISHED'}
class WM_OT_OpenUrl(bpy.types.Operator):
bl_idname = "wm.open_url"
bl_label = "Open URL"
url: bpy.props.StringProperty()
def execute(self, context):
webbrowser.open(self.url)
return {'FINISHED'}
# ------------------------------------------------------------------------
# UI Panels
# ------------------------------------------------------------------------
class VIEW3D_PT_RelVisualPanel(bpy.types.Panel):
bl_label = "Relativity Visualizer"
bl_idname = "VIEW3D_PT_rel_visual"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
p = context.scene.rel_visual
row = layout.row()
row.operator("wm.copy_full_script", text="Copy Code with Values", icon='COPY_ID')
layout.separator()
box = layout.box()
box.label(text="Time Control (重要)", icon='TIME')
box.prop(p, "obs_time", text="Observation Time (t)")
box = layout.box()
box.label(text="Physics Parameters", icon='PHYSICS')
box.prop(p, "velocity")
box.prop(p, "radius")
box = layout.box()
box.label(text="Visual Settings", icon='MESH_GRID')
box.prop(p, "show_rays")
box.prop(p, "resolution")
box.prop(p, "ring_thick")
box.prop(p, "ray_thick")
box.prop(p, "obs_size")
layout.separator()
row = layout.row()
row.label(text="Green: Physical Reality (Now)", icon='FILE_3D')
row = layout.row()
row.label(text="Red: Visual Image (Past)", icon='IMAGE_RGB')
layout.separator()
layout.operator("wm.reset_to_defaults", text="Reset to Defaults", icon='LOOP_BACK')
row = layout.row()
row.scale_y = 1.5
row.operator("object.detach_visual", text="Detach & Keep", icon='PINNED')
layout.operator("object.draw_rel_visual", text="Force Refresh", icon='FILE_REFRESH')
class VIEW3D_PT_Links(bpy.types.Panel):
bl_label = "Theory Links"
bl_idname = "VIEW3D_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:
op = self.layout.operator("wm.open_url", text=l["label"])
op.url = l["url"]
class VIEW3D_PT_System(bpy.types.Panel):
bl_label = "System"
bl_idname = "VIEW3D_PT_system"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
self.layout.operator("wm.remove_addon", icon='CANCEL', text="Remove Addon")
# ------------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------------
classes = (
PG_RelativityVisual,
OBJECT_OT_DrawRelVisual,
OBJECT_OT_DetachVisual,
WM_OT_ResetToDefaults,
WM_OT_RemoveAddon,
WM_OT_CopyFullScript,
WM_OT_OpenUrl,
VIEW3D_PT_RelVisualPanel,
VIEW3D_PT_Links,
VIEW3D_PT_System,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
if not hasattr(bpy.types.Scene, "rel_visual"):
bpy.types.Scene.rel_visual = bpy.props.PointerProperty(type=PG_RelativityVisual)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if hasattr(bpy.types.Scene, "rel_visual"):
del bpy.types.Scene.rel_visual
if __name__ == "__main__":
register()