blender Million 2026
# Copied: 12:15:00
import bpy
import bmesh
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime
# ==============================================================================
# 設定エリア
# ==============================================================================
PREFIX = "AngledCyl_3Sets20260313"
TAB_NAME = "[ 45° 3-Sets ]"
# ### ZIONAD_SOURCE_ID: ANGLED_CYL_3SETS_20260313 ###
bl_info = {
"name": "45-Degree Cylinder & Cone Generator (3 Sets)",
"author": "zionadchat",
"version": (2, 5, 2),
"blender": (5, 0, 0), # Blender 5.0以上 専用
"location": "3D View > Sidebar",
"description": "Generate 3 sets of 45-degree light cylinders and arbitrary velocity observer cylinders.",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: ANGLED_CYL_3SETS_20260313 ###"
ADDON_LINKS = (
{"label": "z時間軸 45度 20260313", "url": "<https://www.notion.so/20230313-322f5dacaf43806b891efa5002e663e0>"},
{"label": "Blender Python API", "url": "<https://docs.blender.org/api/current/index.html>"},
)
# ==============================================================================
# デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"show_preview": True,
"active_tab": 'SET3',
"obs_velocity": -0.5000,
"time_meet": 10.0000,
"light_start_x": 10.0000,
"light_start_y": 0.0000,
"light_speed": 1.0000,
"set1_show": True,
"set1_pt1_xy": (0.0000, 0.0000),
"set1_pt2_xy": (5.0000, 0.0000),
"set1_pt3_xy": (10.0000, 0.0000),
"set1_base_z": 0.0000,
"set1_show_light": True,
"set1_radius": 0.5000,
"set1_cyl_color": (1.0000, 0.7602, 0.0196, 1.0000),
"set1_show_obs": True,
"set1_obs_velocity": 0.5000,
"set1_obs_radius": 0.5000,
"set1_obs_color": (0.0008, 0.0000, 0.6010, 1.0000),
"set1_show_cones": True,
"set1_cone_radius": 10.0000,
"set1_cone_height": 10.0000,
"set1_cone_color": (0.2000, 0.6000, 1.0000, 0.0778),
"set2_show": False,
"set2_pt1_xy": (10.0000, 0.0000),
"set2_pt2_xy": (10.0000, 20.0000),
"set2_pt3_xy": (10.0000, 40.0000),
"set2_base_z": -10.0000,
"set2_show_light": True,
"set2_radius": 0.5000,
"set2_cyl_color": (0.2000, 1.0000, 0.4000, 1.0000),
"set2_show_obs": True,
"set2_obs_velocity": 0.5000,
"set2_obs_radius": 0.5000,
"set2_obs_color": (1.0000, 0.8000, 0.1000, 1.0000),
"set2_show_cones": True,
"set2_cone_radius": 20.0000,
"set2_cone_height": 20.0000,
"set2_cone_color": (0.2000, 1.0000, 0.4000, 0.3000),
"set3_show": True,
"set3_pt1_xy": (10.0000, 0.0000),
"set3_pt2_xy": (5.0000, 0.0000),
"set3_pt3_xy": (0.0000, 0.0000),
"set3_base_z": 0.0000,
"set3_show_light": True,
"set3_radius": 0.5000,
"set3_cyl_color": (1.0000, 0.9208, 0.0092, 1.0000),
"set3_show_obs": True,
"set3_obs_velocity": 0.5000,
"set3_obs_radius": 0.5000,
"set3_obs_color": (1.0000, 0.0179, 0.1817, 1.0000),
"set3_show_cones": True,
"set3_cone_radius": 10.0000,
"set3_cone_height": 10.0000,
"set3_cone_color": (1.0000, 0.1707, 0.6135, 0.0383),
}
# <END_DICT>
# ==============================================================================
# マテリアル作成ロジック (Blender 5.0対応版)
# ==============================================================================
def get_or_create_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
tree = mat.node_tree
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (-200, 0)
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (100, 0)
tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
mat.diffuse_color = color
return mat
# ==============================================================================
# ジオメトリ構築ロジック
# ==============================================================================
def create_single_cylinder(p1, p2, radius, collection, name, mat):
length = (p2 - p1).length
if length < 0.0001: return None
mid_point = (p1 + p2) / 2.0
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=radius, radius2=radius, depth=length)
direction = (p2 - p1).normalized()
rot = Vector((0, 0, 1)).rotation_difference(direction)
bmesh.ops.rotate(bm, cent=(0,0,0), matrix=rot.to_matrix(), verts=bm.verts)
bmesh.ops.translate(bm, vec=mid_point, verts=bm.verts)
mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
obj.data.materials.append(mat)
return obj
def create_inverted_cone(location, radius, height, collection, name, mat):
if height < 0.001 or radius < 0.001: return None
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=False, cap_tris=True, segments=32, radius1=0.0, radius2=radius, depth=height)
bmesh.ops.translate(bm, vec=(0, 0, height / 2.0), verts=bm.verts)
bmesh.ops.translate(bm, vec=location, verts=bm.verts)
mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
obj.data.materials.append(mat)
return obj
def build_all_sets(props, collection, is_preview=False):
objs =[]
for i in range(1, 4):
prefix = f"set{i}"
if not getattr(props, f"{prefix}_show"): continue
x1, y1 = getattr(props, f"{prefix}_pt1_xy")
x2, y2 = getattr(props, f"{prefix}_pt2_xy")
x3, y3 = getattr(props, f"{prefix}_pt3_xy")
base_z = getattr(props, f"{prefix}_base_z")
# 光円錐に基づく時間(Z)の計算
dxy1 = math.hypot(x2 - x1, y2 - y1)
z2 = base_z + dxy1
dxy2 = math.hypot(x3 - x2, y3 - y2)
z3 = z2 + dxy2
p1 = Vector((x1, y1, base_z))
p2 = Vector((x2, y2, z2))
p3 = Vector((x3, y3, z3))
# 1. 光の円柱 (Light Cylinder: v=1.0c)
if getattr(props, f"{prefix}_show_light"):
cyl_rad = getattr(props, f"{prefix}_radius")
cyl_col = getattr(props, f"{prefix}_cyl_color")
cyl_mat_name = f"Mat_Prev_Light_{i}" if is_preview else f"Mat_Light_{i}_{datetime.now().strftime('%H%M%S')}"
cyl_mat = get_or_create_material(cyl_mat_name, cyl_col)
name1 = f"Prev_Set{i}_Light1" if is_preview else f"Set{i}_Light1_{datetime.now().strftime('%H%M%S')}"
name2 = f"Prev_Set{i}_Light2" if is_preview else f"Set{i}_Light2_{datetime.now().strftime('%H%M%S')}"
c1 = create_single_cylinder(p1, p2, cyl_rad, collection, name1, cyl_mat)
if c1: objs.append(c1)
c2 = create_single_cylinder(p2, p3, cyl_rad, collection, name2, cyl_mat)
if c2: objs.append(c2)
# 2. 観測者の円柱 (Observer Cylinder: Velocity v)
if getattr(props, f"{prefix}_show_obs"):
v = getattr(props, f"{prefix}_obs_velocity")
obs_rad = getattr(props, f"{prefix}_obs_radius")
obs_col = getattr(props, f"{prefix}_obs_color")
obs_mat_name = f"Mat_Prev_Obs_{i}" if is_preview else f"Mat_Obs_{i}_{datetime.now().strftime('%H%M%S')}"
obs_mat = get_or_create_material(obs_mat_name, obs_col)
p1_obs = Vector((x1, y1, base_z))
# 同じ時間(Z)で、指定速度(v)分だけXY平面を進む
p2_x_obs = x1 + (x2 - x1) * v
p2_y_obs = y1 + (y2 - y1) * v
p2_obs = Vector((p2_x_obs, p2_y_obs, z2))
p3_x_obs = p2_x_obs + (x3 - x2) * v
p3_y_obs = p2_y_obs + (y3 - y2) * v
p3_obs = Vector((p3_x_obs, p3_y_obs, z3))
name1_obs = f"Prev_Set{i}_Obs1" if is_preview else f"Set{i}_Obs1_{datetime.now().strftime('%H%M%S')}"
name2_obs = f"Prev_Set{i}_Obs2" if is_preview else f"Set{i}_Obs2_{datetime.now().strftime('%H%M%S')}"
c1_obs = create_single_cylinder(p1_obs, p2_obs, obs_rad, collection, name1_obs, obs_mat)
if c1_obs: objs.append(c1_obs)
c2_obs = create_single_cylinder(p2_obs, p3_obs, obs_rad, collection, name2_obs, obs_mat)
if c2_obs: objs.append(c2_obs)
# 3. 逆さ円錐 (Height Cones)
if getattr(props, f"{prefix}_show_cones"):
cone_rad = getattr(props, f"{prefix}_cone_radius")
cone_h = getattr(props, f"{prefix}_cone_height")
cone_col = getattr(props, f"{prefix}_cone_color")
cone_mat_name = f"Mat_Prev_Cone_{i}" if is_preview else f"Mat_Cone_{i}_{datetime.now().strftime('%H%M%S')}"
cone_mat = get_or_create_material(cone_mat_name, cone_col)
for idx, p in enumerate([p1, p2, p3]):
c_name = f"Prev_Set{i}_Cone{idx+1}" if is_preview else f"Set{i}_Cone{idx+1}_{datetime.now().strftime('%H%M%S')}"
cone_obj = create_inverted_cone(p, cone_rad, cone_h, collection, c_name, cone_mat)
if cone_obj: objs.append(cone_obj)
return objs
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
def clear_preview(context):
col = bpy.data.collections.get(PREVIEW_COL_NAME)
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("Prev_Set") and m.users == 0:
bpy.data.meshes.remove(m)
def update_preview(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
clear_preview(context)
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)
objs = build_all_sets(props, col, is_preview=True)
for obj in objs: obj.display_type = 'TEXTURED'
context.view_layer.update()
_timer = None
def delayed_update():
global _timer
_timer = None
if bpy.context and bpy.context.scene: update_preview(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_CylinderProps(PropertyGroup):
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
active_tab: EnumProperty(
items=[('SET1', "Set 1", ""), ('SET2', "Set 2", ""), ('SET3', "Set 3", "")],
default=CURRENT_DEFAULTS['active_tab']
)
# --- Calc Panel Props ---
obs_velocity: FloatProperty(name="Observer Velocity (v)", default=CURRENT_DEFAULTS['obs_velocity'], update=on_update)
time_meet: FloatProperty(name="Meeting Time (t_meet)", default=CURRENT_DEFAULTS['time_meet'], update=on_update)
light_start_x: FloatProperty(name="Light Start X", default=CURRENT_DEFAULTS['light_start_x'], update=on_update)
light_start_y: FloatProperty(name="Light Start Y", default=CURRENT_DEFAULTS['light_start_y'], update=on_update)
light_speed: FloatProperty(name="Speed of Light (c)", default=CURRENT_DEFAULTS['light_speed'], update=on_update)
# --- SET 1 ---
set1_show: BoolProperty(default=CURRENT_DEFAULTS['set1_show'], update=on_update)
set1_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt1_xy'], update=on_update)
set1_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt2_xy'], update=on_update)
set1_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt3_xy'], update=on_update)
set1_base_z: FloatProperty(default=CURRENT_DEFAULTS['set1_base_z'], update=on_update)
set1_show_light: BoolProperty(default=CURRENT_DEFAULTS['set1_show_light'], update=on_update)
set1_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_radius'], min=0.01, update=on_update)
set1_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cyl_color'], update=on_update)
set1_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set1_show_obs'], update=on_update)
set1_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set1_obs_velocity'], update=on_update)
set1_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_obs_radius'], min=0.01, update=on_update)
set1_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_obs_color'], update=on_update)
set1_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set1_show_cones'], update=on_update)
set1_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_radius'], min=0.01, update=on_update)
set1_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_height'], min=0.01, update=on_update)
set1_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cone_color'], update=on_update)
# --- SET 2 ---
set2_show: BoolProperty(default=CURRENT_DEFAULTS['set2_show'], update=on_update)
set2_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt1_xy'], update=on_update)
set2_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt2_xy'], update=on_update)
set2_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt3_xy'], update=on_update)
set2_base_z: FloatProperty(default=CURRENT_DEFAULTS['set2_base_z'], update=on_update)
set2_show_light: BoolProperty(default=CURRENT_DEFAULTS['set2_show_light'], update=on_update)
set2_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_radius'], min=0.01, update=on_update)
set2_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cyl_color'], update=on_update)
set2_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set2_show_obs'], update=on_update)
set2_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set2_obs_velocity'], update=on_update)
set2_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_obs_radius'], min=0.01, update=on_update)
set2_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_obs_color'], update=on_update)
set2_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set2_show_cones'], update=on_update)
set2_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_radius'], min=0.01, update=on_update)
set2_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_height'], min=0.01, update=on_update)
set2_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cone_color'], update=on_update)
# --- SET 3 ---
set3_show: BoolProperty(default=CURRENT_DEFAULTS['set3_show'], update=on_update)
set3_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt1_xy'], update=on_update)
set3_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt2_xy'], update=on_update)
set3_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt3_xy'], update=on_update)
set3_base_z: FloatProperty(default=CURRENT_DEFAULTS['set3_base_z'], update=on_update)
set3_show_light: BoolProperty(default=CURRENT_DEFAULTS['set3_show_light'], update=on_update)
set3_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_radius'], min=0.01, update=on_update)
set3_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cyl_color'], update=on_update)
set3_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set3_show_obs'], update=on_update)
set3_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set3_obs_velocity'], update=on_update)
set3_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_obs_radius'], min=0.01, update=on_update)
set3_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_obs_color'], update=on_update)
set3_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set3_show_cones'], update=on_update)
set3_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_radius'], min=0.01, update=on_update)
set3_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_height'], min=0.01, update=on_update)
set3_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cone_color'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script with Current Values"
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 in Text Editor.")
return {'CANCELLED'}
code = target_text.as_string()
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "active_tab": \'{props.active_tab}\',\n'
new_dict += f' "obs_velocity": {props.obs_velocity:.4f},\n'
new_dict += f' "time_meet": {props.time_meet:.4f},\n'
new_dict += f' "light_start_x": {props.light_start_x:.4f},\n'
new_dict += f' "light_start_y": {props.light_start_y:.4f},\n'
new_dict += f' "light_speed": {props.light_speed:.4f},\n'
for i in range(1, 4):
prefix = f"set{i}"
show = getattr(props, f"{prefix}_show")
p1 = getattr(props, f"{prefix}_pt1_xy")
p2 = getattr(props, f"{prefix}_pt2_xy")
p3 = getattr(props, f"{prefix}_pt3_xy")
bz = getattr(props, f"{prefix}_base_z")
sl = getattr(props, f"{prefix}_show_light")
rad = getattr(props, f"{prefix}_radius")
ccol = getattr(props, f"{prefix}_cyl_color")
so = getattr(props, f"{prefix}_show_obs")
ov = getattr(props, f"{prefix}_obs_velocity")
orad = getattr(props, f"{prefix}_obs_radius")
ocol = getattr(props, f"{prefix}_obs_color")
scone = getattr(props, f"{prefix}_show_cones")
crad = getattr(props, f"{prefix}_cone_radius")
chgt = getattr(props, f"{prefix}_cone_height")
cocol = getattr(props, f"{prefix}_cone_color")
new_dict += f' "{prefix}_show": {show},\n'
new_dict += f' "{prefix}_pt1_xy": ({p1[0]:.4f}, {p1[1]:.4f}),\n'
new_dict += f' "{prefix}_pt2_xy": ({p2[0]:.4f}, {p2[1]:.4f}),\n'
new_dict += f' "{prefix}_pt3_xy": ({p3[0]:.4f}, {p3[1]:.4f}),\n'
new_dict += f' "{prefix}_base_z": {bz:.4f},\n'
new_dict += f' "{prefix}_show_light": {sl},\n'
new_dict += f' "{prefix}_radius": {rad:.4f},\n'
new_dict += f' "{prefix}_cyl_color": ({ccol[0]:.4f}, {ccol[1]:.4f}, {ccol[2]:.4f}, {ccol[3]:.4f}),\n'
new_dict += f' "{prefix}_show_obs": {so},\n'
new_dict += f' "{prefix}_obs_velocity": {ov:.4f},\n'
new_dict += f' "{prefix}_obs_radius": {orad:.4f},\n'
new_dict += f' "{prefix}_obs_color": ({ocol[0]:.4f}, {ocol[1]:.4f}, {ocol[2]:.4f}, {ocol[3]:.4f}),\n'
new_dict += f' "{prefix}_show_cones": {scone},\n'
new_dict += f' "{prefix}_cone_radius": {crad:.4f},\n'
new_dict += f' "{prefix}_cone_height": {chgt:.4f},\n'
new_dict += f' "{prefix}_cone_color": ({cocol[0]:.4f}, {cocol[1]:.4f}, {cocol[2]:.4f}, {cocol[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 with current values!")
except Exception as e:
self.report({'ERROR'}, f"Copy Failed: {e}")
return {'CANCELLED'}
return {'FINISHED'}
class OT_CopyCalcResult(Operator):
bl_idname = f"{OP_PREFIX}.copy_calc_result"
bl_label = "Copy Calculation Result"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
v = props.obs_velocity
t_m = props.time_meet
x_start = props.light_start_x
y_start = props.light_start_y
c = props.light_speed
x_meet = v * t_m
y_meet = 0.0
dx = x_start - x_meet
dy = y_start - y_meet
dist = math.sqrt(dx**2 + dy**2)
t_travel = dist / c if c != 0 else 0
t_emit = t_m - t_travel
text = (
f"Observer Velocity (v): {v:.3f}\n"
f"Meeting Time (t_meet): {t_m:.3f}\n"
f"Light Start Position: X = {x_start:.3f}, Y = {y_start:.3f}\n"
f"Speed of Light (c): {c:.3f}\n"
f"----------------------------------------\n"
f"[ Calculation Formula & Results ]\n"
f"1. Meeting Point (Observer Pos): ({x_meet:.3f}, {y_meet:.3f})\n"
f" X_meet = {v:.3f} * {t_m:.3f} = {x_meet:.3f}\n\n"
f"2. Distance (Light Travel Path):\n"
f" Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - {y_meet:.3f})²)\n"
f" = √({dx:.3f}² + {dy:.3f}²) = {dist:.3f}\n\n"
f"3. Travel Time (t_travel):\n"
f" t_travel = Dist / c = {dist:.3f} / {c:.3f} = {t_travel:.3f}\n\n"
f"4. Light Emit Time (t):\n"
f" t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}\n"
)
context.window_manager.clipboard = text
self.report({'INFO'}, "Calculation results & formulas copied to clipboard!")
return {'FINISHED'}
class OT_CopyIntersectionCalcResult(Operator):
bl_idname = f"{OP_PREFIX}.copy_intersection_calc"
bl_label = "Copy Intersection Result"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
v = props.obs_velocity
x_L = props.light_start_x
y_L = props.light_start_y
c = props.light_speed
A = c**2 - v**2
B = 2 * v * x_L
C = -(x_L**2 + y_L**2)
text = (
f"Observer Velocity (v): {v:.3f}\n"
f"Light Start Position: X = {x_L:.3f}, Y = {y_L:.3f}\n"
f"Speed of Light (c): {c:.3f}\n"
f"Observer and Light start at the same time (t=0).\n"
f"----------------------------------------\n"
f"[ Calculation Formula ]\n"
f"Distance equation: (v*t - X)² + (0 - Y)² = (c*t)²\n"
f"Quadratic form: (c² - v²)t² + (2*v*X)t - (X² + Y²) = 0\n"
f"A = c² - v² = {A:.3f}\n"
f"B = 2*v*X = {B:.3f}\n"
f"C = -(X² + Y²) = {C:.3f}\n\n"
)
t_meet = -1
if abs(A) < 1e-6:
if abs(B) > 1e-6:
t_meet = -C / B
text += f"A ≈ 0, linear equation: B*t + C = 0 => t = {-C:.3f} / {B:.3f} = {t_meet:.3f}\n"
else:
text += f"A ≈ 0 and B ≈ 0, no solution.\n"
else:
D = B**2 - 4*A*C
text += f"D = B² - 4*A*C = {D:.3f}\n"
if D < 0:
text += "D < 0, no real solution (they will never meet).\n"
else:
t1 = (-B + math.sqrt(D)) / (2*A)
t2 = (-B - math.sqrt(D)) / (2*A)
text += f"Roots: t1 = {t1:.3f}, t2 = {t2:.3f}\n"
valid_ts = [t for t in (t1, t2) if t > 0]
if valid_ts:
t_meet = min(valid_ts)
text += f"Valid positive time (t_meet): {t_meet:.3f}\n"
else:
text += "No positive time solution (they met in the past).\n"
if t_meet > 0:
x_meet = v * t_meet
text += f"\n[ Results ]\n"
text += f"Meeting Time (t): {t_meet:.3f}\n"
text += f"Meeting Position: ({x_meet:.3f}, 0.000)\n"
else:
text += f"\n[ Results ]\n"
text += "No valid intersection.\n"
context.window_manager.clipboard = text
self.report({'INFO'}, "Intersection calculation copied to clipboard!")
return {'FINISHED'}
class OT_CreateSets(Operator):
bl_idname = f"{OP_PREFIX}.create_sets"
bl_label = "Create Displayed Sets"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
col = context.collection if context.collection else context.scene.collection
objs = build_all_sets(props, col, is_preview=False)
if objs:
bpy.ops.object.select_all(action='DESELECT')
for obj in objs: obj.select_set(True)
context.view_layer.objects.active = objs[0]
self.report({'INFO'}, f"Generated {len(objs)} Objects successfully!")
else:
self.report({'WARNING'}, "Nothing generated. Check settings.")
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)
self.report({'INFO'}, "Addon Removed successfully.")
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_CalcPanel(Panel):
bl_label = "Light vs Observer Calc"
bl_idname = f"{PREFIX}_PT_calc"
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: return
layout.prop(props, "obs_velocity")
layout.prop(props, "time_meet")
# ▼ 光線の出発位置を2行に分割 ▼
layout.prop(props, "light_start_x")
layout.prop(props, "light_start_y")
layout.prop(props, "light_speed")
# Calculation logic
v = props.obs_velocity
t_m = props.time_meet
x_start = props.light_start_x
y_start = props.light_start_y
c = props.light_speed
x_meet = v * t_m
y_meet = 0.0
dx = x_start - x_meet
dy = y_start - y_meet
dist = math.sqrt(dx**2 + dy**2)
t_travel = dist / c if c != 0 else 0
t_emit = t_m - t_travel
box = layout.box()
box.label(text="[ Formula & Results ]", icon='INFO')
box.label(text=f"Meeting Point: ({x_meet:.3f}, {y_meet:.3f})")
box.label(text=f"Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - 0)²) = {dist:.3f}")
box.label(text=f"t_travel = {dist:.3f} / {c:.3f} = {t_travel:.3f}")
box.label(text=f"t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}")
layout.separator()
layout.operator(OT_CopyCalcResult.bl_idname, icon='COPY_ID', text="Copy Formula & Results")
# ▼ 新規追加: 同時出発(t=0)計算用パネル ▼
class PT_IntersectionCalcPanel(Panel):
bl_label = "Simultaneous Start (t=0) Calc"
bl_idname = f"{PREFIX}_PT_intersection"
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: return
v = props.obs_velocity
x_L = props.light_start_x
y_L = props.light_start_y
c = props.light_speed
A = c**2 - v**2
B = 2 * v * x_L
C = -(x_L**2 + y_L**2)
box = layout.box()
box.label(text="[ Formula ]", icon='INFO')
box.label(text="(c² - v²)t² + (2vX)t - (X² + Y²) = 0")
box.label(text=f"A={A:.3f}, B={B:.3f}, C={C:.3f}")
t_meet = -1
if abs(A) < 1e-6:
if abs(B) > 1e-6:
t_meet = -C / B
else:
D = B**2 - 4*A*C
if D >= 0:
t1 = (-B + math.sqrt(D)) / (2*A)
t2 = (-B - math.sqrt(D)) / (2*A)
valid_ts = [t for t in (t1, t2) if t > 0]
if valid_ts:
t_meet = min(valid_ts)
res_box = layout.box()
res_box.label(text="[ Results ]", icon='LIGHT')
if t_meet > 0:
x_meet = v * t_meet
res_box.label(text=f"Meet Time (t): {t_meet:.3f}")
res_box.label(text=f"Meet Pos: ({x_meet:.3f}, 0.000)")
else:
res_box.label(text="No valid intersection.")
layout.separator()
layout.operator(OT_CopyIntersectionCalcResult.bl_idname, icon='COPY_ID', text="Copy Formula & Results")
def draw_set_ui(layout, props, prefix):
box = layout.box()
if not getattr(props, f"{prefix}_show"):
box.label(text="⚠️ This Set is Currently Hidden", icon='INFO')
p_box = box.box()
p_box.label(text="XY Plane Points", icon='MESH_PLANE')
p_box.prop(props, f"{prefix}_pt1_xy", text="Point 1 (Start)")
p_box.prop(props, f"{prefix}_pt2_xy", text="Point 2 (Mid)")
p_box.prop(props, f"{prefix}_pt3_xy", text="Point 3 (End)")
p_box.prop(props, f"{prefix}_base_z", text="Base Z (Start Height)")
c_box = box.box()
c_row = c_box.row()
c_row.prop(props, f"{prefix}_show_light", text="45° Light Cylinder (v=1.0c)", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_light") else 'HIDE_ON', toggle=True)
if getattr(props, f"{prefix}_show_light"):
c_box.prop(props, f"{prefix}_radius", text="Thickness Radius")
c_box.prop(props, f"{prefix}_cyl_color", text="Color")
o_box = box.box()
o_row = o_box.row()
o_row.prop(props, f"{prefix}_show_obs", text="Observer Cylinder (Velocity v)", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_obs") else 'HIDE_ON', toggle=True)
if getattr(props, f"{prefix}_show_obs"):
o_box.prop(props, f"{prefix}_obs_velocity", text="Velocity (v, % of c)")
o_box.prop(props, f"{prefix}_obs_radius", text="Thickness Radius")
o_box.prop(props, f"{prefix}_obs_color", text="Color")
co_box = box.box()
co_row = co_box.row()
co_row.prop(props, f"{prefix}_show_cones", text="Height Cones at P1, P2, P3", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_cones") else 'HIDE_ON', toggle=True)
if getattr(props, f"{prefix}_show_cones"):
co_box.prop(props, f"{prefix}_cone_radius", text="Cone Base Radius")
co_box.prop(props, f"{prefix}_cone_height", text="Cone Height")
co_box.prop(props, f"{prefix}_cone_color", text="Cone Color (Alpha)")
class PT_MainPanel(Panel):
bl_label = "45-Deg Cyl & Cone (3 Sets)"
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="Please Reload Script"); return
# --- Copy Code Button ---
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Current Values")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
layout.separator()
vis_box = layout.box()
vis_row = vis_box.row(align=True)
vis_row.prop(props, "set1_show", text="Set 1", icon='HIDE_OFF' if props.set1_show else 'HIDE_ON', toggle=True)
vis_row.prop(props, "set2_show", text="Set 2", icon='HIDE_OFF' if props.set2_show else 'HIDE_ON', toggle=True)
vis_row.prop(props, "set3_show", text="Set 3", icon='HIDE_OFF' if props.set3_show else 'HIDE_ON', toggle=True)
layout.separator()
row = layout.row(align=True)
row.prop(props, "active_tab", expand=True)
if props.active_tab == 'SET1': draw_set_ui(layout, props, "set1")
elif props.active_tab == 'SET2': draw_set_ui(layout, props, "set2")
elif props.active_tab == 'SET3': draw_set_ui(layout, props, "set3")
layout.separator()
col = layout.column()
col.scale_y = 1.5
col.operator(OT_CreateSets.bl_idname, icon='MOD_BUILD', text="Create Mesh Object(s)")
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_CylinderProps,
OT_CopyFullScript,
OT_CopyCalcResult,
OT_CopyIntersectionCalcResult, # 追加
OT_CreateSets,
OT_OpenUrl,
OT_RemoveAddon,
PT_MainPanel,
PT_CalcPanel,
PT_IntersectionCalcPanel, # 追加
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(bpy.context)
return None
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_CylinderProps))
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: 12:01:14
# Copied: 11:45:32
# Copied: 04:00:00
import bpy
import bmesh
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime
# ==============================================================================
# 設定エリア
# ==============================================================================
PREFIX = "AngledCyl_3Sets20260313"
TAB_NAME = "[ 45° 3-Sets ]"
# ### ZIONAD_SOURCE_ID: ANGLED_CYL_3SETS_20260313 ###
bl_info = {
"name": "45-Degree Cylinder & Cone Generator (3 Sets)",
"author": "zionadchat",
"version": (2, 5, 0),
"blender": (5, 0, 0), # Blender 5.0以上 専用
"location": "3D View > Sidebar",
"description": "Generate 3 sets of 45-degree light cylinders and arbitrary velocity observer cylinders.",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: ANGLED_CYL_3SETS_20260313 ###"
ADDON_LINKS = (
{"label": "z時間軸 45度 20260313", "url": "<https://www.notion.so/20230313-322f5dacaf43806b891efa5002e663e0>"},
{"label": "Blender Python API", "url": "<https://docs.blender.org/api/current/index.html>"},
)
# ==============================================================================
# デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"show_preview": True,
"active_tab": 'SET3',
"obs_velocity": -0.5000,
"time_meet": 10.0000,
"light_start_x": 10.0000,
"light_start_y": 0.0000,
"light_speed": 1.0000,
"set1_show": True,
"set1_pt1_xy": (0.0000, 0.0000),
"set1_pt2_xy": (5.0000, 0.0000),
"set1_pt3_xy": (10.0000, 0.0000),
"set1_base_z": 0.0000,
"set1_show_light": True,
"set1_radius": 0.5000,
"set1_cyl_color": (1.0000, 0.7602, 0.0196, 1.0000),
"set1_show_obs": True,
"set1_obs_velocity": 0.5000,
"set1_obs_radius": 0.5000,
"set1_obs_color": (0.0008, 0.0000, 0.6010, 1.0000),
"set1_show_cones": True,
"set1_cone_radius": 10.0000,
"set1_cone_height": 10.0000,
"set1_cone_color": (0.2000, 0.6000, 1.0000, 0.0778),
"set2_show": False,
"set2_pt1_xy": (10.0000, 0.0000),
"set2_pt2_xy": (10.0000, 20.0000),
"set2_pt3_xy": (10.0000, 40.0000),
"set2_base_z": -10.0000,
"set2_show_light": True,
"set2_radius": 0.5000,
"set2_cyl_color": (0.2000, 1.0000, 0.4000, 1.0000),
"set2_show_obs": True,
"set2_obs_velocity": 0.5000,
"set2_obs_radius": 0.5000,
"set2_obs_color": (1.0000, 0.8000, 0.1000, 1.0000),
"set2_show_cones": True,
"set2_cone_radius": 20.0000,
"set2_cone_height": 20.0000,
"set2_cone_color": (0.2000, 1.0000, 0.4000, 0.3000),
"set3_show": True,
"set3_pt1_xy": (10.0000, 0.0000),
"set3_pt2_xy": (5.0000, 0.0000),
"set3_pt3_xy": (0.0000, 0.0000),
"set3_base_z": 0.0000,
"set3_show_light": True,
"set3_radius": 0.5000,
"set3_cyl_color": (1.0000, 0.9208, 0.0092, 1.0000),
"set3_show_obs": True,
"set3_obs_velocity": 0.5000,
"set3_obs_radius": 0.5000,
"set3_obs_color": (1.0000, 0.0179, 0.1817, 1.0000),
"set3_show_cones": True,
"set3_cone_radius": 10.0000,
"set3_cone_height": 10.0000,
"set3_cone_color": (1.0000, 0.1707, 0.6135, 0.0383),
}
# <END_DICT>
# ==============================================================================
# マテリアル作成ロジック (Blender 5.0対応版)
# ==============================================================================
def get_or_create_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
tree = mat.node_tree
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (-200, 0)
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (100, 0)
tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
mat.diffuse_color = color
return mat
# ==============================================================================
# ジオメトリ構築ロジック
# ==============================================================================
def create_single_cylinder(p1, p2, radius, collection, name, mat):
length = (p2 - p1).length
if length < 0.0001: return None
mid_point = (p1 + p2) / 2.0
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=radius, radius2=radius, depth=length)
direction = (p2 - p1).normalized()
rot = Vector((0, 0, 1)).rotation_difference(direction)
bmesh.ops.rotate(bm, cent=(0,0,0), matrix=rot.to_matrix(), verts=bm.verts)
bmesh.ops.translate(bm, vec=mid_point, verts=bm.verts)
mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
obj.data.materials.append(mat)
return obj
def create_inverted_cone(location, radius, height, collection, name, mat):
if height < 0.001 or radius < 0.001: return None
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=0.0, radius2=radius, depth=height)
bmesh.ops.translate(bm, vec=(0, 0, height / 2.0), verts=bm.verts)
bmesh.ops.translate(bm, vec=location, verts=bm.verts)
mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
obj.data.materials.append(mat)
return obj
def build_all_sets(props, collection, is_preview=False):
objs =[]
for i in range(1, 4):
prefix = f"set{i}"
if not getattr(props, f"{prefix}_show"): continue
x1, y1 = getattr(props, f"{prefix}_pt1_xy")
x2, y2 = getattr(props, f"{prefix}_pt2_xy")
x3, y3 = getattr(props, f"{prefix}_pt3_xy")
base_z = getattr(props, f"{prefix}_base_z")
# 光円錐に基づく時間(Z)の計算
dxy1 = math.hypot(x2 - x1, y2 - y1)
z2 = base_z + dxy1
dxy2 = math.hypot(x3 - x2, y3 - y2)
z3 = z2 + dxy2
p1 = Vector((x1, y1, base_z))
p2 = Vector((x2, y2, z2))
p3 = Vector((x3, y3, z3))
# 1. 光の円柱 (Light Cylinder: v=1.0c)
if getattr(props, f"{prefix}_show_light"):
cyl_rad = getattr(props, f"{prefix}_radius")
cyl_col = getattr(props, f"{prefix}_cyl_color")
cyl_mat_name = f"Mat_Prev_Light_{i}" if is_preview else f"Mat_Light_{i}_{datetime.now().strftime('%H%M%S')}"
cyl_mat = get_or_create_material(cyl_mat_name, cyl_col)
name1 = f"Prev_Set{i}_Light1" if is_preview else f"Set{i}_Light1_{datetime.now().strftime('%H%M%S')}"
name2 = f"Prev_Set{i}_Light2" if is_preview else f"Set{i}_Light2_{datetime.now().strftime('%H%M%S')}"
c1 = create_single_cylinder(p1, p2, cyl_rad, collection, name1, cyl_mat)
if c1: objs.append(c1)
c2 = create_single_cylinder(p2, p3, cyl_rad, collection, name2, cyl_mat)
if c2: objs.append(c2)
# 2. 観測者の円柱 (Observer Cylinder: Velocity v)
if getattr(props, f"{prefix}_show_obs"):
v = getattr(props, f"{prefix}_obs_velocity")
obs_rad = getattr(props, f"{prefix}_obs_radius")
obs_col = getattr(props, f"{prefix}_obs_color")
obs_mat_name = f"Mat_Prev_Obs_{i}" if is_preview else f"Mat_Obs_{i}_{datetime.now().strftime('%H%M%S')}"
obs_mat = get_or_create_material(obs_mat_name, obs_col)
p1_obs = Vector((x1, y1, base_z))
# 同じ時間(Z)で、指定速度(v)分だけXY平面を進む
p2_x_obs = x1 + (x2 - x1) * v
p2_y_obs = y1 + (y2 - y1) * v
p2_obs = Vector((p2_x_obs, p2_y_obs, z2))
p3_x_obs = p2_x_obs + (x3 - x2) * v
p3_y_obs = p2_y_obs + (y3 - y2) * v
p3_obs = Vector((p3_x_obs, p3_y_obs, z3))
name1_obs = f"Prev_Set{i}_Obs1" if is_preview else f"Set{i}_Obs1_{datetime.now().strftime('%H%M%S')}"
name2_obs = f"Prev_Set{i}_Obs2" if is_preview else f"Set{i}_Obs2_{datetime.now().strftime('%H%M%S')}"
c1_obs = create_single_cylinder(p1_obs, p2_obs, obs_rad, collection, name1_obs, obs_mat)
if c1_obs: objs.append(c1_obs)
c2_obs = create_single_cylinder(p2_obs, p3_obs, obs_rad, collection, name2_obs, obs_mat)
if c2_obs: objs.append(c2_obs)
# 3. 逆さ円錐 (Height Cones)
if getattr(props, f"{prefix}_show_cones"):
cone_rad = getattr(props, f"{prefix}_cone_radius")
cone_h = getattr(props, f"{prefix}_cone_height")
cone_col = getattr(props, f"{prefix}_cone_color")
cone_mat_name = f"Mat_Prev_Cone_{i}" if is_preview else f"Mat_Cone_{i}_{datetime.now().strftime('%H%M%S')}"
cone_mat = get_or_create_material(cone_mat_name, cone_col)
for idx, p in enumerate([p1, p2, p3]):
c_name = f"Prev_Set{i}_Cone{idx+1}" if is_preview else f"Set{i}_Cone{idx+1}_{datetime.now().strftime('%H%M%S')}"
cone_obj = create_inverted_cone(p, cone_rad, cone_h, collection, c_name, cone_mat)
if cone_obj: objs.append(cone_obj)
return objs
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
def clear_preview(context):
col = bpy.data.collections.get(PREVIEW_COL_NAME)
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("Prev_Set") and m.users == 0:
bpy.data.meshes.remove(m)
def update_preview(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
clear_preview(context)
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)
objs = build_all_sets(props, col, is_preview=True)
for obj in objs: obj.display_type = 'TEXTURED'
context.view_layer.update()
_timer = None
def delayed_update():
global _timer
_timer = None
if bpy.context and bpy.context.scene: update_preview(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_CylinderProps(PropertyGroup):
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
active_tab: EnumProperty(
items=[('SET1', "Set 1", ""), ('SET2', "Set 2", ""), ('SET3', "Set 3", "")],
default=CURRENT_DEFAULTS['active_tab']
)
# --- Calc Panel Props ---
obs_velocity: FloatProperty(name="Observer Velocity (v)", default=CURRENT_DEFAULTS['obs_velocity'], update=on_update)
time_meet: FloatProperty(name="Meeting Time (t_meet)", default=CURRENT_DEFAULTS['time_meet'], update=on_update)
light_start_x: FloatProperty(name="Light Start X", default=CURRENT_DEFAULTS['light_start_x'], update=on_update)
light_start_y: FloatProperty(name="Light Start Y", default=CURRENT_DEFAULTS['light_start_y'], update=on_update)
light_speed: FloatProperty(name="Speed of Light (c)", default=CURRENT_DEFAULTS['light_speed'], update=on_update)
# --- SET 1 ---
set1_show: BoolProperty(default=CURRENT_DEFAULTS['set1_show'], update=on_update)
set1_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt1_xy'], update=on_update)
set1_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt2_xy'], update=on_update)
set1_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt3_xy'], update=on_update)
set1_base_z: FloatProperty(default=CURRENT_DEFAULTS['set1_base_z'], update=on_update)
set1_show_light: BoolProperty(default=CURRENT_DEFAULTS['set1_show_light'], update=on_update)
set1_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_radius'], min=0.01, update=on_update)
set1_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cyl_color'], update=on_update)
set1_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set1_show_obs'], update=on_update)
set1_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set1_obs_velocity'], update=on_update)
set1_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_obs_radius'], min=0.01, update=on_update)
set1_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_obs_color'], update=on_update)
set1_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set1_show_cones'], update=on_update)
set1_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_radius'], min=0.01, update=on_update)
set1_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_height'], min=0.01, update=on_update)
set1_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cone_color'], update=on_update)
# --- SET 2 ---
set2_show: BoolProperty(default=CURRENT_DEFAULTS['set2_show'], update=on_update)
set2_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt1_xy'], update=on_update)
set2_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt2_xy'], update=on_update)
set2_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt3_xy'], update=on_update)
set2_base_z: FloatProperty(default=CURRENT_DEFAULTS['set2_base_z'], update=on_update)
set2_show_light: BoolProperty(default=CURRENT_DEFAULTS['set2_show_light'], update=on_update)
set2_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_radius'], min=0.01, update=on_update)
set2_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cyl_color'], update=on_update)
set2_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set2_show_obs'], update=on_update)
set2_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set2_obs_velocity'], update=on_update)
set2_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_obs_radius'], min=0.01, update=on_update)
set2_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_obs_color'], update=on_update)
set2_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set2_show_cones'], update=on_update)
set2_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_radius'], min=0.01, update=on_update)
set2_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_height'], min=0.01, update=on_update)
set2_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cone_color'], update=on_update)
# --- SET 3 ---
set3_show: BoolProperty(default=CURRENT_DEFAULTS['set3_show'], update=on_update)
set3_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt1_xy'], update=on_update)
set3_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt2_xy'], update=on_update)
set3_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt3_xy'], update=on_update)
set3_base_z: FloatProperty(default=CURRENT_DEFAULTS['set3_base_z'], update=on_update)
set3_show_light: BoolProperty(default=CURRENT_DEFAULTS['set3_show_light'], update=on_update)
set3_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_radius'], min=0.01, update=on_update)
set3_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cyl_color'], update=on_update)
set3_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set3_show_obs'], update=on_update)
set3_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set3_obs_velocity'], update=on_update)
set3_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_obs_radius'], min=0.01, update=on_update)
set3_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_obs_color'], update=on_update)
set3_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set3_show_cones'], update=on_update)
set3_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_radius'], min=0.01, update=on_update)
set3_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_height'], min=0.01, update=on_update)
set3_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cone_color'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script with Current Values"
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 in Text Editor.")
return {'CANCELLED'}
code = target_text.as_string()
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "active_tab": \'{props.active_tab}\',\n'
new_dict += f' "obs_velocity": {props.obs_velocity:.4f},\n'
new_dict += f' "time_meet": {props.time_meet:.4f},\n'
new_dict += f' "light_start_x": {props.light_start_x:.4f},\n'
new_dict += f' "light_start_y": {props.light_start_y:.4f},\n'
new_dict += f' "light_speed": {props.light_speed:.4f},\n'
for i in range(1, 4):
prefix = f"set{i}"
show = getattr(props, f"{prefix}_show")
p1 = getattr(props, f"{prefix}_pt1_xy")
p2 = getattr(props, f"{prefix}_pt2_xy")
p3 = getattr(props, f"{prefix}_pt3_xy")
bz = getattr(props, f"{prefix}_base_z")
sl = getattr(props, f"{prefix}_show_light")
rad = getattr(props, f"{prefix}_radius")
ccol = getattr(props, f"{prefix}_cyl_color")
so = getattr(props, f"{prefix}_show_obs")
ov = getattr(props, f"{prefix}_obs_velocity")
orad = getattr(props, f"{prefix}_obs_radius")
ocol = getattr(props, f"{prefix}_obs_color")
scone = getattr(props, f"{prefix}_show_cones")
crad = getattr(props, f"{prefix}_cone_radius")
chgt = getattr(props, f"{prefix}_cone_height")
cocol = getattr(props, f"{prefix}_cone_color")
new_dict += f' "{prefix}_show": {show},\n'
new_dict += f' "{prefix}_pt1_xy": ({p1[0]:.4f}, {p1[1]:.4f}),\n'
new_dict += f' "{prefix}_pt2_xy": ({p2[0]:.4f}, {p2[1]:.4f}),\n'
new_dict += f' "{prefix}_pt3_xy": ({p3[0]:.4f}, {p3[1]:.4f}),\n'
new_dict += f' "{prefix}_base_z": {bz:.4f},\n'
new_dict += f' "{prefix}_show_light": {sl},\n'
new_dict += f' "{prefix}_radius": {rad:.4f},\n'
new_dict += f' "{prefix}_cyl_color": ({ccol[0]:.4f}, {ccol[1]:.4f}, {ccol[2]:.4f}, {ccol[3]:.4f}),\n'
new_dict += f' "{prefix}_show_obs": {so},\n'
new_dict += f' "{prefix}_obs_velocity": {ov:.4f},\n'
new_dict += f' "{prefix}_obs_radius": {orad:.4f},\n'
new_dict += f' "{prefix}_obs_color": ({ocol[0]:.4f}, {ocol[1]:.4f}, {ocol[2]:.4f}, {ocol[3]:.4f}),\n'
new_dict += f' "{prefix}_show_cones": {scone},\n'
new_dict += f' "{prefix}_cone_radius": {crad:.4f},\n'
new_dict += f' "{prefix}_cone_height": {chgt:.4f},\n'
new_dict += f' "{prefix}_cone_color": ({cocol[0]:.4f}, {cocol[1]:.4f}, {cocol[2]:.4f}, {cocol[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 with current values!")
except Exception as e:
self.report({'ERROR'}, f"Copy Failed: {e}")
return {'CANCELLED'}
return {'FINISHED'}
class OT_CopyCalcResult(Operator):
bl_idname = f"{OP_PREFIX}.copy_calc_result"
bl_label = "Copy Calculation Result"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
v = props.obs_velocity
t_m = props.time_meet
x_start = props.light_start_x
y_start = props.light_start_y
c = props.light_speed
x_meet = v * t_m
y_meet = 0.0
dx = x_start - x_meet
dy = y_start - y_meet
dist = math.sqrt(dx**2 + dy**2)
t_travel = dist / c if c != 0 else 0
t_emit = t_m - t_travel
text = (
f"Observer Velocity (v): {v:.3f}\n"
f"Meeting Time (t_meet): {t_m:.3f}\n"
f"Light Start Position: X = {x_start:.3f}, Y = {y_start:.3f}\n"
f"Speed of Light (c): {c:.3f}\n"
f"----------------------------------------\n"
f"[ Calculation Formula & Results ]\n"
f"1. Meeting Point (Observer Pos): ({x_meet:.3f}, {y_meet:.3f})\n"
f" X_meet = {v:.3f} * {t_m:.3f} = {x_meet:.3f}\n\n"
f"2. Distance (Light Travel Path):\n"
f" Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - {y_meet:.3f})²)\n"
f" = √({dx:.3f}² + {dy:.3f}²) = {dist:.3f}\n\n"
f"3. Travel Time (t_travel):\n"
f" t_travel = Dist / c = {dist:.3f} / {c:.3f} = {t_travel:.3f}\n\n"
f"4. Light Emit Time (t):\n"
f" t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}\n"
)
context.window_manager.clipboard = text
self.report({'INFO'}, "Calculation results & formulas copied to clipboard!")
return {'FINISHED'}
class OT_CreateSets(Operator):
bl_idname = f"{OP_PREFIX}.create_sets"
bl_label = "Create Displayed Sets"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
col = context.collection if context.collection else context.scene.collection
objs = build_all_sets(props, col, is_preview=False)
if objs:
bpy.ops.object.select_all(action='DESELECT')
for obj in objs: obj.select_set(True)
context.view_layer.objects.active = objs[0]
self.report({'INFO'}, f"Generated {len(objs)} Objects successfully!")
else:
self.report({'WARNING'}, "Nothing generated. Check settings.")
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)
self.report({'INFO'}, "Addon Removed successfully.")
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_CalcPanel(Panel):
bl_label = "Light vs Observer Calc"
bl_idname = f"{PREFIX}_PT_calc"
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: return
layout.prop(props, "obs_velocity")
layout.prop(props, "time_meet")
row = layout.row(align=True)
row.prop(props, "light_start_x")
row.prop(props, "light_start_y")
layout.prop(props, "light_speed")
# Calculation logic
v = props.obs_velocity
t_m = props.time_meet
x_start = props.light_start_x
y_start = props.light_start_y
c = props.light_speed
x_meet = v * t_m
y_meet = 0.0
dx = x_start - x_meet
dy = y_start - y_meet
dist = math.sqrt(dx**2 + dy**2)
t_travel = dist / c if c != 0 else 0
t_emit = t_m - t_travel
box = layout.box()
box.label(text="[ Formula & Results ]", icon='INFO')
box.label(text=f"Meeting Point: ({x_meet:.3f}, {y_meet:.3f})")
box.label(text=f"Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - 0)²) = {dist:.3f}")
box.label(text=f"t_travel = {dist:.3f} / {c:.3f} = {t_travel:.3f}")
box.label(text=f"t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}")
layout.separator()
layout.operator(OT_CopyCalcResult.bl_idname, icon='COPY_ID', text="Copy Formula & Results")
def draw_set_ui(layout, props, prefix):
box = layout.box()
if not getattr(props, f"{prefix}_show"):
box.label(text="⚠️ This Set is Currently Hidden", icon='INFO')
p_box = box.box()
p_box.label(text="XY Plane Points", icon='MESH_PLANE')
p_box.prop(props, f"{prefix}_pt1_xy", text="Point 1 (Start)")
p_box.prop(props, f"{prefix}_pt2_xy", text="Point 2 (Mid)")
p_box.prop(props, f"{prefix}_pt3_xy", text="Point 3 (End)")
p_box.prop(props, f"{prefix}_base_z", text="Base Z (Start Height)")
c_box = box.box()
c_row = c_box.row()
c_row.prop(props, f"{prefix}_show_light", text="45° Light Cylinder (v=1.0c)", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_light") else 'HIDE_ON', toggle=True)
if getattr(props, f"{prefix}_show_light"):
c_box.prop(props, f"{prefix}_radius", text="Thickness Radius")
c_box.prop(props, f"{prefix}_cyl_color", text="Color")
o_box = box.box()
o_row = o_box.row()
o_row.prop(props, f"{prefix}_show_obs", text="Observer Cylinder (Velocity v)", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_obs") else 'HIDE_ON', toggle=True)
if getattr(props, f"{prefix}_show_obs"):
o_box.prop(props, f"{prefix}_obs_velocity", text="Velocity (v, % of c)")
o_box.prop(props, f"{prefix}_obs_radius", text="Thickness Radius")
o_box.prop(props, f"{prefix}_obs_color", text="Color")
co_box = box.box()
co_row = co_box.row()
co_row.prop(props, f"{prefix}_show_cones", text="Height Cones at P1, P2, P3", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_cones") else 'HIDE_ON', toggle=True)
if getattr(props, f"{prefix}_show_cones"):
co_box.prop(props, f"{prefix}_cone_radius", text="Cone Base Radius")
co_box.prop(props, f"{prefix}_cone_height", text="Cone Height")
co_box.prop(props, f"{prefix}_cone_color", text="Cone Color (Alpha)")
class PT_MainPanel(Panel):
bl_label = "45-Deg Cyl & Cone (3 Sets)"
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="Please Reload Script"); return
# --- Copy Code Button ---
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Current Values")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
layout.separator()
vis_box = layout.box()
vis_row = vis_box.row(align=True)
vis_row.prop(props, "set1_show", text="Set 1", icon='HIDE_OFF' if props.set1_show else 'HIDE_ON', toggle=True)
vis_row.prop(props, "set2_show", text="Set 2", icon='HIDE_OFF' if props.set2_show else 'HIDE_ON', toggle=True)
vis_row.prop(props, "set3_show", text="Set 3", icon='HIDE_OFF' if props.set3_show else 'HIDE_ON', toggle=True)
layout.separator()
row = layout.row(align=True)
row.prop(props, "active_tab", expand=True)
if props.active_tab == 'SET1': draw_set_ui(layout, props, "set1")
elif props.active_tab == 'SET2': draw_set_ui(layout, props, "set2")
elif props.active_tab == 'SET3': draw_set_ui(layout, props, "set3")
layout.separator()
col = layout.column()
col.scale_y = 1.5
col.operator(OT_CreateSets.bl_idname, icon='MOD_BUILD', text="Create Mesh Object(s)")
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_CylinderProps,
OT_CopyFullScript,
OT_CopyCalcResult,
OT_CreateSets,
OT_OpenUrl,
OT_RemoveAddon,
PT_MainPanel,
PT_CalcPanel,
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(bpy.context)
return None
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_CylinderProps))
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: 11:45:32
# Copied: 04:00:00
import bpy
import bmesh
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime
# ==============================================================================
# 設定エリア
# ==============================================================================
PREFIX = "AngledCyl_3Sets20260313"
TAB_NAME = "[ 45° 3-Sets ]"
# ### ZIONAD_SOURCE_ID: ANGLED_CYL_3SETS_20260313 ###
bl_info = {
"name": "45-Degree Cylinder & Cone Generator (3 Sets)",
"author": "zionadchat",
"version": (2, 5, 0),
"blender": (5, 0, 0), # Blender 5.0以上 専用
"location": "3D View > Sidebar",
"description": "Generate 3 sets of 45-degree light cylinders and arbitrary velocity observer cylinders.",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: ANGLED_CYL_3SETS_20260313 ###"
ADDON_LINKS = (
{"label": "z時間軸 45度 20260313", "url": "<https://www.notion.so/20230313-322f5dacaf43806b891efa5002e663e0>"},
{"label": "Blender Python API", "url": "<https://docs.blender.org/api/current/index.html>"},
)
# ==============================================================================
# デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"show_preview": True,
"active_tab": 'SET1',
"obs_velocity": 0.1000,
"time_meet": 10.0000,
"light_start_x": 10.0000,
"light_start_y": 0.0000,
"light_speed": 1.0000,
"set1_show": True,
"set1_pt1_xy": (0.0000, 0.0000),
"set1_pt2_xy": (5.0000, 0.0000),
"set1_pt3_xy": (10.0000, 0.0000),
"set1_base_z": 0.0000,
"set1_show_light": True,
"set1_radius": 0.5000,
"set1_cyl_color": (0.2000, 0.6000, 1.0000, 1.0000),
"set1_show_obs": True,
"set1_obs_velocity": 0.5000,
"set1_obs_radius": 0.5000,
"set1_obs_color": (1.0000, 0.8000, 0.1000, 1.0000),
"set1_show_cones": True,
"set1_cone_radius": 10.0000,
"set1_cone_height": 10.0000,
"set1_cone_color": (0.2000, 0.6000, 1.0000, 0.3000),
"set2_show": False,
"set2_pt1_xy": (0.0000, 10.0000),
"set2_pt2_xy": (5.0000, 15.0000),
"set2_pt3_xy": (10.0000, 10.0000),
"set2_base_z": 0.0000,
"set2_show_light": True,
"set2_radius": 0.5000,
"set2_cyl_color": (0.2000, 1.0000, 0.4000, 1.0000),
"set2_show_obs": False,
"set2_obs_velocity": 0.5000,
"set2_obs_radius": 0.5000,
"set2_obs_color": (1.0000, 0.8000, 0.1000, 1.0000),
"set2_show_cones": True,
"set2_cone_radius": 10.0000,
"set2_cone_height": 10.0000,
"set2_cone_color": (0.2000, 1.0000, 0.4000, 0.3000),
"set3_show": False,
"set3_pt1_xy": (0.0000, -10.0000),
"set3_pt2_xy": (5.0000, -5.0000),
"set3_pt3_xy": (10.0000, -10.0000),
"set3_base_z": 0.0000,
"set3_show_light": True,
"set3_radius": 0.5000,
"set3_cyl_color": (1.0000, 0.3000, 0.2000, 1.0000),
"set3_show_obs": False,
"set3_obs_velocity": 0.5000,
"set3_obs_radius": 0.5000,
"set3_obs_color": (1.0000, 0.8000, 0.1000, 1.0000),
"set3_show_cones": True,
"set3_cone_radius": 10.0000,
"set3_cone_height": 10.0000,
"set3_cone_color": (1.0000, 0.3000, 0.2000, 0.3000),
}
# <END_DICT>
# ==============================================================================
# マテリアル作成ロジック (Blender 5.0対応版)
# ==============================================================================
def get_or_create_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
tree = mat.node_tree
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (-200, 0)
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (100, 0)
tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
mat.diffuse_color = color
return mat
# ==============================================================================
# ジオメトリ構築ロジック
# ==============================================================================
def create_single_cylinder(p1, p2, radius, collection, name, mat):
length = (p2 - p1).length
if length < 0.0001: return None
mid_point = (p1 + p2) / 2.0
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=radius, radius2=radius, depth=length)
direction = (p2 - p1).normalized()
rot = Vector((0, 0, 1)).rotation_difference(direction)
bmesh.ops.rotate(bm, cent=(0,0,0), matrix=rot.to_matrix(), verts=bm.verts)
bmesh.ops.translate(bm, vec=mid_point, verts=bm.verts)
mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
obj.data.materials.append(mat)
return obj
def create_inverted_cone(location, radius, height, collection, name, mat):
if height < 0.001 or radius < 0.001: return None
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=0.0, radius2=radius, depth=height)
bmesh.ops.translate(bm, vec=(0, 0, height / 2.0), verts=bm.verts)
bmesh.ops.translate(bm, vec=location, verts=bm.verts)
mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
obj.data.materials.append(mat)
return obj
def build_all_sets(props, collection, is_preview=False):
objs =[]
for i in range(1, 4):
prefix = f"set{i}"
if not getattr(props, f"{prefix}_show"): continue
x1, y1 = getattr(props, f"{prefix}_pt1_xy")
x2, y2 = getattr(props, f"{prefix}_pt2_xy")
x3, y3 = getattr(props, f"{prefix}_pt3_xy")
base_z = getattr(props, f"{prefix}_base_z")
# 光円錐に基づく時間(Z)の計算
dxy1 = math.hypot(x2 - x1, y2 - y1)
z2 = base_z + dxy1
dxy2 = math.hypot(x3 - x2, y3 - y2)
z3 = z2 + dxy2
p1 = Vector((x1, y1, base_z))
p2 = Vector((x2, y2, z2))
p3 = Vector((x3, y3, z3))
# 1. 光の円柱 (Light Cylinder: v=1.0c)
if getattr(props, f"{prefix}_show_light"):
cyl_rad = getattr(props, f"{prefix}_radius")
cyl_col = getattr(props, f"{prefix}_cyl_color")
cyl_mat_name = f"Mat_Prev_Light_{i}" if is_preview else f"Mat_Light_{i}_{datetime.now().strftime('%H%M%S')}"
cyl_mat = get_or_create_material(cyl_mat_name, cyl_col)
name1 = f"Prev_Set{i}_Light1" if is_preview else f"Set{i}_Light1_{datetime.now().strftime('%H%M%S')}"
name2 = f"Prev_Set{i}_Light2" if is_preview else f"Set{i}_Light2_{datetime.now().strftime('%H%M%S')}"
c1 = create_single_cylinder(p1, p2, cyl_rad, collection, name1, cyl_mat)
if c1: objs.append(c1)
c2 = create_single_cylinder(p2, p3, cyl_rad, collection, name2, cyl_mat)
if c2: objs.append(c2)
# 2. 観測者の円柱 (Observer Cylinder: Velocity v)
if getattr(props, f"{prefix}_show_obs"):
v = getattr(props, f"{prefix}_obs_velocity")
obs_rad = getattr(props, f"{prefix}_obs_radius")
obs_col = getattr(props, f"{prefix}_obs_color")
obs_mat_name = f"Mat_Prev_Obs_{i}" if is_preview else f"Mat_Obs_{i}_{datetime.now().strftime('%H%M%S')}"
obs_mat = get_or_create_material(obs_mat_name, obs_col)
p1_obs = Vector((x1, y1, base_z))
# 同じ時間(Z)で、指定速度(v)分だけXY平面を進む
p2_x_obs = x1 + (x2 - x1) * v
p2_y_obs = y1 + (y2 - y1) * v
p2_obs = Vector((p2_x_obs, p2_y_obs, z2))
p3_x_obs = p2_x_obs + (x3 - x2) * v
p3_y_obs = p2_y_obs + (y3 - y2) * v
p3_obs = Vector((p3_x_obs, p3_y_obs, z3))
name1_obs = f"Prev_Set{i}_Obs1" if is_preview else f"Set{i}_Obs1_{datetime.now().strftime('%H%M%S')}"
name2_obs = f"Prev_Set{i}_Obs2" if is_preview else f"Set{i}_Obs2_{datetime.now().strftime('%H%M%S')}"
c1_obs = create_single_cylinder(p1_obs, p2_obs, obs_rad, collection, name1_obs, obs_mat)
if c1_obs: objs.append(c1_obs)
c2_obs = create_single_cylinder(p2_obs, p3_obs, obs_rad, collection, name2_obs, obs_mat)
if c2_obs: objs.append(c2_obs)
# 3. 逆さ円錐 (Height Cones)
if getattr(props, f"{prefix}_show_cones"):
cone_rad = getattr(props, f"{prefix}_cone_radius")
cone_h = getattr(props, f"{prefix}_cone_height")
cone_col = getattr(props, f"{prefix}_cone_color")
cone_mat_name = f"Mat_Prev_Cone_{i}" if is_preview else f"Mat_Cone_{i}_{datetime.now().strftime('%H%M%S')}"
cone_mat = get_or_create_material(cone_mat_name, cone_col)
for idx, p in enumerate([p1, p2, p3]):
c_name = f"Prev_Set{i}_Cone{idx+1}" if is_preview else f"Set{i}_Cone{idx+1}_{datetime.now().strftime('%H%M%S')}"
cone_obj = create_inverted_cone(p, cone_rad, cone_h, collection, c_name, cone_mat)
if cone_obj: objs.append(cone_obj)
return objs
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
def clear_preview(context):
col = bpy.data.collections.get(PREVIEW_COL_NAME)
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("Prev_Set") and m.users == 0:
bpy.data.meshes.remove(m)
def update_preview(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
clear_preview(context)
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)
objs = build_all_sets(props, col, is_preview=True)
for obj in objs: obj.display_type = 'TEXTURED'
context.view_layer.update()
_timer = None
def delayed_update():
global _timer
_timer = None
if bpy.context and bpy.context.scene: update_preview(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_CylinderProps(PropertyGroup):
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
active_tab: EnumProperty(
items=[('SET1', "Set 1", ""), ('SET2', "Set 2", ""), ('SET3', "Set 3", "")],
default=CURRENT_DEFAULTS['active_tab']
)
# --- Calc Panel Props ---
obs_velocity: FloatProperty(name="Observer Velocity (v)", default=CURRENT_DEFAULTS['obs_velocity'], update=on_update)
time_meet: FloatProperty(name="Meeting Time (t_meet)", default=CURRENT_DEFAULTS['time_meet'], update=on_update)
light_start_x: FloatProperty(name="Light Start X", default=CURRENT_DEFAULTS['light_start_x'], update=on_update)
light_start_y: FloatProperty(name="Light Start Y", default=CURRENT_DEFAULTS['light_start_y'], update=on_update)
light_speed: FloatProperty(name="Speed of Light (c)", default=CURRENT_DEFAULTS['light_speed'], update=on_update)
# --- SET 1 ---
set1_show: BoolProperty(default=CURRENT_DEFAULTS['set1_show'], update=on_update)
set1_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt1_xy'], update=on_update)
set1_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt2_xy'], update=on_update)
set1_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt3_xy'], update=on_update)
set1_base_z: FloatProperty(default=CURRENT_DEFAULTS['set1_base_z'], update=on_update)
set1_show_light: BoolProperty(default=CURRENT_DEFAULTS['set1_show_light'], update=on_update)
set1_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_radius'], min=0.01, update=on_update)
set1_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cyl_color'], update=on_update)
set1_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set1_show_obs'], update=on_update)
set1_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set1_obs_velocity'], update=on_update)
set1_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_obs_radius'], min=0.01, update=on_update)
set1_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_obs_color'], update=on_update)
set1_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set1_show_cones'], update=on_update)
set1_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_radius'], min=0.01, update=on_update)
set1_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_height'], min=0.01, update=on_update)
set1_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cone_color'], update=on_update)
# --- SET 2 ---
set2_show: BoolProperty(default=CURRENT_DEFAULTS['set2_show'], update=on_update)
set2_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt1_xy'], update=on_update)
set2_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt2_xy'], update=on_update)
set2_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt3_xy'], update=on_update)
set2_base_z: FloatProperty(default=CURRENT_DEFAULTS['set2_base_z'], update=on_update)
set2_show_light: BoolProperty(default=CURRENT_DEFAULTS['set2_show_light'], update=on_update)
set2_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_radius'], min=0.01, update=on_update)
set2_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cyl_color'], update=on_update)
set2_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set2_show_obs'], update=on_update)
set2_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set2_obs_velocity'], update=on_update)
set2_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_obs_radius'], min=0.01, update=on_update)
set2_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_obs_color'], update=on_update)
set2_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set2_show_cones'], update=on_update)
set2_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_radius'], min=0.01, update=on_update)
set2_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_height'], min=0.01, update=on_update)
set2_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cone_color'], update=on_update)
# --- SET 3 ---
set3_show: BoolProperty(default=CURRENT_DEFAULTS['set3_show'], update=on_update)
set3_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt1_xy'], update=on_update)
set3_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt2_xy'], update=on_update)
set3_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt3_xy'], update=on_update)
set3_base_z: FloatProperty(default=CURRENT_DEFAULTS['set3_base_z'], update=on_update)
set3_show_light: BoolProperty(default=CURRENT_DEFAULTS['set3_show_light'], update=on_update)
set3_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_radius'], min=0.01, update=on_update)
set3_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cyl_color'], update=on_update)
set3_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set3_show_obs'], update=on_update)
set3_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set3_obs_velocity'], update=on_update)
set3_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_obs_radius'], min=0.01, update=on_update)
set3_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_obs_color'], update=on_update)
set3_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set3_show_cones'], update=on_update)
set3_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_radius'], min=0.01, update=on_update)
set3_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_height'], min=0.01, update=on_update)
set3_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cone_color'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script with Current Values"
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 in Text Editor.")
return {'CANCELLED'}
code = target_text.as_string()
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "active_tab": \'{props.active_tab}\',\n'
new_dict += f' "obs_velocity": {props.obs_velocity:.4f},\n'
new_dict += f' "time_meet": {props.time_meet:.4f},\n'
new_dict += f' "light_start_x": {props.light_start_x:.4f},\n'
new_dict += f' "light_start_y": {props.light_start_y:.4f},\n'
new_dict += f' "light_speed": {props.light_speed:.4f},\n'
for i in range(1, 4):
prefix = f"set{i}"
show = getattr(props, f"{prefix}_show")
p1 = getattr(props, f"{prefix}_pt1_xy")
p2 = getattr(props, f"{prefix}_pt2_xy")
p3 = getattr(props, f"{prefix}_pt3_xy")
bz = getattr(props, f"{prefix}_base_z")
sl = getattr(props, f"{prefix}_show_light")
rad = getattr(props, f"{prefix}_radius")
ccol = getattr(props, f"{prefix}_cyl_color")
so = getattr(props, f"{prefix}_show_obs")
ov = getattr(props, f"{prefix}_obs_velocity")
orad = getattr(props, f"{prefix}_obs_radius")
ocol = getattr(props, f"{prefix}_obs_color")
scone = getattr(props, f"{prefix}_show_cones")
crad = getattr(props, f"{prefix}_cone_radius")
chgt = getattr(props, f"{prefix}_cone_height")
cocol = getattr(props, f"{prefix}_cone_color")
new_dict += f' "{prefix}_show": {show},\n'
new_dict += f' "{prefix}_pt1_xy": ({p1[0]:.4f}, {p1[1]:.4f}),\n'
new_dict += f' "{prefix}_pt2_xy": ({p2[0]:.4f}, {p2[1]:.4f}),\n'
new_dict += f' "{prefix}_pt3_xy": ({p3[0]:.4f}, {p3[1]:.4f}),\n'
new_dict += f' "{prefix}_base_z": {bz:.4f},\n'
new_dict += f' "{prefix}_show_light": {sl},\n'
new_dict += f' "{prefix}_radius": {rad:.4f},\n'
new_dict += f' "{prefix}_cyl_color": ({ccol[0]:.4f}, {ccol[1]:.4f}, {ccol[2]:.4f}, {ccol[3]:.4f}),\n'
new_dict += f' "{prefix}_show_obs": {so},\n'
new_dict += f' "{prefix}_obs_velocity": {ov:.4f},\n'
new_dict += f' "{prefix}_obs_radius": {orad:.4f},\n'
new_dict += f' "{prefix}_obs_color": ({ocol[0]:.4f}, {ocol[1]:.4f}, {ocol[2]:.4f}, {ocol[3]:.4f}),\n'
new_dict += f' "{prefix}_show_cones": {scone},\n'
new_dict += f' "{prefix}_cone_radius": {crad:.4f},\n'
new_dict += f' "{prefix}_cone_height": {chgt:.4f},\n'
new_dict += f' "{prefix}_cone_color": ({cocol[0]:.4f}, {cocol[1]:.4f}, {cocol[2]:.4f}, {cocol[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 with current values!")
except Exception as e:
self.report({'ERROR'}, f"Copy Failed: {e}")
return {'CANCELLED'}
return {'FINISHED'}
class OT_CopyCalcResult(Operator):
bl_idname = f"{OP_PREFIX}.copy_calc_result"
bl_label = "Copy Calculation Result"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
v = props.obs_velocity
t_m = props.time_meet
x_start = props.light_start_x
y_start = props.light_start_y
c = props.light_speed
x_meet = v * t_m
y_meet = 0.0
dx = x_start - x_meet
dy = y_start - y_meet
dist = math.sqrt(dx**2 + dy**2)
t_travel = dist / c if c != 0 else 0
t_emit = t_m - t_travel
text = (
f"Observer Velocity (v): {v:.3f}\n"
f"Meeting Time (t_meet): {t_m:.3f}\n"
f"Light Start Position: X = {x_start:.3f}, Y = {y_start:.3f}\n"
f"Speed of Light (c): {c:.3f}\n"
f"----------------------------------------\n"
f"[ Calculation Formula & Results ]\n"
f"1. Meeting Point (Observer Pos): ({x_meet:.3f}, {y_meet:.3f})\n"
f" X_meet = {v:.3f} * {t_m:.3f} = {x_meet:.3f}\n\n"
f"2. Distance (Light Travel Path):\n"
f" Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - {y_meet:.3f})²)\n"
f" = √({dx:.3f}² + {dy:.3f}²) = {dist:.3f}\n\n"
f"3. Travel Time (t_travel):\n"
f" t_travel = Dist / c = {dist:.3f} / {c:.3f} = {t_travel:.3f}\n\n"
f"4. Light Emit Time (t):\n"
f" t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}\n"
)
context.window_manager.clipboard = text
self.report({'INFO'}, "Calculation results & formulas copied to clipboard!")
return {'FINISHED'}
class OT_CreateSets(Operator):
bl_idname = f"{OP_PREFIX}.create_sets"
bl_label = "Create Displayed Sets"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
col = context.collection if context.collection else context.scene.collection
objs = build_all_sets(props, col, is_preview=False)
if objs:
bpy.ops.object.select_all(action='DESELECT')
for obj in objs: obj.select_set(True)
context.view_layer.objects.active = objs[0]
self.report({'INFO'}, f"Generated {len(objs)} Objects successfully!")
else:
self.report({'WARNING'}, "Nothing generated. Check settings.")
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)
self.report({'INFO'}, "Addon Removed successfully.")
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_CalcPanel(Panel):
bl_label = "Light vs Observer Calc"
bl_idname = f"{PREFIX}_PT_calc"
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: return
layout.prop(props, "obs_velocity")
layout.prop(props, "time_meet")
row = layout.row(align=True)
row.prop(props, "light_start_x")
row.prop(props, "light_start_y")
layout.prop(props, "light_speed")
# Calculation logic
v = props.obs_velocity
t_m = props.time_meet
x_start = props.light_start_x
y_start = props.light_start_y
c = props.light_speed
x_meet = v * t_m
y_meet = 0.0
dx = x_start - x_meet
dy = y_start - y_meet
dist = math.sqrt(dx**2 + dy**2)
t_travel = dist / c if c != 0 else 0
t_emit = t_m - t_travel
box = layout.box()
box.label(text="[ Formula & Results ]", icon='INFO')
box.label(text=f"Meeting Point: ({x_meet:.3f}, {y_meet:.3f})")
box.label(text=f"Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - 0)²) = {dist:.3f}")
box.label(text=f"t_travel = {dist:.3f} / {c:.3f} = {t_travel:.3f}")
box.label(text=f"t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}")
layout.separator()
layout.operator(OT_CopyCalcResult.bl_idname, icon='COPY_ID', text="Copy Formula & Results")
def draw_set_ui(layout, props, prefix):
box = layout.box()
if not getattr(props, f"{prefix}_show"):
box.label(text="⚠️ This Set is Currently Hidden", icon='INFO')
p_box = box.box()
p_box.label(text="XY Plane Points", icon='MESH_PLANE')
p_box.prop(props, f"{prefix}_pt1_xy", text="Point 1 (Start)")
p_box.prop(props, f"{prefix}_pt2_xy", text="Point 2 (Mid)")
p_box.prop(props, f"{prefix}_pt3_xy", text="Point 3 (End)")
p_box.prop(props, f"{prefix}_base_z", text="Base Z (Start Height)")
c_box = box.box()
c_row = c_box.row()
c_row.prop(props, f"{prefix}_show_light", text="45° Light Cylinder (v=1.0c)", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_light") else 'HIDE_ON', toggle=True)
if getattr(props, f"{prefix}_show_light"):
c_box.prop(props, f"{prefix}_radius", text="Thickness Radius")
c_box.prop(props, f"{prefix}_cyl_color", text="Color")
o_box = box.box()
o_row = o_box.row()
o_row.prop(props, f"{prefix}_show_obs", text="Observer Cylinder (Velocity v)", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_obs") else 'HIDE_ON', toggle=True)
if getattr(props, f"{prefix}_show_obs"):
o_box.prop(props, f"{prefix}_obs_velocity", text="Velocity (v, % of c)")
o_box.prop(props, f"{prefix}_obs_radius", text="Thickness Radius")
o_box.prop(props, f"{prefix}_obs_color", text="Color")
co_box = box.box()
co_row = co_box.row()
co_row.prop(props, f"{prefix}_show_cones", text="Height Cones at P1, P2, P3", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_cones") else 'HIDE_ON', toggle=True)
if getattr(props, f"{prefix}_show_cones"):
co_box.prop(props, f"{prefix}_cone_radius", text="Cone Base Radius")
co_box.prop(props, f"{prefix}_cone_height", text="Cone Height")
co_box.prop(props, f"{prefix}_cone_color", text="Cone Color (Alpha)")
class PT_MainPanel(Panel):
bl_label = "45-Deg Cyl & Cone (3 Sets)"
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="Please Reload Script"); return
# --- Copy Code Button ---
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Current Values")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
layout.separator()
vis_box = layout.box()
vis_row = vis_box.row(align=True)
vis_row.prop(props, "set1_show", text="Set 1", icon='HIDE_OFF' if props.set1_show else 'HIDE_ON', toggle=True)
vis_row.prop(props, "set2_show", text="Set 2", icon='HIDE_OFF' if props.set2_show else 'HIDE_ON', toggle=True)
vis_row.prop(props, "set3_show", text="Set 3", icon='HIDE_OFF' if props.set3_show else 'HIDE_ON', toggle=True)
layout.separator()
row = layout.row(align=True)
row.prop(props, "active_tab", expand=True)
if props.active_tab == 'SET1': draw_set_ui(layout, props, "set1")
elif props.active_tab == 'SET2': draw_set_ui(layout, props, "set2")
elif props.active_tab == 'SET3': draw_set_ui(layout, props, "set3")
layout.separator()
col = layout.column()
col.scale_y = 1.5
col.operator(OT_CreateSets.bl_idname, icon='MOD_BUILD', text="Create Mesh Object(s)")
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_CylinderProps,
OT_CopyFullScript,
OT_CopyCalcResult,
OT_CreateSets,
OT_OpenUrl,
OT_RemoveAddon,
PT_MainPanel,
PT_CalcPanel,
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(bpy.context)
return None
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_CylinderProps))
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: 04:00:00
import bpy
import bmesh
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime
# ==============================================================================
# 設定エリア
# ==============================================================================
PREFIX = "AngledCyl_3Sets20260313"
TAB_NAME = "[ 45° 3-Sets ]"
# ### ZIONAD_SOURCE_ID: ANGLED_CYL_3SETS_20260313 ###
bl_info = {
"name": "45-Degree Cylinder & Cone Generator (3 Sets)",
"author": "zionadchat",
"version": (2, 5, 0),
"blender": (5, 0, 0), # Blender 5.0以上 専用
"location": "3D View > Sidebar",
"description": "Generate 3 sets of 45-degree light cylinders and arbitrary velocity observer cylinders.",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: ANGLED_CYL_3SETS_20260313 ###"
ADDON_LINKS = (
{"label": "z時間軸 45度 20260313", "url": "<https://www.notion.so/20230313-322f5dacaf43806b891efa5002e663e0>"},
{"label": "Blender Python API", "url": "<https://docs.blender.org/api/current/index.html>"},
)
# ==============================================================================
# デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"show_preview": True,
"active_tab": 'SET1',
"obs_velocity": 0.1000,
"time_meet": 10.0000,
"light_start_x": 10.0000,
"light_start_y": 0.0000,
"light_speed": 1.0000,
"set1_show": True,
"set1_pt1_xy": (0.0000, 0.0000),
"set1_pt2_xy": (5.0000, 5.0000),
"set1_pt3_xy": (10.0000, 0.0000),
"set1_base_z": 0.0000,
"set1_show_light": True,
"set1_radius": 0.5000,
"set1_cyl_color": (0.2000, 0.6000, 1.0000, 1.0000),
"set1_show_obs": True,
"set1_obs_velocity": 0.5000,
"set1_obs_radius": 0.5000,
"set1_obs_color": (1.0000, 0.8000, 0.1000, 1.0000),
"set1_show_cones": True,
"set1_cone_radius": 10.0000,
"set1_cone_height": 10.0000,
"set1_cone_color": (0.2000, 0.6000, 1.0000, 0.3000),
"set2_show": True,
"set2_pt1_xy": (0.0000, 10.0000),
"set2_pt2_xy": (5.0000, 15.0000),
"set2_pt3_xy": (10.0000, 10.0000),
"set2_base_z": 0.0000,
"set2_show_light": True,
"set2_radius": 0.5000,
"set2_cyl_color": (0.2000, 1.0000, 0.4000, 1.0000),
"set2_show_obs": False,
"set2_obs_velocity": 0.5000,
"set2_obs_radius": 0.5000,
"set2_obs_color": (1.0000, 0.8000, 0.1000, 1.0000),
"set2_show_cones": True,
"set2_cone_radius": 10.0000,
"set2_cone_height": 10.0000,
"set2_cone_color": (0.2000, 1.0000, 0.4000, 0.3000),
"set3_show": True,
"set3_pt1_xy": (0.0000, -10.0000),
"set3_pt2_xy": (5.0000, -5.0000),
"set3_pt3_xy": (10.0000, -10.0000),
"set3_base_z": 0.0000,
"set3_show_light": True,
"set3_radius": 0.5000,
"set3_cyl_color": (1.0000, 0.3000, 0.2000, 1.0000),
"set3_show_obs": False,
"set3_obs_velocity": 0.5000,
"set3_obs_radius": 0.5000,
"set3_obs_color": (1.0000, 0.8000, 0.1000, 1.0000),
"set3_show_cones": True,
"set3_cone_radius": 10.0000,
"set3_cone_height": 10.0000,
"set3_cone_color": (1.0000, 0.3000, 0.2000, 0.3000),
}
# <END_DICT>
# ==============================================================================
# マテリアル作成ロジック (Blender 5.0対応版)
# ==============================================================================
def get_or_create_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
tree = mat.node_tree
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (-200, 0)
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (100, 0)
tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
mat.diffuse_color = color
return mat
# ==============================================================================
# ジオメトリ構築ロジック
# ==============================================================================
def create_single_cylinder(p1, p2, radius, collection, name, mat):
length = (p2 - p1).length
if length < 0.0001: return None
mid_point = (p1 + p2) / 2.0
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=radius, radius2=radius, depth=length)
direction = (p2 - p1).normalized()
rot = Vector((0, 0, 1)).rotation_difference(direction)
bmesh.ops.rotate(bm, cent=(0,0,0), matrix=rot.to_matrix(), verts=bm.verts)
bmesh.ops.translate(bm, vec=mid_point, verts=bm.verts)
mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
obj.data.materials.append(mat)
return obj
def create_inverted_cone(location, radius, height, collection, name, mat):
if height < 0.001 or radius < 0.001: return None
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=0.0, radius2=radius, depth=height)
bmesh.ops.translate(bm, vec=(0, 0, height / 2.0), verts=bm.verts)
bmesh.ops.translate(bm, vec=location, verts=bm.verts)
mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
obj.data.materials.append(mat)
return obj
def build_all_sets(props, collection, is_preview=False):
objs =[]
for i in range(1, 4):
prefix = f"set{i}"
if not getattr(props, f"{prefix}_show"): continue
x1, y1 = getattr(props, f"{prefix}_pt1_xy")
x2, y2 = getattr(props, f"{prefix}_pt2_xy")
x3, y3 = getattr(props, f"{prefix}_pt3_xy")
base_z = getattr(props, f"{prefix}_base_z")
# 光円錐に基づく時間(Z)の計算
dxy1 = math.hypot(x2 - x1, y2 - y1)
z2 = base_z + dxy1
dxy2 = math.hypot(x3 - x2, y3 - y2)
z3 = z2 + dxy2
p1 = Vector((x1, y1, base_z))
p2 = Vector((x2, y2, z2))
p3 = Vector((x3, y3, z3))
# 1. 光の円柱 (Light Cylinder: v=1.0c)
if getattr(props, f"{prefix}_show_light"):
cyl_rad = getattr(props, f"{prefix}_radius")
cyl_col = getattr(props, f"{prefix}_cyl_color")
cyl_mat_name = f"Mat_Prev_Light_{i}" if is_preview else f"Mat_Light_{i}_{datetime.now().strftime('%H%M%S')}"
cyl_mat = get_or_create_material(cyl_mat_name, cyl_col)
name1 = f"Prev_Set{i}_Light1" if is_preview else f"Set{i}_Light1_{datetime.now().strftime('%H%M%S')}"
name2 = f"Prev_Set{i}_Light2" if is_preview else f"Set{i}_Light2_{datetime.now().strftime('%H%M%S')}"
c1 = create_single_cylinder(p1, p2, cyl_rad, collection, name1, cyl_mat)
if c1: objs.append(c1)
c2 = create_single_cylinder(p2, p3, cyl_rad, collection, name2, cyl_mat)
if c2: objs.append(c2)
# 2. 観測者の円柱 (Observer Cylinder: Velocity v)
if getattr(props, f"{prefix}_show_obs"):
v = getattr(props, f"{prefix}_obs_velocity")
obs_rad = getattr(props, f"{prefix}_obs_radius")
obs_col = getattr(props, f"{prefix}_obs_color")
obs_mat_name = f"Mat_Prev_Obs_{i}" if is_preview else f"Mat_Obs_{i}_{datetime.now().strftime('%H%M%S')}"
obs_mat = get_or_create_material(obs_mat_name, obs_col)
p1_obs = Vector((x1, y1, base_z))
# 同じ時間(Z)で、指定速度(v)分だけXY平面を進む
p2_x_obs = x1 + (x2 - x1) * v
p2_y_obs = y1 + (y2 - y1) * v
p2_obs = Vector((p2_x_obs, p2_y_obs, z2))
p3_x_obs = p2_x_obs + (x3 - x2) * v
p3_y_obs = p2_y_obs + (y3 - y2) * v
p3_obs = Vector((p3_x_obs, p3_y_obs, z3))
name1_obs = f"Prev_Set{i}_Obs1" if is_preview else f"Set{i}_Obs1_{datetime.now().strftime('%H%M%S')}"
name2_obs = f"Prev_Set{i}_Obs2" if is_preview else f"Set{i}_Obs2_{datetime.now().strftime('%H%M%S')}"
c1_obs = create_single_cylinder(p1_obs, p2_obs, obs_rad, collection, name1_obs, obs_mat)
if c1_obs: objs.append(c1_obs)
c2_obs = create_single_cylinder(p2_obs, p3_obs, obs_rad, collection, name2_obs, obs_mat)
if c2_obs: objs.append(c2_obs)
# 3. 逆さ円錐 (Height Cones)
if getattr(props, f"{prefix}_show_cones"):
cone_rad = getattr(props, f"{prefix}_cone_radius")
cone_h = getattr(props, f"{prefix}_cone_height")
cone_col = getattr(props, f"{prefix}_cone_color")
cone_mat_name = f"Mat_Prev_Cone_{i}" if is_preview else f"Mat_Cone_{i}_{datetime.now().strftime('%H%M%S')}"
cone_mat = get_or_create_material(cone_mat_name, cone_col)
for idx, p in enumerate([p1, p2, p3]):
c_name = f"Prev_Set{i}_Cone{idx+1}" if is_preview else f"Set{i}_Cone{idx+1}_{datetime.now().strftime('%H%M%S')}"
cone_obj = create_inverted_cone(p, cone_rad, cone_h, collection, c_name, cone_mat)
if cone_obj: objs.append(cone_obj)
return objs
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
def clear_preview(context):
col = bpy.data.collections.get(PREVIEW_COL_NAME)
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("Prev_Set") and m.users == 0:
bpy.data.meshes.remove(m)
def update_preview(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
clear_preview(context)
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)
objs = build_all_sets(props, col, is_preview=True)
for obj in objs: obj.display_type = 'TEXTURED'
context.view_layer.update()
_timer = None
def delayed_update():
global _timer
_timer = None
if bpy.context and bpy.context.scene: update_preview(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_CylinderProps(PropertyGroup):
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
active_tab: EnumProperty(
items=[('SET1', "Set 1", ""), ('SET2', "Set 2", ""), ('SET3', "Set 3", "")],
default=CURRENT_DEFAULTS['active_tab']
)
# --- Calc Panel Props ---
obs_velocity: FloatProperty(name="Observer Velocity (v)", default=CURRENT_DEFAULTS['obs_velocity'], update=on_update)
time_meet: FloatProperty(name="Meeting Time (t_meet)", default=CURRENT_DEFAULTS['time_meet'], update=on_update)
light_start_x: FloatProperty(name="Light Start X", default=CURRENT_DEFAULTS['light_start_x'], update=on_update)
light_start_y: FloatProperty(name="Light Start Y", default=CURRENT_DEFAULTS['light_start_y'], update=on_update)
light_speed: FloatProperty(name="Speed of Light (c)", default=CURRENT_DEFAULTS['light_speed'], update=on_update)
# --- SET 1 ---
set1_show: BoolProperty(default=CURRENT_DEFAULTS['set1_show'], update=on_update)
set1_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt1_xy'], update=on_update)
set1_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt2_xy'], update=on_update)
set1_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt3_xy'], update=on_update)
set1_base_z: FloatProperty(default=CURRENT_DEFAULTS['set1_base_z'], update=on_update)
set1_show_light: BoolProperty(default=CURRENT_DEFAULTS['set1_show_light'], update=on_update)
set1_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_radius'], min=0.01, update=on_update)
set1_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cyl_color'], update=on_update)
set1_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set1_show_obs'], update=on_update)
set1_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set1_obs_velocity'], update=on_update)
set1_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_obs_radius'], min=0.01, update=on_update)
set1_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_obs_color'], update=on_update)
set1_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set1_show_cones'], update=on_update)
set1_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_radius'], min=0.01, update=on_update)
set1_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_height'], min=0.01, update=on_update)
set1_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cone_color'], update=on_update)
# --- SET 2 ---
set2_show: BoolProperty(default=CURRENT_DEFAULTS['set2_show'], update=on_update)
set2_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt1_xy'], update=on_update)
set2_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt2_xy'], update=on_update)
set2_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt3_xy'], update=on_update)
set2_base_z: FloatProperty(default=CURRENT_DEFAULTS['set2_base_z'], update=on_update)
set2_show_light: BoolProperty(default=CURRENT_DEFAULTS['set2_show_light'], update=on_update)
set2_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_radius'], min=0.01, update=on_update)
set2_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cyl_color'], update=on_update)
set2_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set2_show_obs'], update=on_update)
set2_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set2_obs_velocity'], update=on_update)
set2_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_obs_radius'], min=0.01, update=on_update)
set2_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_obs_color'], update=on_update)
set2_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set2_show_cones'], update=on_update)
set2_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_radius'], min=0.01, update=on_update)
set2_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_height'], min=0.01, update=on_update)
set2_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cone_color'], update=on_update)
# --- SET 3 ---
set3_show: BoolProperty(default=CURRENT_DEFAULTS['set3_show'], update=on_update)
set3_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt1_xy'], update=on_update)
set3_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt2_xy'], update=on_update)
set3_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt3_xy'], update=on_update)
set3_base_z: FloatProperty(default=CURRENT_DEFAULTS['set3_base_z'], update=on_update)
set3_show_light: BoolProperty(default=CURRENT_DEFAULTS['set3_show_light'], update=on_update)
set3_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_radius'], min=0.01, update=on_update)
set3_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cyl_color'], update=on_update)
set3_show_obs: BoolProperty(default=CURRENT_DEFAULTS['set3_show_obs'], update=on_update)
set3_obs_velocity: FloatProperty(default=CURRENT_DEFAULTS['set3_obs_velocity'], update=on_update)
set3_obs_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_obs_radius'], min=0.01, update=on_update)
set3_obs_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_obs_color'], update=on_update)
set3_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set3_show_cones'], update=on_update)
set3_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_radius'], min=0.01, update=on_update)
set3_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_height'], min=0.01, update=on_update)
set3_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cone_color'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script with Current Values"
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 in Text Editor.")
return {'CANCELLED'}
code = target_text.as_string()
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "active_tab": \'{props.active_tab}\',\n'
new_dict += f' "obs_velocity": {props.obs_velocity:.4f},\n'
new_dict += f' "time_meet": {props.time_meet:.4f},\n'
new_dict += f' "light_start_x": {props.light_start_x:.4f},\n'
new_dict += f' "light_start_y": {props.light_start_y:.4f},\n'
new_dict += f' "light_speed": {props.light_speed:.4f},\n'
for i in range(1, 4):
prefix = f"set{i}"
show = getattr(props, f"{prefix}_show")
p1 = getattr(props, f"{prefix}_pt1_xy")
p2 = getattr(props, f"{prefix}_pt2_xy")
p3 = getattr(props, f"{prefix}_pt3_xy")
bz = getattr(props, f"{prefix}_base_z")
sl = getattr(props, f"{prefix}_show_light")
rad = getattr(props, f"{prefix}_radius")
ccol = getattr(props, f"{prefix}_cyl_color")
so = getattr(props, f"{prefix}_show_obs")
ov = getattr(props, f"{prefix}_obs_velocity")
orad = getattr(props, f"{prefix}_obs_radius")
ocol = getattr(props, f"{prefix}_obs_color")
scone = getattr(props, f"{prefix}_show_cones")
crad = getattr(props, f"{prefix}_cone_radius")
chgt = getattr(props, f"{prefix}_cone_height")
cocol = getattr(props, f"{prefix}_cone_color")
new_dict += f' "{prefix}_show": {show},\n'
new_dict += f' "{prefix}_pt1_xy": ({p1[0]:.4f}, {p1[1]:.4f}),\n'
new_dict += f' "{prefix}_pt2_xy": ({p2[0]:.4f}, {p2[1]:.4f}),\n'
new_dict += f' "{prefix}_pt3_xy": ({p3[0]:.4f}, {p3[1]:.4f}),\n'
new_dict += f' "{prefix}_base_z": {bz:.4f},\n'
new_dict += f' "{prefix}_show_light": {sl},\n'
new_dict += f' "{prefix}_radius": {rad:.4f},\n'
new_dict += f' "{prefix}_cyl_color": ({ccol[0]:.4f}, {ccol[1]:.4f}, {ccol[2]:.4f}, {ccol[3]:.4f}),\n'
new_dict += f' "{prefix}_show_obs": {so},\n'
new_dict += f' "{prefix}_obs_velocity": {ov:.4f},\n'
new_dict += f' "{prefix}_obs_radius": {orad:.4f},\n'
new_dict += f' "{prefix}_obs_color": ({ocol[0]:.4f}, {ocol[1]:.4f}, {ocol[2]:.4f}, {ocol[3]:.4f}),\n'
new_dict += f' "{prefix}_show_cones": {scone},\n'
new_dict += f' "{prefix}_cone_radius": {crad:.4f},\n'
new_dict += f' "{prefix}_cone_height": {chgt:.4f},\n'
new_dict += f' "{prefix}_cone_color": ({cocol[0]:.4f}, {cocol[1]:.4f}, {cocol[2]:.4f}, {cocol[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 with current values!")
except Exception as e:
self.report({'ERROR'}, f"Copy Failed: {e}")
return {'CANCELLED'}
return {'FINISHED'}
class OT_CopyCalcResult(Operator):
bl_idname = f"{OP_PREFIX}.copy_calc_result"
bl_label = "Copy Calculation Result"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
v = props.obs_velocity
t_m = props.time_meet
x_start = props.light_start_x
y_start = props.light_start_y
c = props.light_speed
x_meet = v * t_m
y_meet = 0.0
dx = x_start - x_meet
dy = y_start - y_meet
dist = math.sqrt(dx**2 + dy**2)
t_travel = dist / c if c != 0 else 0
t_emit = t_m - t_travel
text = (
f"Observer Velocity (v): {v:.3f}\n"
f"Meeting Time (t_meet): {t_m:.3f}\n"
f"Light Start Position: X = {x_start:.3f}, Y = {y_start:.3f}\n"
f"Speed of Light (c): {c:.3f}\n"
f"----------------------------------------\n"
f"[ Calculation Formula & Results ]\n"
f"1. Meeting Point (Observer Pos): ({x_meet:.3f}, {y_meet:.3f})\n"
f" X_meet = {v:.3f} * {t_m:.3f} = {x_meet:.3f}\n\n"
f"2. Distance (Light Travel Path):\n"
f" Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - {y_meet:.3f})²)\n"
f" = √({dx:.3f}² + {dy:.3f}²) = {dist:.3f}\n\n"
f"3. Travel Time (t_travel):\n"
f" t_travel = Dist / c = {dist:.3f} / {c:.3f} = {t_travel:.3f}\n\n"
f"4. Light Emit Time (t):\n"
f" t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}\n"
)
context.window_manager.clipboard = text
self.report({'INFO'}, "Calculation results & formulas copied to clipboard!")
return {'FINISHED'}
class OT_CreateSets(Operator):
bl_idname = f"{OP_PREFIX}.create_sets"
bl_label = "Create Displayed Sets"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
col = context.collection if context.collection else context.scene.collection
objs = build_all_sets(props, col, is_preview=False)
if objs:
bpy.ops.object.select_all(action='DESELECT')
for obj in objs: obj.select_set(True)
context.view_layer.objects.active = objs[0]
self.report({'INFO'}, f"Generated {len(objs)} Objects successfully!")
else:
self.report({'WARNING'}, "Nothing generated. Check settings.")
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)
self.report({'INFO'}, "Addon Removed successfully.")
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_CalcPanel(Panel):
bl_label = "Light vs Observer Calc"
bl_idname = f"{PREFIX}_PT_calc"
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: return
layout.prop(props, "obs_velocity")
layout.prop(props, "time_meet")
row = layout.row(align=True)
row.prop(props, "light_start_x")
row.prop(props, "light_start_y")
layout.prop(props, "light_speed")
# Calculation logic
v = props.obs_velocity
t_m = props.time_meet
x_start = props.light_start_x
y_start = props.light_start_y
c = props.light_speed
x_meet = v * t_m
y_meet = 0.0
dx = x_start - x_meet
dy = y_start - y_meet
dist = math.sqrt(dx**2 + dy**2)
t_travel = dist / c if c != 0 else 0
t_emit = t_m - t_travel
box = layout.box()
box.label(text="[ Formula & Results ]", icon='INFO')
box.label(text=f"Meeting Point: ({x_meet:.3f}, {y_meet:.3f})")
box.label(text=f"Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - 0)²) = {dist:.3f}")
box.label(text=f"t_travel = {dist:.3f} / {c:.3f} = {t_travel:.3f}")
box.label(text=f"t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}")
layout.separator()
layout.operator(OT_CopyCalcResult.bl_idname, icon='COPY_ID', text="Copy Formula & Results")
def draw_set_ui(layout, props, prefix):
box = layout.box()
if not getattr(props, f"{prefix}_show"):
box.label(text="⚠️ This Set is Currently Hidden", icon='INFO')
p_box = box.box()
p_box.label(text="XY Plane Points", icon='MESH_PLANE')
p_box.prop(props, f"{prefix}_pt1_xy", text="Point 1 (Start)")
p_box.prop(props, f"{prefix}_pt2_xy", text="Point 2 (Mid)")
p_box.prop(props, f"{prefix}_pt3_xy", text="Point 3 (End)")
p_box.prop(props, f"{prefix}_base_z", text="Base Z (Start Height)")
c_box = box.box()
c_row = c_box.row()
c_row.prop(props, f"{prefix}_show_light", text="45° Light Cylinder (v=1.0c)", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_light") else 'HIDE_ON', toggle=True)
if getattr(props, f"{prefix}_show_light"):
c_box.prop(props, f"{prefix}_radius", text="Thickness Radius")
c_box.prop(props, f"{prefix}_cyl_color", text="Color")
o_box = box.box()
o_row = o_box.row()
o_row.prop(props, f"{prefix}_show_obs", text="Observer Cylinder (Velocity v)", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_obs") else 'HIDE_ON', toggle=True)
if getattr(props, f"{prefix}_show_obs"):
o_box.prop(props, f"{prefix}_obs_velocity", text="Velocity (v, % of c)")
o_box.prop(props, f"{prefix}_obs_radius", text="Thickness Radius")
o_box.prop(props, f"{prefix}_obs_color", text="Color")
co_box = box.box()
co_row = co_box.row()
co_row.prop(props, f"{prefix}_show_cones", text="Height Cones at P1, P2, P3", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_cones") else 'HIDE_ON', toggle=True)
if getattr(props, f"{prefix}_show_cones"):
co_box.prop(props, f"{prefix}_cone_radius", text="Cone Base Radius")
co_box.prop(props, f"{prefix}_cone_height", text="Cone Height")
co_box.prop(props, f"{prefix}_cone_color", text="Cone Color (Alpha)")
class PT_MainPanel(Panel):
bl_label = "45-Deg Cyl & Cone (3 Sets)"
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="Please Reload Script"); return
# --- Copy Code Button ---
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Current Values")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
layout.separator()
vis_box = layout.box()
vis_row = vis_box.row(align=True)
vis_row.prop(props, "set1_show", text="Set 1", icon='HIDE_OFF' if props.set1_show else 'HIDE_ON', toggle=True)
vis_row.prop(props, "set2_show", text="Set 2", icon='HIDE_OFF' if props.set2_show else 'HIDE_ON', toggle=True)
vis_row.prop(props, "set3_show", text="Set 3", icon='HIDE_OFF' if props.set3_show else 'HIDE_ON', toggle=True)
layout.separator()
row = layout.row(align=True)
row.prop(props, "active_tab", expand=True)
if props.active_tab == 'SET1': draw_set_ui(layout, props, "set1")
elif props.active_tab == 'SET2': draw_set_ui(layout, props, "set2")
elif props.active_tab == 'SET3': draw_set_ui(layout, props, "set3")
layout.separator()
col = layout.column()
col.scale_y = 1.5
col.operator(OT_CreateSets.bl_idname, icon='MOD_BUILD', text="Create Mesh Object(s)")
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_CylinderProps,
OT_CopyFullScript,
OT_CopyCalcResult,
OT_CreateSets,
OT_OpenUrl,
OT_RemoveAddon,
PT_MainPanel,
PT_CalcPanel,
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(bpy.context)
return None
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_CylinderProps))
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: 02:27:00
import bpy
import bmesh
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime
# ==============================================================================
# 設定エリア
# ==============================================================================
PREFIX = "AngledCyl_3Sets20260313"
TAB_NAME = "[ 45° 3-Sets ]"
# ### ZIONAD_SOURCE_ID: ANGLED_CYL_3SETS_20260313 ###
bl_info = {
"name": "45-Degree Cylinder & Cone Generator (3 Sets)",
"author": "zionadchat",
"version": (2, 4, 0),
"blender": (5, 0, 0), # Blender 5.0以上 専用
"location": "3D View > Sidebar",
"description": "Generate 3 sets of 45-degree continuous cylinders with inverted height cones.",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: ANGLED_CYL_3SETS_20260313 ###"
ADDON_LINKS = (
{"label": "z時間軸 45度 20260313", "url": "<https://www.notion.so/20230313-322f5dacaf43806b891efa5002e663e0>"},
{"label": "Blender Python API", "url": "<https://docs.blender.org/api/current/index.html>"},
)
# ==============================================================================
# デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"show_preview": True,
"active_tab": 'SET1',
"obs_velocity": 0.1000,
"time_meet": 10.0000,
"light_start_x": 10.0000,
"light_start_y": 0.0000,
"light_speed": 1.0000,
"set1_show": True,
"set1_pt1_xy": (0.0000, 0.0000),
"set1_pt2_xy": (5.0000, 5.0000),
"set1_pt3_xy": (10.0000, 0.0000),
"set1_base_z": 0.0000,
"set1_radius": 0.5000,
"set1_cyl_color": (0.2000, 0.6000, 1.0000, 1.0000),
"set1_show_cones": True,
"set1_cone_radius": 10.0000,
"set1_cone_height": 10.0000,
"set1_cone_color": (0.2000, 0.6000, 1.0000, 0.3000),
"set2_show": True,
"set2_pt1_xy": (0.0000, 10.0000),
"set2_pt2_xy": (5.0000, 15.0000),
"set2_pt3_xy": (10.0000, 10.0000),
"set2_base_z": 0.0000,
"set2_radius": 0.5000,
"set2_cyl_color": (0.2000, 1.0000, 0.4000, 1.0000),
"set2_show_cones": True,
"set2_cone_radius": 10.0000,
"set2_cone_height": 10.0000,
"set2_cone_color": (0.2000, 1.0000, 0.4000, 0.3000),
"set3_show": True,
"set3_pt1_xy": (0.0000, -10.0000),
"set3_pt2_xy": (5.0000, -5.0000),
"set3_pt3_xy": (10.0000, -10.0000),
"set3_base_z": 0.0000,
"set3_radius": 0.5000,
"set3_cyl_color": (1.0000, 0.3000, 0.2000, 1.0000),
"set3_show_cones": True,
"set3_cone_radius": 10.0000,
"set3_cone_height": 10.0000,
"set3_cone_color": (1.0000, 0.3000, 0.2000, 0.3000),
}
# <END_DICT>
# ==============================================================================
# マテリアル作成ロジック (Blender 5.0対応版)
# ==============================================================================
def get_or_create_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
tree = mat.node_tree
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (-200, 0)
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (100, 0)
tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
mat.diffuse_color = color
return mat
# ==============================================================================
# ジオメトリ構築ロジック
# ==============================================================================
def create_single_cylinder(p1, p2, radius, collection, name, mat):
length = (p2 - p1).length
if length < 0.0001: return None
mid_point = (p1 + p2) / 2.0
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=radius, radius2=radius, depth=length)
direction = (p2 - p1).normalized()
rot = Vector((0, 0, 1)).rotation_difference(direction)
bmesh.ops.rotate(bm, cent=(0,0,0), matrix=rot.to_matrix(), verts=bm.verts)
bmesh.ops.translate(bm, vec=mid_point, verts=bm.verts)
mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
obj.data.materials.append(mat)
return obj
def create_inverted_cone(location, radius, height, collection, name, mat):
if height < 0.001 or radius < 0.001: return None
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=0.0, radius2=radius, depth=height)
bmesh.ops.translate(bm, vec=(0, 0, height / 2.0), verts=bm.verts)
bmesh.ops.translate(bm, vec=location, verts=bm.verts)
mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
obj.data.materials.append(mat)
return obj
def build_all_sets(props, collection, is_preview=False):
objs =[]
for i in range(1, 4):
prefix = f"set{i}"
if not getattr(props, f"{prefix}_show"): continue
x1, y1 = getattr(props, f"{prefix}_pt1_xy")
x2, y2 = getattr(props, f"{prefix}_pt2_xy")
x3, y3 = getattr(props, f"{prefix}_pt3_xy")
base_z = getattr(props, f"{prefix}_base_z")
dxy1 = math.hypot(x2 - x1, y2 - y1)
z2 = base_z + dxy1
dxy2 = math.hypot(x3 - x2, y3 - y2)
z3 = z2 + dxy2
p1 = Vector((x1, y1, base_z))
p2 = Vector((x2, y2, z2))
p3 = Vector((x3, y3, z3))
cyl_rad = getattr(props, f"{prefix}_radius")
cyl_col = getattr(props, f"{prefix}_cyl_color")
cyl_mat_name = f"Mat_Prev_Cyl_{i}" if is_preview else f"Mat_Cyl_{i}_{datetime.now().strftime('%H%M%S')}"
cyl_mat = get_or_create_material(cyl_mat_name, cyl_col)
name1 = f"Prev_Set{i}_Cyl1" if is_preview else f"Set{i}_Cyl1_{datetime.now().strftime('%H%M%S')}"
name2 = f"Prev_Set{i}_Cyl2" if is_preview else f"Set{i}_Cyl2_{datetime.now().strftime('%H%M%S')}"
c1 = create_single_cylinder(p1, p2, cyl_rad, collection, name1, cyl_mat)
if c1: objs.append(c1)
c2 = create_single_cylinder(p2, p3, cyl_rad, collection, name2, cyl_mat)
if c2: objs.append(c2)
if getattr(props, f"{prefix}_show_cones"):
cone_rad = getattr(props, f"{prefix}_cone_radius")
cone_h = getattr(props, f"{prefix}_cone_height")
cone_col = getattr(props, f"{prefix}_cone_color")
cone_mat_name = f"Mat_Prev_Cone_{i}" if is_preview else f"Mat_Cone_{i}_{datetime.now().strftime('%H%M%S')}"
cone_mat = get_or_create_material(cone_mat_name, cone_col)
for idx, p in enumerate([p1, p2, p3]):
c_name = f"Prev_Set{i}_Cone{idx+1}" if is_preview else f"Set{i}_Cone{idx+1}_{datetime.now().strftime('%H%M%S')}"
cone_obj = create_inverted_cone(p, cone_rad, cone_h, collection, c_name, cone_mat)
if cone_obj: objs.append(cone_obj)
return objs
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
def clear_preview(context):
col = bpy.data.collections.get(PREVIEW_COL_NAME)
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("Prev_Set") and m.users == 0:
bpy.data.meshes.remove(m)
def update_preview(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
clear_preview(context)
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)
objs = build_all_sets(props, col, is_preview=True)
for obj in objs: obj.display_type = 'TEXTURED'
context.view_layer.update()
_timer = None
def delayed_update():
global _timer
_timer = None
if bpy.context and bpy.context.scene: update_preview(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_CylinderProps(PropertyGroup):
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
active_tab: EnumProperty(
items=[('SET1', "Set 1", ""), ('SET2', "Set 2", ""), ('SET3', "Set 3", "")],
default=CURRENT_DEFAULTS['active_tab']
)
# --- Calc Panel Props ---
obs_velocity: FloatProperty(name="Observer Velocity (v)", default=CURRENT_DEFAULTS['obs_velocity'], update=on_update)
time_meet: FloatProperty(name="Meeting Time (t_meet)", default=CURRENT_DEFAULTS['time_meet'], update=on_update)
light_start_x: FloatProperty(name="Light Start X", default=CURRENT_DEFAULTS['light_start_x'], update=on_update)
light_start_y: FloatProperty(name="Light Start Y", default=CURRENT_DEFAULTS['light_start_y'], update=on_update)
light_speed: FloatProperty(name="Speed of Light (c)", default=CURRENT_DEFAULTS['light_speed'], update=on_update)
# --- SET 1 ---
set1_show: BoolProperty(default=CURRENT_DEFAULTS['set1_show'], update=on_update)
set1_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt1_xy'], update=on_update)
set1_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt2_xy'], update=on_update)
set1_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set1_pt3_xy'], update=on_update)
set1_base_z: FloatProperty(default=CURRENT_DEFAULTS['set1_base_z'], update=on_update)
set1_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_radius'], min=0.01, update=on_update)
set1_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cyl_color'], update=on_update)
set1_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set1_show_cones'], update=on_update)
set1_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_radius'], min=0.01, update=on_update)
set1_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set1_cone_height'], min=0.01, update=on_update)
set1_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set1_cone_color'], update=on_update)
# --- SET 2 ---
set2_show: BoolProperty(default=CURRENT_DEFAULTS['set2_show'], update=on_update)
set2_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt1_xy'], update=on_update)
set2_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt2_xy'], update=on_update)
set2_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set2_pt3_xy'], update=on_update)
set2_base_z: FloatProperty(default=CURRENT_DEFAULTS['set2_base_z'], update=on_update)
set2_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_radius'], min=0.01, update=on_update)
set2_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cyl_color'], update=on_update)
set2_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set2_show_cones'], update=on_update)
set2_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_radius'], min=0.01, update=on_update)
set2_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set2_cone_height'], min=0.01, update=on_update)
set2_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set2_cone_color'], update=on_update)
# --- SET 3 ---
set3_show: BoolProperty(default=CURRENT_DEFAULTS['set3_show'], update=on_update)
set3_pt1_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt1_xy'], update=on_update)
set3_pt2_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt2_xy'], update=on_update)
set3_pt3_xy: FloatVectorProperty(size=2, default=CURRENT_DEFAULTS['set3_pt3_xy'], update=on_update)
set3_base_z: FloatProperty(default=CURRENT_DEFAULTS['set3_base_z'], update=on_update)
set3_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_radius'], min=0.01, update=on_update)
set3_cyl_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cyl_color'], update=on_update)
set3_show_cones: BoolProperty(default=CURRENT_DEFAULTS['set3_show_cones'], update=on_update)
set3_cone_radius: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_radius'], min=0.01, update=on_update)
set3_cone_height: FloatProperty(default=CURRENT_DEFAULTS['set3_cone_height'], min=0.01, update=on_update)
set3_cone_color: FloatVectorProperty(subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['set3_cone_color'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script with Current Values"
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 in Text Editor.")
return {'CANCELLED'}
code = target_text.as_string()
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "show_preview": {props.show_preview},\n'
new_dict += f' "active_tab": \'{props.active_tab}\',\n'
new_dict += f' "obs_velocity": {props.obs_velocity:.4f},\n'
new_dict += f' "time_meet": {props.time_meet:.4f},\n'
new_dict += f' "light_start_x": {props.light_start_x:.4f},\n'
new_dict += f' "light_start_y": {props.light_start_y:.4f},\n'
new_dict += f' "light_speed": {props.light_speed:.4f},\n'
for i in range(1, 4):
prefix = f"set{i}"
show = getattr(props, f"{prefix}_show")
p1 = getattr(props, f"{prefix}_pt1_xy")
p2 = getattr(props, f"{prefix}_pt2_xy")
p3 = getattr(props, f"{prefix}_pt3_xy")
bz = getattr(props, f"{prefix}_base_z")
rad = getattr(props, f"{prefix}_radius")
ccol = getattr(props, f"{prefix}_cyl_color")
scone = getattr(props, f"{prefix}_show_cones")
crad = getattr(props, f"{prefix}_cone_radius")
chgt = getattr(props, f"{prefix}_cone_height")
cocol = getattr(props, f"{prefix}_cone_color")
new_dict += f' "{prefix}_show": {show},\n'
new_dict += f' "{prefix}_pt1_xy": ({p1[0]:.4f}, {p1[1]:.4f}),\n'
new_dict += f' "{prefix}_pt2_xy": ({p2[0]:.4f}, {p2[1]:.4f}),\n'
new_dict += f' "{prefix}_pt3_xy": ({p3[0]:.4f}, {p3[1]:.4f}),\n'
new_dict += f' "{prefix}_base_z": {bz:.4f},\n'
new_dict += f' "{prefix}_radius": {rad:.4f},\n'
new_dict += f' "{prefix}_cyl_color": ({ccol[0]:.4f}, {ccol[1]:.4f}, {ccol[2]:.4f}, {ccol[3]:.4f}),\n'
new_dict += f' "{prefix}_show_cones": {scone},\n'
new_dict += f' "{prefix}_cone_radius": {crad:.4f},\n'
new_dict += f' "{prefix}_cone_height": {chgt:.4f},\n'
new_dict += f' "{prefix}_cone_color": ({cocol[0]:.4f}, {cocol[1]:.4f}, {cocol[2]:.4f}, {cocol[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 with current values!")
except Exception as e:
self.report({'ERROR'}, f"Copy Failed: {e}")
return {'CANCELLED'}
return {'FINISHED'}
class OT_CopyCalcResult(Operator):
bl_idname = f"{OP_PREFIX}.copy_calc_result"
bl_label = "Copy Calculation Result"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
v = props.obs_velocity
t_m = props.time_meet
x_start = props.light_start_x
y_start = props.light_start_y
c = props.light_speed
# Calculation logic
x_meet = v * t_m
y_meet = 0.0 # Observer travels on X-axis (Y=0)
dx = x_start - x_meet
dy = y_start - y_meet
dist = math.sqrt(dx**2 + dy**2)
t_travel = dist / c if c != 0 else 0
t_emit = t_m - t_travel
text = (
f"Observer Velocity (v): {v:.3f}\n"
f"Meeting Time (t_meet): {t_m:.3f}\n"
f"Light Start Position: X = {x_start:.3f}, Y = {y_start:.3f}\n"
f"Speed of Light (c): {c:.3f}\n"
f"----------------------------------------\n"
f"[ Calculation Formula & Results ]\n"
f"1. Meeting Point (Observer Pos): ({x_meet:.3f}, {y_meet:.3f})\n"
f" X_meet = {v:.3f} * {t_m:.3f} = {x_meet:.3f}\n\n"
f"2. Distance (Light Travel Path):\n"
f" Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - {y_meet:.3f})²)\n"
f" = √({dx:.3f}² + {dy:.3f}²) = {dist:.3f}\n\n"
f"3. Travel Time (t_travel):\n"
f" t_travel = Dist / c = {dist:.3f} / {c:.3f} = {t_travel:.3f}\n\n"
f"4. Light Emit Time (t):\n"
f" t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}\n"
)
context.window_manager.clipboard = text
self.report({'INFO'}, "Calculation results & formulas copied to clipboard!")
return {'FINISHED'}
class OT_CreateSets(Operator):
bl_idname = f"{OP_PREFIX}.create_sets"
bl_label = "Create Displayed Sets"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
col = context.collection if context.collection else context.scene.collection
objs = build_all_sets(props, col, is_preview=False)
if objs:
bpy.ops.object.select_all(action='DESELECT')
for obj in objs: obj.select_set(True)
context.view_layer.objects.active = objs[0]
self.report({'INFO'}, f"Generated {len(objs)} Objects successfully!")
else:
self.report({'WARNING'}, "Nothing generated. Check settings.")
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)
self.report({'INFO'}, "Addon Removed successfully.")
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_CalcPanel(Panel):
bl_label = "Light vs Observer Calc"
bl_idname = f"{PREFIX}_PT_calc"
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: return
layout.prop(props, "obs_velocity")
layout.prop(props, "time_meet")
row = layout.row(align=True)
row.prop(props, "light_start_x")
row.prop(props, "light_start_y")
layout.prop(props, "light_speed")
# Calculation logic
v = props.obs_velocity
t_m = props.time_meet
x_start = props.light_start_x
y_start = props.light_start_y
c = props.light_speed
x_meet = v * t_m
y_meet = 0.0
dx = x_start - x_meet
dy = y_start - y_meet
dist = math.sqrt(dx**2 + dy**2)
t_travel = dist / c if c != 0 else 0
t_emit = t_m - t_travel
box = layout.box()
box.label(text="[ Formula & Results ]", icon='INFO')
box.label(text=f"Meeting Point: ({x_meet:.3f}, {y_meet:.3f})")
box.label(text=f"Dist = √(({x_start:.3f} - {x_meet:.3f})² + ({y_start:.3f} - 0)²) = {dist:.3f}")
box.label(text=f"t_travel = {dist:.3f} / {c:.3f} = {t_travel:.3f}")
box.label(text=f"t_emit = {t_m:.3f} - {t_travel:.3f} = {t_emit:.3f}")
layout.separator()
layout.operator(OT_CopyCalcResult.bl_idname, icon='COPY_ID', text="Copy Formula & Results")
def draw_set_ui(layout, props, prefix):
box = layout.box()
if not getattr(props, f"{prefix}_show"):
box.label(text="⚠️ This Set is Currently Hidden", icon='INFO')
p_box = box.box()
p_box.label(text="XY Plane Points", icon='MESH_PLANE')
p_box.prop(props, f"{prefix}_pt1_xy", text="Point 1 (Start)")
p_box.prop(props, f"{prefix}_pt2_xy", text="Point 2 (Mid)")
p_box.prop(props, f"{prefix}_pt3_xy", text="Point 3 (End)")
c_box = box.box()
c_box.label(text="45° Continuous Cylinders", icon='MESH_CYLINDER')
c_box.prop(props, f"{prefix}_base_z", text="Base Z (Start Height)")
c_box.prop(props, f"{prefix}_radius", text="Thickness Radius")
c_box.prop(props, f"{prefix}_cyl_color", text="Color")
co_box = box.box()
co_row = co_box.row()
co_row.prop(props, f"{prefix}_show_cones", text="Height Cones at P1, P2, P3", icon='HIDE_OFF' if getattr(props, f"{prefix}_show_cones") else 'HIDE_ON', toggle=True)
if getattr(props, f"{prefix}_show_cones"):
co_box.prop(props, f"{prefix}_cone_radius", text="Cone Base Radius")
co_box.prop(props, f"{prefix}_cone_height", text="Cone Height")
co_box.prop(props, f"{prefix}_cone_color", text="Cone Color (Alpha)")
class PT_MainPanel(Panel):
bl_label = "45-Deg Cyl & Cone (3 Sets)"
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="Please Reload Script"); return
# --- Copy Code Button ---
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Current Values")
layout.separator()
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
layout.separator()
vis_box = layout.box()
vis_row = vis_box.row(align=True)
vis_row.prop(props, "set1_show", text="Set 1", icon='HIDE_OFF' if props.set1_show else 'HIDE_ON', toggle=True)
vis_row.prop(props, "set2_show", text="Set 2", icon='HIDE_OFF' if props.set2_show else 'HIDE_ON', toggle=True)
vis_row.prop(props, "set3_show", text="Set 3", icon='HIDE_OFF' if props.set3_show else 'HIDE_ON', toggle=True)
layout.separator()
row = layout.row(align=True)
row.prop(props, "active_tab", expand=True)
if props.active_tab == 'SET1': draw_set_ui(layout, props, "set1")
elif props.active_tab == 'SET2': draw_set_ui(layout, props, "set2")
elif props.active_tab == 'SET3': draw_set_ui(layout, props, "set3")
layout.separator()
col = layout.column()
col.scale_y = 1.5
col.operator(OT_CreateSets.bl_idname, icon='MOD_BUILD', text="Create Mesh Object(s)")
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_CylinderProps,
OT_CopyFullScript,
OT_CopyCalcResult,
OT_CreateSets,
OT_OpenUrl,
OT_RemoveAddon,
PT_MainPanel,
PT_CalcPanel,
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(bpy.context)
return None
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_CylinderProps))
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()
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
# ==============================================================================
# 設定エリア
# ==============================================================================
PREFIX = "AngledCyl2026"
TAB_NAME = "[ 45° Cylinder ]"
bl_info = {
"name": "45-Degree Cylinder Generator",
"author": "zionadchat",
"version": (1, 0, 0),
"blender": (5, 0, 0), # Blender 5.0以上 専用
"location": "3D View > Sidebar",
"description": "Generate a 45-degree cylinder from two XY points.",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
ADDON_LINKS = (
{"label": "Blender Python API", "url": "<https://docs.blender.org/api/current/index.html>"},
{"label": "Math (Vector) Reference", "url": "<https://docs.blender.org/api/current/mathutils.html>"},
)
CURRENT_DEFAULTS = {
"show_preview": True,
"pt1_xy": (0.0000, 0.0000),
"pt2_xy": (5.0000, 5.0000),
"base_z": 0.0000,
"radius": 1.0000,
"cylinder_color": (0.2000, 0.6000, 0.8000, 1.0000)
}
# ==============================================================================
# マテリアル作成ロジック (Blender 5.0対応版)
# ==============================================================================
def get_or_create_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
tree = mat.node_tree
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (-200, 0)
bsdf.inputs["Base Color"].default_value = color
# Blender 5.0の Principled BSDF 仕様対応
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
out = tree.nodes.new("ShaderNodeOutputMaterial")
out.location = (100, 0)
tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
mat.diffuse_color = color
return mat
# ==============================================================================
# ジオメトリ構築ロジック
# ==============================================================================
def build_cylinder(props, collection, is_preview=False):
x1, y1 = props.pt1_xy
x2, y2 = props.pt2_xy
z1 = props.base_z
# XY平面上の距離を計算
dx = x2 - x1
dy = y2 - y1
dxy = math.sqrt(dx**2 + dy**2)
# 同一座標等の場合は生成しない
if dxy < 0.0001:
return None
# Z軸に対して45度にするため、高さの差(dZ)をXY平面の距離(dXY)と同一にする
z2 = z1 + dxy
p1 = Vector((x1, y1, z1))
p2 = Vector((x2, y2, z2))
length = (p2 - p1).length
mid_point = (p1 + p2) / 2.0
# 円柱の生成
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, segments=32, radius1=props.radius, radius2=props.radius, depth=length)
# 向きの設定 (初期Z軸方向を p1 -> p2 への方向ベクトルへ回転)
direction = (p2 - p1).normalized()
rot = Vector((0, 0, 1)).rotation_difference(direction)
bmesh.ops.rotate(bm, cent=(0,0,0), matrix=rot.to_matrix(), verts=bm.verts)
bmesh.ops.translate(bm, vec=mid_point, verts=bm.verts)
# オブジェクト化
name = f"Prev_Cylinder_{PREFIX}" if is_preview else f"AngledCyl_{datetime.now().strftime('%H%M%S')}"
mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(name, mesh)
collection.objects.link(obj)
# マテリアルの適用
mat_name = f"Mat_Prev_{PREFIX}" if is_preview else f"Mat_{name}"
mat = get_or_create_material(mat_name, props.cylinder_color)
obj.data.materials.append(mat)
return obj
# ==============================================================================
# プレビュー用ロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
def clear_preview(context):
col = bpy.data.collections.get(PREVIEW_COL_NAME)
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_Cylinder_{PREFIX}") and m.users == 0:
bpy.data.meshes.remove(m)
def update_preview(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
clear_preview(context)
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)
obj = build_cylinder(props, col, is_preview=True)
if obj:
obj.display_type = 'TEXTURED'
context.view_layer.update()
_timer = None
def delayed_update():
global _timer
_timer = None
if bpy.context and bpy.context.scene: update_preview(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_CylinderProps(PropertyGroup):
show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
pt1_xy: FloatVectorProperty(name="Point 1 (Low) XY", size=2, default=CURRENT_DEFAULTS['pt1_xy'], update=on_update)
pt2_xy: FloatVectorProperty(name="Point 2 (High) XY", size=2, default=CURRENT_DEFAULTS['pt2_xy'], update=on_update)
base_z: FloatProperty(name="Base Z (Low Z Height)", default=CURRENT_DEFAULTS['base_z'], update=on_update)
radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['radius'], min=0.01, update=on_update)
cylinder_color: FloatVectorProperty(name="Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['cylinder_color'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_CreateCylinder(Operator):
bl_idname = f"{OP_PREFIX}.create_cylinder"
bl_label = "Create 45-Deg Cylinder"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
col = context.collection if context.collection else context.scene.collection
obj = build_cylinder(props, col, is_preview=False)
if obj:
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
context.view_layer.objects.active = obj
self.report({'INFO'}, "45-Degree Cylinder Created!")
else:
self.report({'WARNING'}, "Points are too close. Generation canceled.")
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)
self.report({'INFO'}, "Addon Removed successfully.")
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_MainPanel(Panel):
bl_label = "45-Deg Cylinder Gen"
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="Please Reload Script")
return
layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
box = layout.box()
box.label(text="XY Plane Points", icon='MESH_PLANE')
box.prop(props, "pt1_xy")
box.prop(props, "pt2_xy")
box2 = layout.box()
box2.label(text="Cylinder Base & Shape", icon='MESH_CYLINDER')
box2.prop(props, "base_z")
box2.prop(props, "radius")
box2.prop(props, "cylinder_color")
# 情報を表示 (プレビューを分かりやすくするため)
layout.separator()
x1, y1 = props.pt1_xy
x2, y2 = props.pt2_xy
dxy = math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
dz = dxy # 45度角なのでXY距離とZ高さが同一
length = math.sqrt(dxy**2 + dz**2)
info_col = layout.column(align=True)
info_col.label(text=f"Calculated Height (+Z): {dz:.3f}", icon='SORT_ASC')
info_col.label(text=f"Total Cylinder Length: {length:.3f}", icon='LINCURVE')
layout.separator()
col = layout.column()
col.scale_y = 1.5
col.operator(OT_CreateCylinder.bl_idname, icon='MESH_CYLINDER', text="Create Object")
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_CylinderProps, OT_CreateCylinder, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, 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(bpy.context)
return None
def register():
for c in classes:
bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_CylinderProps))
# スクリプト実行後、自動で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()