サイドバー", } # ============================================================================== # 定数 & 設定エリア # ============================================================================== ADDON_CATEGORY_NAME = bl_info["category"] PREFIX = "interval_track_gen_2026" MAIN_COLLECTION_NAME = "光時計グループ" BASE_SHAPE_COLLECTION_NAME = "ベース図形" SPHERE_COLLECTION_NAME = "球体" END_TORUS_COLLECTION_NAME = "先端トーラス" BASE_OBJ_NAME = "光時計" SPHERE_OBJ_BASE_NAME = "マーカー" "> サイドバー", } # ============================================================================== # 定数 & 設定エリア # ============================================================================== ADDON_CATEGORY_NAME = bl_info["category"] PREFIX = "interval_track_gen_2026" MAIN_COLLECTION_NAME = "光時計グループ" BASE_SHAPE_COLLECTION_NAME = "ベース図形" SPHERE_COLLECTION_NAME = "球体" END_TORUS_COLLECTION_NAME = "先端トーラス" BASE_OBJ_NAME = "光時計" SPHERE_OBJ_BASE_NAME = "マーカー" "> サイドバー", } # ============================================================================== # 定数 & 設定エリア # ============================================================================== ADDON_CATEGORY_NAME = bl_info["category"] PREFIX = "interval_track_gen_2026" MAIN_COLLECTION_NAME = "光時計グループ" BASE_SHAPE_COLLECTION_NAME = "ベース図形" SPHERE_COLLECTION_NAME = "球体" END_TORUS_COLLECTION_NAME = "先端トーラス" BASE_OBJ_NAME = "光時計" SPHERE_OBJ_BASE_NAME = "マーカー" ">

import bpy
import bmesh
import math
import mathutils
import random
from bpy.types import Operator, Panel, PropertyGroup
# ==============================================================================
# アドオンのメタデータ
# ==============================================================================
bl_info = {
"name": "軌跡エフェクト生成 (等間隔 / 到達ハイブリッド)",
"author": "zionadchat",
"version": (3, 6, 0),
"blender": (4, 4, 0),
"category": " 22 [ 軌跡ジェネレーター ] ",
"description": "円柱/トーラス、軌跡生成、先端トーラス生成機能搭載",
"location": "3Dビュー > サイドバー",
}
# ==============================================================================
# 定数 & 設定エリア
# ==============================================================================
ADDON_CATEGORY_NAME = bl_info["category"]
PREFIX = "interval_track_gen_2026"
MAIN_COLLECTION_NAME = "光時計グループ"
BASE_SHAPE_COLLECTION_NAME = "ベース図形"
SPHERE_COLLECTION_NAME = "球体"
END_TORUS_COLLECTION_NAME = "先端トーラス"
BASE_OBJ_NAME = "光時計"
SPHERE_OBJ_BASE_NAME = "マーカー"
ADDON_LINKS = [
{"label": "アドオン削除パネル 20260628", "url": "<https://note.com/zionadmillion/n/ndc6fd2eabd2a>", "icon": "URL"},
{"label": "進化版 画面中央 透視投影視座位置", "url": "<https://www.notion.so/20260319bb-327f5dacaf43801e8e37ce489dc1d593>", "icon": "URL"},
{"label": "5520 背景色 変更", "url": "<https://www.notion.so/5520-20260316-314f5dacaf4380da9be4c05551d40710>", "icon": "URL"},
]
# ==============================================================================
# ユーティリティ関数
# ==============================================================================
def get_color_material(color, prefix="Mat_Track_"):
"""色(RGB)を名前にしたマテリアルを取得・生成し、重複を自動防止"""
r = int(max(0.0, min(1.0, color[0])) * 255)
g = int(max(0.0, min(1.0, color[1])) * 255)
b = int(max(0.0, min(1.0, color[2])) * 255)
mat_name = f"{prefix}{r:02X}{g:02X}{b:02X}"
mat = bpy.data.materials.get(mat_name)
if not mat:
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf: bsdf.inputs['Base Color'].default_value = color
mat.diffuse_color = color
else:
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf: bsdf.inputs['Base Color'].default_value = color
mat.diffuse_color = color
return mat
def cleanup_unused_materials():
"""使われていない自動生成マテリアルを削除し、スライダー操作時のゴミを消す"""
for mat in list(bpy.data.materials):
if (mat.name.startswith("Mat_Base_") or
mat.name.startswith("Mat_Sphere_") or
mat.name.startswith("Mat_EndTorus_")) and mat.users == 0:
bpy.data.materials.remove(mat)
def create_torus_bmesh(bm, major_segments, minor_segments, major_radius, minor_radius):
verts = []
for i in range(major_segments):
theta = i * 2 * math.pi / major_segments
cos_theta = math.cos(theta)
sin_theta = math.sin(theta)
loop_verts = []
for j in range(minor_segments):
phi = j * 2 * math.pi / minor_segments
cos_phi = math.cos(phi)
sin_phi = math.sin(phi)
x = (major_radius + minor_radius * cos_phi) * cos_theta
y = (major_radius + minor_radius * cos_phi) * sin_theta
z = minor_radius * sin_phi
v = bm.verts.new((x, y, z))
loop_verts.append(v)
verts.append(loop_verts)
for i in range(major_segments):
next_i = (i + 1) % major_segments
for j in range(minor_segments):
next_j = (j + 1) % minor_segments
bm.faces.new((verts[i][j], verts[i][next_j], verts[next_i][next_j], verts[next_i][j]))
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
def get_or_create_collection(name, parent_col=None):
col = bpy.data.collections.get(name)
if not col:
col = bpy.data.collections.new(name)
if parent_col: parent_col.children.link(col)
else: bpy.context.scene.collection.children.link(col)
return col
def get_active_light_clock(context):
for obj in context.scene.objects:
if obj.get("is_light_clock") == 1: return obj
return None
def update_light_clock(self, context):
clock_obj = get_active_light_clock(context)
if not clock_obj: return
props = context.scene.light_clock_props
# --------------------------------------------------------------------------
# 1. ベース図形の更新
# --------------------------------------------------------------------------
bm = bmesh.new()
if props.base_shape == 'CYLINDER':
start_pos = mathutils.Vector((props.start_x, props.start_y, props.start_z))
end_pos = mathutils.Vector((props.end_x, props.end_y, props.end_z))
vec = end_pos - start_pos
length = vec.length
bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
for v in bm.verts: v.co.z += 0.5
clock_obj.location = start_pos
if length > 0.0001:
clock_obj.rotation_euler = vec.to_track_quat('Z', 'Y').to_euler()
else:
clock_obj.rotation_euler = (0, 0, 0)
radius = props.thickness / 2.0
clock_obj.scale = (radius, radius, length)
elif props.base_shape == 'TORUS':
create_torus_bmesh(bm, major_segments=48, minor_segments=16,
major_radius=props.torus_major_radius,
minor_radius=props.torus_minor_radius)
clock_obj.location = (props.torus_loc_x, props.torus_loc_y, props.torus_loc_z)
clock_obj.rotation_euler = (props.torus_rot_x, props.torus_rot_y, props.torus_rot_z)
clock_obj.scale = (1.0, 1.0, 1.0)
bm.to_mesh(clock_obj.data)
bm.free()
clock_mat = get_color_material(props.color, prefix="Mat_Base_")
if clock_obj.data.materials: clock_obj.data.materials[0] = clock_mat
else: clock_obj.data.materials.append(clock_mat)
# --------------------------------------------------------------------------
# 2. 球体の生成と数合わせ
# --------------------------------------------------------------------------
sphere_objs = [obj for obj in bpy.data.objects if obj.get("is_light_clock_sphere") == 1]
base_count = props.sphere_count if props.use_spheres else 0
gather_add = props.gather_count if (props.use_spheres and props.use_gather) else 0
target_count = base_count + (base_count * gather_add)
current_count = len(sphere_objs)
main_col = get_or_create_collection(MAIN_COLLECTION_NAME, context.scene.collection)
sphere_col = get_or_create_collection(SPHERE_COLLECTION_NAME, main_col)
if current_count < target_count:
for _ in range(target_count - current_count):
mesh = bpy.data.meshes.new(f"{SPHERE_OBJ_BASE_NAME}_Mesh")
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
bm.to_mesh(mesh)
bm.free()
new_sphere = bpy.data.objects.new(f"{SPHERE_OBJ_BASE_NAME}_編集中", mesh)
new_sphere["is_light_clock_sphere"] = 1
new_sphere["individual_color"] = (random.random(), random.random(), random.random(), 1.0)
sphere_col.objects.link(new_sphere)
sphere_objs.append(new_sphere)
elif current_count > target_count:
for _ in range(current_count - target_count):
obj_to_remove = sphere_objs.pop()
mesh = obj_to_remove.data
bpy.data.objects.remove(obj_to_remove, do_unlink=True)
if mesh: bpy.data.meshes.remove(mesh, do_unlink=True)
if "group_color" not in clock_obj:
clock_obj["group_color"] = (random.random(), random.random(), random.random(), 1.0)
# --------------------------------------------------------------------------
# 2.5 先端トーラスの生成と数合わせ
# --------------------------------------------------------------------------
end_torus_objs = [obj for obj in bpy.data.objects if obj.get("is_light_clock_end_torus") == 1]
target_end_torus_count = base_count if (props.use_spheres and props.use_gather and props.show_end_torus) else 0
end_torus_col = get_or_create_collection(END_TORUS_COLLECTION_NAME, main_col)
if target_end_torus_count > 0:
end_torus_mesh = bpy.data.meshes.get("先端トーラス_Mesh")
if not end_torus_mesh:
end_torus_mesh = bpy.data.meshes.new("先端トーラス_Mesh")
bm_t = bmesh.new()
create_torus_bmesh(bm_t, major_segments=32, minor_segments=12,
major_radius=props.end_torus_major_radius,
minor_radius=props.end_torus_minor_radius)
bm_t.to_mesh(end_torus_mesh)
bm_t.free()
current_t_count = len(end_torus_objs)
if current_t_count < target_end_torus_count:
# 必要な場合、作成した同一メッシュを共有して生成
end_torus_mesh = bpy.data.meshes.get("先端トーラス_Mesh")
for _ in range(target_end_torus_count - current_t_count):
new_torus = bpy.data.objects.new("先端トーラス_編集中", end_torus_mesh)
new_torus["is_light_clock_end_torus"] = 1
end_torus_col.objects.link(new_torus)
end_torus_objs.append(new_torus)
elif current_t_count > target_end_torus_count:
for _ in range(current_t_count - target_end_torus_count):
obj_to_remove = end_torus_objs.pop()
bpy.data.objects.remove(obj_to_remove, do_unlink=True)
# --------------------------------------------------------------------------
# 3. 球体と先端トーラスの配置・色設定
# --------------------------------------------------------------------------
base_data = []
for i in range(base_count):
sphere = sphere_objs[i]
if props.base_shape == 'CYLINDER':
ratio = i / (base_count - 1) if base_count > 1 else 0.5
world_pos = start_pos.lerp(end_pos, ratio)
t_size_ratio = ratio
else: # TORUS
ratio = i / base_count
theta = ratio * 2 * math.pi
x = math.cos(theta) * props.torus_major_radius
y = math.sin(theta) * props.torus_major_radius
local_pos = mathutils.Vector((x, y, 0.0))
t_loc = mathutils.Vector((props.torus_loc_x, props.torus_loc_y, props.torus_loc_z))
t_rot = mathutils.Euler((props.torus_rot_x, props.torus_rot_y, props.torus_rot_z), 'XYZ').to_matrix()
world_pos = t_loc + t_rot @ local_pos
t_size_ratio = i / max(1, base_count - 1) if base_count > 1 else 0.0
sphere.location = world_pos
target_ratio = props.sphere_size_target / 10.0
t_size = min(t_size_ratio / target_ratio, 1.0) if target_ratio > 0 else 1.0
scale_factor = 1.0
if props.sphere_size_pattern == 'DECREASE': scale_factor = 1.0 - 0.8 * t_size
elif props.sphere_size_pattern == 'INCREASE': scale_factor = 0.2 + 0.8 * t_size
final_radius = props.sphere_base_radius * scale_factor
sphere.scale = (final_radius, final_radius, final_radius)
c = (1,1,1,1)
if props.sphere_color_type == 'CYLINDER_COLOR': c = props.color
elif props.sphere_color_type == 'CUSTOM_COLOR': c = props.sphere_custom_color
elif props.sphere_color_type == 'GROUP_RANDOM': c = clock_obj["group_color"]
elif props.sphere_color_type == 'INDIVIDUAL_RANDOM': c = sphere["individual_color"]
sp_mat = get_color_material(c, prefix="Mat_Sphere_")
if sphere.data.materials: sphere.data.materials[0] = sp_mat
else: sphere.data.materials.append(sp_mat)
base_data.append({'pos': world_pos, 'radius': final_radius, 'color': c})
# --- 軌跡の配置 ---
if props.use_spheres and props.use_gather:
gather_pos = mathutils.Vector((props.gather_x, props.gather_y, props.gather_z))
for i, data in enumerate(base_data):
dir_vec = mathutils.Vector((0, 0, 1))
diff = gather_pos - data['pos']
if diff.length > 0.0001:
dir_vec = diff.normalized()
for j in range(gather_add):
traj_idx = base_count + (i * gather_add) + j
traj_sphere = sphere_objs[traj_idx]
if props.gather_mode == 'REACH':
t = (j + 1) / gather_add
traj_sphere.location = data['pos'].lerp(gather_pos, t)
else:
dist = float(j + 1) * props.gather_interval
traj_sphere.location = data['pos'] + dir_vec * dist
t = (j + 1) / gather_add
g_scale = 1.0
if props.gather_size_pattern == 'DECREASE': g_scale = 1.0 - 0.8 * t
elif props.gather_size_pattern == 'INCREASE': g_scale = 1.0 + 2.0 * t
tr_radius = data['radius'] * g_scale
traj_sphere.scale = (tr_radius, tr_radius, tr_radius)
tr_mat = get_color_material(data['color'], prefix="Mat_Sphere_")
if traj_sphere.data.materials: traj_sphere.data.materials[0] = tr_mat
else: traj_sphere.data.materials.append(tr_mat)
# --- 先端トーラスの配置 ---
if props.show_end_torus and j == gather_add - 1:
t_obj = end_torus_objs[i]
t_obj.location = traj_sphere.location
# 軌跡の進行方向に対して垂直(Z軸が進行方向)に向かせる
t_obj.rotation_euler = dir_vec.to_track_quat('Z', 'Y').to_euler()
t_mat = get_color_material(data['color'], prefix="Mat_EndTorus_")
if t_obj.data.materials: t_obj.data.materials[0] = t_mat
else: t_obj.data.materials.append(t_mat)
cleanup_unused_materials()
# ==============================================================================
# Properties
# ==============================================================================
class LightClockProperties(PropertyGroup):
base_shape: bpy.props.EnumProperty(
name="ベース形状", items=[('CYLINDER', "円柱", ""), ('TORUS', "トーラス", "")],
default='CYLINDER', update=update_light_clock
)
color: bpy.props.FloatVectorProperty(name="図形の色", subtype='COLOR', size=4, default=(1.0, 0.0, 0.0, 1.0), min=0.0, max=1.0, update=update_light_clock)
thickness: bpy.props.FloatProperty(name="太さ", default=1.0, min=0.01, update=update_light_clock)
start_x: bpy.props.FloatProperty(name="X", default=-15.0, update=update_light_clock)
start_y: bpy.props.FloatProperty(name="Y", default=0.0, update=update_light_clock)
start_z: bpy.props.FloatProperty(name="Z", default=0.0, update=update_light_clock)
end_x: bpy.props.FloatProperty(name="X", default=15.0, update=update_light_clock)
end_y: bpy.props.FloatProperty(name="Y", default=0.0, update=update_light_clock)
end_z: bpy.props.FloatProperty(name="Z", default=0.0, update=update_light_clock)
torus_major_radius: bpy.props.FloatProperty(name="大半径", default=10.0, min=0.01, update=update_light_clock)
torus_minor_radius: bpy.props.FloatProperty(name="小半径", default=0.5, min=0.01, update=update_light_clock)
torus_loc_x: bpy.props.FloatProperty(name="X", default=0.0, update=update_light_clock)
torus_loc_y: bpy.props.FloatProperty(name="Y", default=0.0, update=update_light_clock)
torus_loc_z: bpy.props.FloatProperty(name="Z", default=0.0, update=update_light_clock)
torus_rot_x: bpy.props.FloatProperty(name="X", default=0.0, subtype='ANGLE', update=update_light_clock)
torus_rot_y: bpy.props.FloatProperty(name="Y", default=0.0, subtype='ANGLE', update=update_light_clock)
torus_rot_z: bpy.props.FloatProperty(name="Z", default=0.0, subtype='ANGLE', update=update_light_clock)
use_spheres: bpy.props.BoolProperty(name="球体マーカーを配置する", default=True, update=update_light_clock)
sphere_count: bpy.props.IntProperty(name="配置数", default=11, min=1, max=36, update=update_light_clock)
sphere_color_type: bpy.props.EnumProperty(
name="色の種類",
items=[('CYLINDER_COLOR', "図形と同じ色", ""), ('CUSTOM_COLOR', "指定色", ""),
('GROUP_RANDOM', "グループランダム", ""), ('INDIVIDUAL_RANDOM', "完全個別ランダム", "")],
default='CYLINDER_COLOR', update=update_light_clock
)
sphere_custom_color: bpy.props.FloatVectorProperty(name="球体指定色", subtype='COLOR', size=4, default=(0.0, 1.0, 0.0, 1.0), min=0.0, max=1.0, update=update_light_clock)
sphere_base_radius: bpy.props.FloatProperty(name="基本サイズ", default=1.5, min=0.01, update=update_light_clock)
sphere_size_target: bpy.props.IntProperty(name="変化ターゲット位置", default=10, min=0, max=10, update=update_light_clock)
sphere_size_pattern: bpy.props.EnumProperty(
name="サイズ変化", items=[('CONSTANT', "変わらない", ""), ('DECREASE', "指定位置へ小さくなる", ""), ('INCREASE', "指定位置へ大きくなる", "")],
default='CONSTANT', update=update_light_clock
)
use_gather: bpy.props.BoolProperty(name="軌跡を生成", default=False, update=update_light_clock)
gather_mode: bpy.props.EnumProperty(
name="軌跡の配置モード",
items=[('REACH', "目標座標へ到達させる", ""), ('DIRECTION', "目標の方向へ等間隔で並べる", "")],
default='DIRECTION', update=update_light_clock
)
gather_interval: bpy.props.FloatProperty(name="等間隔の距離", default=3.0, min=0.01, update=update_light_clock)
gather_x: bpy.props.FloatProperty(name="X", default=0.0, update=update_light_clock)
gather_y: bpy.props.FloatProperty(name="Y", default=0.0, update=update_light_clock)
gather_z: bpy.props.FloatProperty(name="Z", default=0.0, update=update_light_clock)
gather_count: bpy.props.IntProperty(name="追加配置数 (1ルート)", default=10, min=1, max=100, update=update_light_clock)
gather_size_pattern: bpy.props.EnumProperty(
name="軌跡サイズ変化", items=[('CONSTANT', "変わらない", ""), ('DECREASE', "先端へ行くほど小さくなる", ""), ('INCREASE', "先端へ行くほど大きくなる", "")],
default='CONSTANT', update=update_light_clock
)
# --- 先端トーラス ---
show_end_torus: bpy.props.BoolProperty(name="軌跡の先端にトーラスを表示", default=False, update=update_light_clock)
end_torus_major_radius: bpy.props.FloatProperty(name="大半径", default=2.0, min=0.01, update=update_light_clock)
end_torus_minor_radius: bpy.props.FloatProperty(name="小半径", default=0.2, min=0.01, update=update_light_clock)
# ==============================================================================
# Operators & Panels
# ==============================================================================
class FocusObjectAndSetViewOperator(Operator):
bl_idname = f"{PREFIX}.focus_object_and_set_view"
bl_label = "選択オブジェクトを中央に (視線-Y)"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
obj = context.active_object
if not obj: return {'CANCELLED'}
rv3d = None
for area in context.screen.areas:
if area.type == 'VIEW_3D':
rv3d = area.spaces.active.region_3d
break
if not rv3d: return {'CANCELLED'}
rv3d.view_location = obj.location
rv3d.view_rotation = mathutils.Euler((math.radians(90), 0.0, 0.0), 'XYZ').to_quaternion()
rv3d.view_perspective = 'PERSP'
return {'FINISHED'}
class CreateLightClockOperator(Operator):
bl_idname = f"{PREFIX}.create_light_clock"
bl_label = "図形を生成"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
if get_active_light_clock(context): getattr(bpy.ops, PREFIX).detach_light_clock()
main_col = get_or_create_collection(MAIN_COLLECTION_NAME, context.scene.collection)
base_col = get_or_create_collection(BASE_SHAPE_COLLECTION_NAME, main_col)
mesh = bpy.data.meshes.new(f"{BASE_OBJ_NAME}_Mesh")
clock_obj = bpy.data.objects.new(f"{BASE_OBJ_NAME}_編集中", mesh)
clock_obj["is_light_clock"] = 1
base_col.objects.link(clock_obj)
bpy.ops.object.select_all(action='DESELECT')
clock_obj.select_set(True)
context.view_layer.objects.active = clock_obj
update_light_clock(None, context)
return {'FINISHED'}
class DetachLightClockOperator(Operator):
bl_idname = f"{PREFIX}.detach_light_clock"
bl_label = "アドオンから切り離し"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
clock_obj = get_active_light_clock(context)
if not clock_obj: return {'CANCELLED'}
del clock_obj["is_light_clock"]
if "group_color" in clock_obj: del clock_obj["group_color"]
clock_obj.name = clock_obj.name.replace("_編集中", "")
sphere_objs = [obj for obj in bpy.data.objects if obj.get("is_light_clock_sphere") == 1]
for s in sphere_objs:
del s["is_light_clock_sphere"]
if "individual_color" in s: del s["individual_color"]
s.name = s.name.replace("_編集中", "")
end_torus_objs = [obj for obj in bpy.data.objects if obj.get("is_light_clock_end_torus") == 1]
for t in end_torus_objs:
del t["is_light_clock_end_torus"]
t.name = t.name.replace("_編集中", "")
return {'FINISHED'}
class RemoveAllPanels(Operator):
bl_idname = f"{PREFIX}.remove_all_panels"
bl_label = "アドオン削除"
def execute(self, context):
unregister()
return {'FINISHED'}
class OpenURLOperator(Operator):
bl_idname = f"{PREFIX}.open_url"
bl_label = "URLを開く"
url: bpy.props.StringProperty(default="")
def execute(self, context):
import webbrowser
webbrowser.open(self.url)
return {'FINISHED'}
# ==============================================================================
# Panels
# ==============================================================================
class BasePanel(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY_NAME
class VIEW3D_PT_LightClockPanel(BasePanel):
bl_label = "軌跡生成 (円柱 / トーラス)"
bl_idname = f"{PREFIX}_PT_main_panel"
bl_order = 1
def draw(self, context):
layout = self.layout
props = context.scene.light_clock_props
layout.operator(f"{PREFIX}.focus_object_and_set_view", icon='VIEW_CAMERA')
layout.separator()
box_pos = layout.box()
box_pos.prop(props, "base_shape")
box_pos.separator()
if props.base_shape == 'CYLINDER':
box_pos.label(text="円柱の座標:", icon='MESH_CYLINDER')
col = box_pos.column(align=True)
row1 = col.row(align=True); row1.label(text="出発点:")
row1.prop(props, "start_x"); row1.prop(props, "start_y"); row1.prop(props, "start_z")
row2 = col.row(align=True); row2.label(text="指定位置:")
row2.prop(props, "end_x"); row2.prop(props, "end_y"); row2.prop(props, "end_z")
box_pos.separator()
box_pos.prop(props, "thickness")
else:
box_pos.label(text="トーラス設定 (XY平面ベース):", icon='MESH_TORUS')
col = box_pos.column(align=True)
row_rad = col.row(align=True)
row_rad.prop(props, "torus_major_radius")
row_rad.prop(props, "torus_minor_radius")
col.separator()
col.label(text="移動 (中心位置):")
row_loc = col.row(align=True)
row_loc.prop(props, "torus_loc_x"); row_loc.prop(props, "torus_loc_y"); row_loc.prop(props, "torus_loc_z")
col.label(text="回転 (XYZ):")
row_rot = col.row(align=True)
row_rot.prop(props, "torus_rot_x"); row_rot.prop(props, "torus_rot_y"); row_rot.prop(props, "torus_rot_z")
box_pos.separator()
box_pos.prop(props, "color")
box_sp = layout.box()
box_sp.prop(props, "use_spheres", icon='SPHERE')
if props.use_spheres:
box_sp.prop(props, "sphere_count", slider=True)
box_sp.prop(props, "sphere_color_type")
if props.sphere_color_type == 'CUSTOM_COLOR': box_sp.prop(props, "sphere_custom_color")
box_sp.separator()
box_sp.prop(props, "sphere_base_radius")
box_sp.prop(props, "sphere_size_pattern")
if props.sphere_size_pattern != 'CONSTANT': box_sp.prop(props, "sphere_size_target", slider=True)
box_gather = layout.box()
box_gather.prop(props, "use_gather", icon='TRACKING')
if props.use_gather:
col_g = box_gather.column(align=True)
col_g.prop(props, "gather_mode")
col_g.separator()
if props.gather_mode == 'REACH':
col_g.label(text="到達させる座標 (集合点):")
else:
col_g.label(text="方向指示点の座標:")
row_g = col_g.row(align=True)
row_g.prop(props, "gather_x")
row_g.prop(props, "gather_y")
row_g.prop(props, "gather_z")
if props.gather_mode == 'DIRECTION':
col_g.separator()
col_g.prop(props, "gather_interval")
box_gather.separator()
box_gather.prop(props, "gather_count", slider=True)
box_gather.prop(props, "gather_size_pattern")
# --- 先端トーラスUI ---
box_end = box_gather.box()
box_end.prop(props, "show_end_torus", icon='MESH_TORUS')
if props.show_end_torus:
col_e = box_end.column(align=True)
row_e = col_e.row(align=True)
row_e.prop(props, "end_torus_major_radius")
row_e.prop(props, "end_torus_minor_radius")
layout.separator()
row = layout.row(align=True)
row.scale_y = 1.2
row.operator(f"{PREFIX}.create_light_clock", text="図形作成", icon='TIME')
row.operator(f"{PREFIX}.detach_light_clock", text="切り離し (独立)", icon='UNLINKED')
class VIEW3D_PT_LinkPanel(BasePanel):
bl_label = "リンク"
bl_idname = f"{PREFIX}_PT_link_panel"
bl_order = 2
def draw(self, context):
for link in ADDON_LINKS:
op = self.layout.operator(f"{PREFIX}.open_url", text=link["label"], icon=link.get("icon", "URL"))
op.url = link["url"]
class VIEW3D_PT_RemovePanel(BasePanel):
bl_label = "アドオン削除"
bl_idname = f"{PREFIX}_PT_remove"
bl_order = 3
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
self.layout.operator(f"{PREFIX}.remove_all_panels", text="アドオン削除", icon='CANCEL')
# ==============================================================================
# Registration
# ==============================================================================
classes = [
LightClockProperties, FocusObjectAndSetViewOperator, CreateLightClockOperator,
DetachLightClockOperator, RemoveAllPanels, OpenURLOperator,
VIEW3D_PT_LightClockPanel, VIEW3D_PT_LinkPanel, VIEW3D_PT_RemovePanel
]
def register():
for cls in classes: bpy.utils.register_class(cls)
bpy.types.Scene.light_clock_props = bpy.props.PointerProperty(type=LightClockProperties)
def unregister():
for cls in reversed(classes):
try: bpy.utils.unregister_class(cls)
except: pass
if hasattr(bpy.types.Scene, "light_clock_props"): del bpy.types.Scene.light_clock_props
if __name__ == "__main__": register()