N Panel > 3D View", "description": "線分の回転を反時計回り(プラス)にし、両端の角度情報も表示", "warning": "", "doc_url": "", "category": "3D View", } import bpy import math # 作成日 2026年07月21日 # =================================== # 4. 共通定義 # =================================== ADDON_TITLE = "Airplane Landing Visualizer" PREFIX_VAL = "AIR_LANDING" ADDON_CATEGORY = "3D View" PREFIX = PREFIX_VAL.lower() # ========================================================= # 6. 初期設定 # ========================================================= # 数値・座標 START_X_DEFAULT = -100.0 # 開始地点のX座標 LANDING_ANGLE_DEFAULT = 30.0 # 初期降下角度(度) # サイズ (半径・長さ・太さ) RADIUS_CYLINDER_DEFAULT = 0.5 # 降下軌道(円柱)の太さ RADIUS_SPHERE_DEFAULT = 1.5 # 飛行機(現在位置・球体)の大きさ RADIUS_TARGET_DEFAULT = 2.0 # 着地点の大きさ LENGTH_LINE_DEFAULT = 2.0 #"> N Panel > 3D View", "description": "線分の回転を反時計回り(プラス)にし、両端の角度情報も表示", "warning": "", "doc_url": "", "category": "3D View", } import bpy import math # 作成日 2026年07月21日 # =================================== # 4. 共通定義 # =================================== ADDON_TITLE = "Airplane Landing Visualizer" PREFIX_VAL = "AIR_LANDING" ADDON_CATEGORY = "3D View" PREFIX = PREFIX_VAL.lower() # ========================================================= # 6. 初期設定 # ========================================================= # 数値・座標 START_X_DEFAULT = -100.0 # 開始地点のX座標 LANDING_ANGLE_DEFAULT = 30.0 # 初期降下角度(度) # サイズ (半径・長さ・太さ) RADIUS_CYLINDER_DEFAULT = 0.5 # 降下軌道(円柱)の太さ RADIUS_SPHERE_DEFAULT = 1.5 # 飛行機(現在位置・球体)の大きさ RADIUS_TARGET_DEFAULT = 2.0 # 着地点の大きさ LENGTH_LINE_DEFAULT = 2.0 #"> N Panel > 3D View", "description": "線分の回転を反時計回り(プラス)にし、両端の角度情報も表示", "warning": "", "doc_url": "", "category": "3D View", } import bpy import math # 作成日 2026年07月21日 # =================================== # 4. 共通定義 # =================================== ADDON_TITLE = "Airplane Landing Visualizer" PREFIX_VAL = "AIR_LANDING" ADDON_CATEGORY = "3D View" PREFIX = PREFIX_VAL.lower() # ========================================================= # 6. 初期設定 # ========================================================= # 数値・座標 START_X_DEFAULT = -100.0 # 開始地点のX座標 LANDING_ANGLE_DEFAULT = 30.0 # 初期降下角度(度) # サイズ (半径・長さ・太さ) RADIUS_CYLINDER_DEFAULT = 0.5 # 降下軌道(円柱)の太さ RADIUS_SPHERE_DEFAULT = 1.5 # 飛行機(現在位置・球体)の大きさ RADIUS_TARGET_DEFAULT = 2.0 # 着地点の大きさ LENGTH_LINE_DEFAULT = 2.0 #">
bl_info = {
"name": "Airplane Landing Visualizer",
"author": "ChatGPT",
"version": (1, 7, 0),
"blender": (5, 0, 0),
"location": "View3D > N Panel > 3D View",
"description": "線分の回転を反時計回り(プラス)にし、両端の角度情報も表示",
"warning": "",
"doc_url": "",
"category": "3D View",
}
import bpy
import math
# 作成日 2026年07月21日
# ===================================
# 4. 共通定義
# ===================================
ADDON_TITLE = "Airplane Landing Visualizer"
PREFIX_VAL = "AIR_LANDING"
ADDON_CATEGORY = "3D View"
PREFIX = PREFIX_VAL.lower()
# =========================================================
# 6. 初期設定
# =========================================================
# 数値・座標
START_X_DEFAULT = -100.0 # 開始地点のX座標
LANDING_ANGLE_DEFAULT = 30.0 # 初期降下角度(度)
# サイズ (半径・長さ・太さ)
RADIUS_CYLINDER_DEFAULT = 0.5 # 降下軌道(円柱)の太さ
RADIUS_SPHERE_DEFAULT = 1.5 # 飛行機(現在位置・球体)の大きさ
RADIUS_TARGET_DEFAULT = 2.0 # 着地点の大きさ
LENGTH_LINE_DEFAULT = 2.0 # 線分の全長
THICKNESS_LINE_DEFAULT = 0.1 # 線分の太さ(半径)
# 角度
LINE_PITCH_DEFAULT = 0.0 # 線分の初期Y軸回転角度(0.0でX軸に完全平行)
# 色 (R, G, B, A)
COLOR_GLIDE_PATH = (0.0, 0.5, 1.0, 0.3) # 軌道(青・半透明)
COLOR_AIRPLANE = (1.0, 0.0, 0.0, 1.0) # 飛行機(赤)
COLOR_TARGET = (0.0, 1.0, 0.0, 1.0) # 着地点(緑)
COLOR_LINE = (1.0, 1.0, 0.0, 1.0) # 姿勢線分(黄)
# =========================================================
# 処理ロジック・コールバック
# =========================================================
def create_material(name, color):
mat = bpy.data.materials.get(name)
if mat is None:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
bsdf.inputs["Alpha"].default_value = color[3]
return mat
def update_airplane_and_line_pos(self, context):
sphere = bpy.data.objects.get(f"{PREFIX}_Airplane")
line = bpy.data.objects.get(f"{PREFIX}_Line")
if sphere and line:
x = self.current_x
angle_rad = math.radians(self.angle)
z = -x * math.tan(angle_rad)
sphere.location = (x, 0, z)
line.location = (x, 0, z)
pitch_rad = math.radians(self.line_pitch)
# プラスで反時計回り(機首上げ)になるように引き算に変更
line.rotation_euler = (0, math.radians(90) - pitch_rad, 0)
def update_glide_path(self, context):
obj = bpy.data.objects.get(f"{PREFIX}_GlidePath")
if obj:
start_x = START_X_DEFAULT
angle_rad = math.radians(self.angle)
start_z = -start_x * math.tan(angle_rad)
obj.location = (start_x / 2.0, 0, start_z / 2.0)
length = math.sqrt(start_x**2 + start_z**2)
radius = self.cyl_radius
obj.scale = (radius, radius, length / 2.0)
Ry = math.atan2(-start_x, -start_z)
obj.rotation_euler = (0, Ry, 0)
update_airplane_and_line_pos(self, context)
def update_cyl_scale(self, context):
obj = bpy.data.objects.get(f"{PREFIX}_GlidePath")
if obj:
obj.scale[0] = self.cyl_radius
obj.scale[1] = self.cyl_radius
def update_sph_scale(self, context):
obj = bpy.data.objects.get(f"{PREFIX}_Airplane")
if obj:
r = self.sph_radius
obj.scale = (r, r, r)
def update_line_scale(self, context):
obj = bpy.data.objects.get(f"{PREFIX}_Line")
if obj:
t = self.line_thickness
obj.scale = (t, t, self.line_length / 2.0)
def update_colors(self, context):
mats = {
f"{PREFIX}_GlidePath_Mat": self.cyl_color,
f"{PREFIX}_Airplane_Mat": self.sph_color,
f"{PREFIX}_Line_Mat": self.line_color
}
for mat_name, color in mats.items():
mat = bpy.data.materials.get(mat_name)
if mat and mat.use_nodes:
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
bsdf.inputs["Alpha"].default_value = color[3]
# =========================================================
# プロパティグループ
# =========================================================
class AIR_LANDING_Properties(bpy.types.PropertyGroup):
current_x: bpy.props.FloatProperty(
name="現在位置 X", default=START_X_DEFAULT, min=START_X_DEFAULT, max=0.0, update=update_airplane_and_line_pos
)
angle: bpy.props.FloatProperty(
name="降下角度", default=LANDING_ANGLE_DEFAULT, min=1.0, max=89.0, update=update_glide_path
)
line_pitch: bpy.props.FloatProperty(
name="Y軸回転 (ピッチ)", default=LINE_PITCH_DEFAULT, min=-180.0, max=180.0, update=update_airplane_and_line_pos
)
cyl_radius: bpy.props.FloatProperty(
name="軌道の太さ", default=RADIUS_CYLINDER_DEFAULT, min=0.01, max=10.0, update=update_cyl_scale
)
sph_radius: bpy.props.FloatProperty(
name="球体の大きさ", default=RADIUS_SPHERE_DEFAULT, min=0.1, max=10.0, update=update_sph_scale
)
line_length: bpy.props.FloatProperty(
name="線分の全長", default=LENGTH_LINE_DEFAULT, min=0.1, max=50.0, update=update_line_scale
)
line_thickness: bpy.props.FloatProperty(
name="線分の太さ", default=THICKNESS_LINE_DEFAULT, min=0.01, max=5.0, update=update_line_scale
)
cyl_color: bpy.props.FloatVectorProperty(
name="軌道の色", subtype='COLOR', size=4, default=COLOR_GLIDE_PATH, update=update_colors, min=0.0, max=1.0
)
sph_color: bpy.props.FloatVectorProperty(
name="球体の色", subtype='COLOR', size=4, default=COLOR_AIRPLANE, update=update_colors, min=0.0, max=1.0
)
line_color: bpy.props.FloatVectorProperty(
name="線分の色", subtype='COLOR', size=4, default=COLOR_LINE, update=update_colors, min=0.0, max=1.0
)
# =========================================================
# オペレーター
# =========================================================
class AIR_LANDING_OT_copy_text(bpy.types.Operator):
bl_idname = f"{PREFIX}.copy_text"
bl_label = "コピー"
text_to_copy: bpy.props.StringProperty()
def execute(self, context):
context.window_manager.clipboard = self.text_to_copy
self.report({'INFO'}, f"コピーしました: {self.text_to_copy}")
return {'FINISHED'}
class AIR_LANDING_OT_create_scene(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_scene"
bl_label = "シーンを生成・リセット"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.object.select_all(action='DESELECT')
for obj in bpy.context.scene.objects:
if obj.name.startswith(f"{PREFIX}_"):
obj.select_set(True)
bpy.ops.object.delete()
props = context.scene.air_landing_props
# 1. 着地点
bpy.ops.mesh.primitive_uv_sphere_add(radius=RADIUS_TARGET_DEFAULT, align='WORLD', location=(0, 0, 0))
target = context.active_object
target.name = f"{PREFIX}_Target"
target.data.materials.append(create_material(f"{PREFIX}_Target_Mat", COLOR_TARGET))
# 2. 降下軌道
bpy.ops.mesh.primitive_cylinder_add(radius=1.0, depth=2.0, align='WORLD', location=(0,0,0))
cyl = context.active_object
cyl.name = f"{PREFIX}_GlidePath"
cyl.data.materials.append(create_material(f"{PREFIX}_GlidePath_Mat", COLOR_GLIDE_PATH))
# 3. 飛行機 / 現在位置
bpy.ops.mesh.primitive_uv_sphere_add(radius=1.0, align='WORLD', location=(0,0,0))
airplane = context.active_object
airplane.name = f"{PREFIX}_Airplane"
airplane.data.materials.append(create_material(f"{PREFIX}_Airplane_Mat", COLOR_AIRPLANE))
# 4. 機体姿勢(線分)
bpy.ops.mesh.primitive_cylinder_add(radius=1.0, depth=2.0, align='WORLD', location=(0,0,0))
line = context.active_object
line.name = f"{PREFIX}_Line"
line.data.materials.append(create_material(f"{PREFIX}_Line_Mat", COLOR_LINE))
# 初期値の適用
props.current_x = START_X_DEFAULT
props.line_pitch = LINE_PITCH_DEFAULT
update_glide_path(props, context)
update_cyl_scale(props, context)
update_sph_scale(props, context)
update_line_scale(props, context)
self.report({'INFO'}, "降下シミュレーションシーンを生成しました")
return {'FINISHED'}
# =========================================================
# UI (Nパネル)
# =========================================================
class AIR_LANDING_PT_main_panel(bpy.types.Panel):
bl_label = ADDON_TITLE
bl_idname = f"{PREFIX}_PT_main_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
props = context.scene.air_landing_props
col = layout.column(align=True)
col.operator(AIR_LANDING_OT_create_scene.bl_idname, icon='PLAY')
col.separator()
col.prop(props, "angle")
col.prop(props, "current_x")
col.separator()
# 姿勢(線分)操作
box_line = layout.box()
box_line.label(text="機体姿勢(黄色の線分):", icon='DRIVER_ROTATIONAL_DIFFERENCE')
box_line.prop(props, "line_pitch")
box_line.prop(props, "line_length")
box_line.prop(props, "line_thickness")
col.separator()
# --- 計算・情報表示ブロック (小数点1桁で統一) ---
x = props.current_x
angle_rad = math.radians(props.angle)
z = -x * math.tan(angle_rad)
# 中央(0,0,0)からの距離と角度
dist_center = math.sqrt(x**2 + 0.0**2 + z**2)
horiz_dist = abs(x)
angle_from_origin = math.degrees(math.atan2(z, horiz_dist)) if horiz_dist > 0 else 90.0
# 線分の両端座標の計算 (プラスを反時計回りにする計算)
pitch_rad = math.radians(props.line_pitch)
half_len = props.line_length / 2.0
dx = half_len * math.cos(pitch_rad)
dz = half_len * math.sin(pitch_rad)
tip1 = (x + dx, 0.0, z + dz)
tip2 = (x - dx, 0.0, z - dz)
# 両端の距離
dist_tip1 = math.sqrt(tip1[0]**2 + tip1[1]**2 + tip1[2]**2)
dist_tip2 = math.sqrt(tip2[0]**2 + tip2[1]**2 + tip2[2]**2)
# 両端の角度 (中央からの仰角/俯角)
horiz_dist_1 = abs(tip1[0])
angle_tip1 = math.degrees(math.atan2(tip1[2], horiz_dist_1)) if horiz_dist_1 > 0 else 90.0
horiz_dist_2 = abs(tip2[0])
angle_tip2 = math.degrees(math.atan2(tip2[2], horiz_dist_2)) if horiz_dist_2 > 0 else 90.0
# 小数点1桁にフォーマット
s_center = f"中央 距離: {dist_center:.1f} | 角度: {angle_from_origin:.1f}°"
s_tip1 = f"先端1 距離: {dist_tip1:.1f} | 角度: {angle_tip1:.1f}°"
s_tip2 = f"先端2 距離: {dist_tip2:.1f} | 角度: {angle_tip2:.1f}°"
all_text = f"{s_center}\n{s_tip1}\n{s_tip2}"
box_info = layout.box()
box_info.label(text="原点(0,0,0)からの距離・角度:", icon='INFO')
# 1行ずつ表示 & 各行のコピーボタン
row1 = box_info.row()
row1.label(text=s_center)
op1 = row1.operator(AIR_LANDING_OT_copy_text.bl_idname, text="", icon='COPYDOWN')
op1.text_to_copy = s_center
row2 = box_info.row()
row2.label(text=s_tip1)
op2 = row2.operator(AIR_LANDING_OT_copy_text.bl_idname, text="", icon='COPYDOWN')
op2.text_to_copy = s_tip1
row3 = box_info.row()
row3.label(text=s_tip2)
op3 = row3.operator(AIR_LANDING_OT_copy_text.bl_idname, text="", icon='COPYDOWN')
op3.text_to_copy = s_tip2
# 一括コピーボタン
row_all = box_info.row()
op_all = row_all.operator(AIR_LANDING_OT_copy_text.bl_idname, text="一括コピー", icon='COPYDOWN')
op_all.text_to_copy = all_text
col.separator()
# 方程式表示ブロック
slope = math.tan(math.radians(props.angle))
eq_str = f"Z = {-slope:.1f} * X (X <= 0, Y = 0)"
box_eq = layout.box()
box_eq.label(text="軌道の方程式:")
row_eq = box_eq.row()
row_eq.label(text=eq_str)
op = row_eq.operator(AIR_LANDING_OT_copy_text.bl_idname, text="", icon='COPYDOWN')
op.text_to_copy = eq_str
# 見た目調整ブロック
box_style = layout.box()
box_style.label(text="見た目の調整", icon='COLOR')
r_c = box_style.row()
r_c.prop(props, "cyl_radius")
r_c.prop(props, "cyl_color", text="")
r_s = box_style.row()
r_s.prop(props, "sph_radius")
r_s.prop(props, "sph_color", text="")
r_l = box_style.row()
r_l.prop(props, "line_color", text="")
# =========================================================
# 登録・解除
# =========================================================
classes = (
AIR_LANDING_Properties,
AIR_LANDING_OT_copy_text,
AIR_LANDING_OT_create_scene,
AIR_LANDING_PT_main_panel,
)
def apply_startup_view_settings():
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.show_region_ui = True
space.shading.type = 'MATERIAL'
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.air_landing_props = bpy.props.PointerProperty(type=AIR_LANDING_Properties)
bpy.app.timers.register(apply_startup_view_settings, first_interval=0.1)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
del bpy.types.Scene.air_landing_props
if __name__ == "__main__":
register()
bl_info = {
"name": "Airplane Landing Visualizer",
"author": "ChatGPT",
"version": (1, 6, 0),
"blender": (5, 0, 0),
"location": "View3D > N Panel > 3D View",
"description": "線分の最大長さを50に拡張し、情報を小数点1桁で表示する機能",
"warning": "",
"doc_url": "",
"category": "3D View",
}
import bpy
import math
# 作成日 2026年07月21日
# ===================================
# 4. 共通定義
# ===================================
ADDON_TITLE = "Airplane Landing Visualizer"
PREFIX_VAL = "AIR_LANDING"
ADDON_CATEGORY = "3D View"
PREFIX = PREFIX_VAL.lower()
# =========================================================
# 6. 初期設定
# =========================================================
# 数値・座標
START_X_DEFAULT = -100.0 # 開始地点のX座標
LANDING_ANGLE_DEFAULT = 30.0 # 初期降下角度(度)
# サイズ (半径・長さ・太さ)
RADIUS_CYLINDER_DEFAULT = 0.5 # 降下軌道(円柱)の太さ
RADIUS_SPHERE_DEFAULT = 1.5 # 飛行機(現在位置・球体)の大きさ
RADIUS_TARGET_DEFAULT = 2.0 # 着地点の大きさ
LENGTH_LINE_DEFAULT = 2.0 # 線分の全長
THICKNESS_LINE_DEFAULT = 0.1 # 線分の太さ(半径)
# 角度
LINE_PITCH_DEFAULT = 0.0 # 線分の初期Y軸回転角度(0.0でX軸に完全平行)
# 色 (R, G, B, A)
COLOR_GLIDE_PATH = (0.0, 0.5, 1.0, 0.3) # 軌道(青・半透明)
COLOR_AIRPLANE = (1.0, 0.0, 0.0, 1.0) # 飛行機(赤)
COLOR_TARGET = (0.0, 1.0, 0.0, 1.0) # 着地点(緑)
COLOR_LINE = (1.0, 1.0, 0.0, 1.0) # 姿勢線分(黄)
# =========================================================
# 処理ロジック・コールバック
# =========================================================
def create_material(name, color):
mat = bpy.data.materials.get(name)
if mat is None:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
bsdf.inputs["Alpha"].default_value = color[3]
return mat
def update_airplane_and_line_pos(self, context):
sphere = bpy.data.objects.get(f"{PREFIX}_Airplane")
line = bpy.data.objects.get(f"{PREFIX}_Line")
if sphere and line:
x = self.current_x
angle_rad = math.radians(self.angle)
z = -x * math.tan(angle_rad)
sphere.location = (x, 0, z)
line.location = (x, 0, z)
pitch_rad = math.radians(self.line_pitch)
line.rotation_euler = (0, math.radians(90) + pitch_rad, 0)
def update_glide_path(self, context):
obj = bpy.data.objects.get(f"{PREFIX}_GlidePath")
if obj:
start_x = START_X_DEFAULT
angle_rad = math.radians(self.angle)
start_z = -start_x * math.tan(angle_rad)
obj.location = (start_x / 2.0, 0, start_z / 2.0)
length = math.sqrt(start_x**2 + start_z**2)
radius = self.cyl_radius
obj.scale = (radius, radius, length / 2.0)
Ry = math.atan2(-start_x, -start_z)
obj.rotation_euler = (0, Ry, 0)
update_airplane_and_line_pos(self, context)
def update_cyl_scale(self, context):
obj = bpy.data.objects.get(f"{PREFIX}_GlidePath")
if obj:
obj.scale[0] = self.cyl_radius
obj.scale[1] = self.cyl_radius
def update_sph_scale(self, context):
obj = bpy.data.objects.get(f"{PREFIX}_Airplane")
if obj:
r = self.sph_radius
obj.scale = (r, r, r)
def update_line_scale(self, context):
obj = bpy.data.objects.get(f"{PREFIX}_Line")
if obj:
t = self.line_thickness
obj.scale = (t, t, self.line_length / 2.0)
def update_colors(self, context):
mats = {
f"{PREFIX}_GlidePath_Mat": self.cyl_color,
f"{PREFIX}_Airplane_Mat": self.sph_color,
f"{PREFIX}_Line_Mat": self.line_color
}
for mat_name, color in mats.items():
mat = bpy.data.materials.get(mat_name)
if mat and mat.use_nodes:
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
bsdf.inputs["Alpha"].default_value = color[3]
# =========================================================
# プロパティグループ
# =========================================================
class AIR_LANDING_Properties(bpy.types.PropertyGroup):
current_x: bpy.props.FloatProperty(
name="現在位置 X", default=START_X_DEFAULT, min=START_X_DEFAULT, max=0.0, update=update_airplane_and_line_pos
)
angle: bpy.props.FloatProperty(
name="降下角度", default=LANDING_ANGLE_DEFAULT, min=1.0, max=89.0, update=update_glide_path
)
line_pitch: bpy.props.FloatProperty(
name="Y軸回転 (ピッチ)", default=LINE_PITCH_DEFAULT, min=-180.0, max=180.0, update=update_airplane_and_line_pos
)
cyl_radius: bpy.props.FloatProperty(
name="軌道の太さ", default=RADIUS_CYLINDER_DEFAULT, min=0.01, max=10.0, update=update_cyl_scale
)
sph_radius: bpy.props.FloatProperty(
name="球体の大きさ", default=RADIUS_SPHERE_DEFAULT, min=0.1, max=10.0, update=update_sph_scale
)
line_length: bpy.props.FloatProperty(
name="線分の全長", default=LENGTH_LINE_DEFAULT, min=0.1, max=50.0, update=update_line_scale
)
line_thickness: bpy.props.FloatProperty(
name="線分の太さ", default=THICKNESS_LINE_DEFAULT, min=0.01, max=5.0, update=update_line_scale
)
cyl_color: bpy.props.FloatVectorProperty(
name="軌道の色", subtype='COLOR', size=4, default=COLOR_GLIDE_PATH, update=update_colors, min=0.0, max=1.0
)
sph_color: bpy.props.FloatVectorProperty(
name="球体の色", subtype='COLOR', size=4, default=COLOR_AIRPLANE, update=update_colors, min=0.0, max=1.0
)
line_color: bpy.props.FloatVectorProperty(
name="線分の色", subtype='COLOR', size=4, default=COLOR_LINE, update=update_colors, min=0.0, max=1.0
)
# =========================================================
# オペレーター
# =========================================================
class AIR_LANDING_OT_copy_text(bpy.types.Operator):
bl_idname = f"{PREFIX}.copy_text"
bl_label = "コピー"
text_to_copy: bpy.props.StringProperty()
def execute(self, context):
context.window_manager.clipboard = self.text_to_copy
self.report({'INFO'}, f"コピーしました: {self.text_to_copy}")
return {'FINISHED'}
class AIR_LANDING_OT_create_scene(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_scene"
bl_label = "シーンを生成・リセット"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.object.select_all(action='DESELECT')
for obj in bpy.context.scene.objects:
if obj.name.startswith(f"{PREFIX}_"):
obj.select_set(True)
bpy.ops.object.delete()
props = context.scene.air_landing_props
# 1. 着地点
bpy.ops.mesh.primitive_uv_sphere_add(radius=RADIUS_TARGET_DEFAULT, align='WORLD', location=(0, 0, 0))
target = context.active_object
target.name = f"{PREFIX}_Target"
target.data.materials.append(create_material(f"{PREFIX}_Target_Mat", COLOR_TARGET))
# 2. 降下軌道
bpy.ops.mesh.primitive_cylinder_add(radius=1.0, depth=2.0, align='WORLD', location=(0,0,0))
cyl = context.active_object
cyl.name = f"{PREFIX}_GlidePath"
cyl.data.materials.append(create_material(f"{PREFIX}_GlidePath_Mat", COLOR_GLIDE_PATH))
# 3. 飛行機 / 現在位置
bpy.ops.mesh.primitive_uv_sphere_add(radius=1.0, align='WORLD', location=(0,0,0))
airplane = context.active_object
airplane.name = f"{PREFIX}_Airplane"
airplane.data.materials.append(create_material(f"{PREFIX}_Airplane_Mat", COLOR_AIRPLANE))
# 4. 機体姿勢(線分)
bpy.ops.mesh.primitive_cylinder_add(radius=1.0, depth=2.0, align='WORLD', location=(0,0,0))
line = context.active_object
line.name = f"{PREFIX}_Line"
line.data.materials.append(create_material(f"{PREFIX}_Line_Mat", COLOR_LINE))
# 初期値の適用
props.current_x = START_X_DEFAULT
props.line_pitch = LINE_PITCH_DEFAULT
update_glide_path(props, context)
update_cyl_scale(props, context)
update_sph_scale(props, context)
update_line_scale(props, context)
self.report({'INFO'}, "降下シミュレーションシーンを生成しました")
return {'FINISHED'}
# =========================================================
# UI (Nパネル)
# =========================================================
class AIR_LANDING_PT_main_panel(bpy.types.Panel):
bl_label = ADDON_TITLE
bl_idname = f"{PREFIX}_PT_main_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
props = context.scene.air_landing_props
col = layout.column(align=True)
col.operator(AIR_LANDING_OT_create_scene.bl_idname, icon='PLAY')
col.separator()
col.prop(props, "angle")
col.prop(props, "current_x")
col.separator()
# 姿勢(線分)操作
box_line = layout.box()
box_line.label(text="機体姿勢(黄色の線分):", icon='DRIVER_ROTATIONAL_DIFFERENCE')
box_line.prop(props, "line_pitch")
box_line.prop(props, "line_length")
box_line.prop(props, "line_thickness")
col.separator()
# --- 計算・情報表示ブロック (小数点1桁で統一) ---
x = props.current_x
angle_rad = math.radians(props.angle)
z = -x * math.tan(angle_rad)
# 中央(0,0,0)からの距離
dist_center = math.sqrt(x**2 + 0.0**2 + z**2)
# 中央(0,0,0)からの仰角/俯角
horiz_dist = abs(x)
angle_from_origin = math.degrees(math.atan2(z, horiz_dist)) if horiz_dist > 0 else 90.0
# 線分の両端座標の計算
total_pitch = math.radians(90.0 + props.line_pitch)
half_len = props.line_length / 2.0
dx = half_len * math.sin(total_pitch)
dz = half_len * math.cos(total_pitch)
tip1 = (x + dx, 0.0, z + dz)
tip2 = (x - dx, 0.0, z - dz)
dist_tip1 = math.sqrt(tip1[0]**2 + tip1[1]**2 + tip1[2]**2)
dist_tip2 = math.sqrt(tip2[0]**2 + tip2[1]**2 + tip2[2]**2)
# 小数点1桁にフォーマット
s_center = f"中央(0,0,0) 距離: {dist_center:.1f} | 角度: {angle_from_origin:.1f}°"
s_tip1 = f"先端1(0,0,0) 距離: {dist_tip1:.1f}"
s_tip2 = f"先端2(0,0,0) 距離: {dist_tip2:.1f}"
all_text = f"{s_center}\n{s_tip1}\n{s_tip2}"
box_info = layout.box()
box_info.label(text="位置・距離・角度情報:", icon='INFO')
# 1行ずつ表示 & 各行のコピーボタン
row1 = box_info.row()
row1.label(text=s_center)
op1 = row1.operator(AIR_LANDING_OT_copy_text.bl_idname, text="", icon='COPYDOWN')
op1.text_to_copy = s_center
row2 = box_info.row()
row2.label(text=s_tip1)
op2 = row2.operator(AIR_LANDING_OT_copy_text.bl_idname, text="", icon='COPYDOWN')
op2.text_to_copy = s_tip1
row3 = box_info.row()
row3.label(text=s_tip2)
op3 = row3.operator(AIR_LANDING_OT_copy_text.bl_idname, text="", icon='COPYDOWN')
op3.text_to_copy = s_tip2
# 一括コピーボタン
row_all = box_info.row()
op_all = row_all.operator(AIR_LANDING_OT_copy_text.bl_idname, text="一括コピー", icon='COPYDOWN')
op_all.text_to_copy = all_text
col.separator()
# 方程式表示ブロック
slope = math.tan(math.radians(props.angle))
eq_str = f"Z = {-slope:.1f} * X (X <= 0, Y = 0)"
box_eq = layout.box()
box_eq.label(text="軌道の方程式:")
row_eq = box_eq.row()
row_eq.label(text=eq_str)
op = row_eq.operator(AIR_LANDING_OT_copy_text.bl_idname, text="", icon='COPYDOWN')
op.text_to_copy = eq_str
# 見た目調整ブロック
box_style = layout.box()
box_style.label(text="見た目の調整", icon='COLOR')
r_c = box_style.row()
r_c.prop(props, "cyl_radius")
r_c.prop(props, "cyl_color", text="")
r_s = box_style.row()
r_s.prop(props, "sph_radius")
r_s.prop(props, "sph_color", text="")
r_l = box_style.row()
r_l.prop(props, "line_color", text="")
# =========================================================
# 登録・解除
# =========================================================
classes = (
AIR_LANDING_Properties,
AIR_LANDING_OT_copy_text,
AIR_LANDING_OT_create_scene,
AIR_LANDING_PT_main_panel,
)
def apply_startup_view_settings():
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.show_region_ui = True
space.shading.type = 'MATERIAL'
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.air_landing_props = bpy.props.PointerProperty(type=AIR_LANDING_Properties)
bpy.app.timers.register(apply_startup_view_settings, first_interval=0.1)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
del bpy.types.Scene.air_landing_props
if __name__ == "__main__":
register()
bl_info = {
"name": "Airplane Landing Visualizer",
"author": "ChatGPT",
"version": (1, 5, 0),
"blender": (5, 0, 0),
"location": "View3D > N Panel > 3D View",
"description": "線分の太さ調整および原点からの距離・角度情報の1行表示&一括コピー機能",
"warning": "",
"doc_url": "",
"category": "3D View",
}
import bpy
import math
import numpy as np
# 作成日 2026年07月21日
# ===================================
# 4. 共通定義
# ===================================
ADDON_TITLE = "Airplane Landing Visualizer"
PREFIX_VAL = "AIR_LANDING"
ADDON_CATEGORY = "3D View"
PREFIX = PREFIX_VAL.lower()
# =========================================================
# 6. 初期設定
# =========================================================
START_X_DEFAULT = -100.0
LANDING_ANGLE_DEFAULT = 30.0
RADIUS_CYLINDER_DEFAULT = 0.5
RADIUS_SPHERE_DEFAULT = 1.5
RADIUS_TARGET_DEFAULT = 2.0
LENGTH_LINE_DEFAULT = 2.0
THICKNESS_LINE_DEFAULT = 0.1
LINE_PITCH_DEFAULT = 0.0
COLOR_GLIDE_PATH = (0.0, 0.5, 1.0, 0.3)
COLOR_AIRPLANE = (1.0, 0.0, 0.0, 1.0)
COLOR_TARGET = (0.0, 1.0, 0.0, 1.0)
COLOR_LINE = (1.0, 1.0, 0.0, 1.0)
# =========================================================
# 処理ロジック・コールバック
# =========================================================
def create_material(name, color):
mat = bpy.data.materials.get(name)
if mat is None:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
bsdf.inputs["Alpha"].default_value = color[3]
return mat
def update_airplane_and_line_pos(self, context):
sphere = bpy.data.objects.get(f"{PREFIX}_Airplane")
line = bpy.data.objects.get(f"{PREFIX}_Line")
if sphere and line:
x = self.current_x
angle_rad = math.radians(self.angle)
z = -x * math.tan(angle_rad)
sphere.location = (x, 0, z)
line.location = (x, 0, z)
pitch_rad = math.radians(self.line_pitch)
line.rotation_euler = (0, math.radians(90) + pitch_rad, 0)
def update_glide_path(self, context):
obj = bpy.data.objects.get(f"{PREFIX}_GlidePath")
if obj:
start_x = START_X_DEFAULT
angle_rad = math.radians(self.angle)
start_z = -start_x * math.tan(angle_rad)
obj.location = (start_x / 2.0, 0, start_z / 2.0)
length = math.sqrt(start_x**2 + start_z**2)
radius = self.cyl_radius
obj.scale = (radius, radius, length / 2.0)
Ry = math.atan2(-start_x, -start_z)
obj.rotation_euler = (0, Ry, 0)
update_airplane_and_line_pos(self, context)
def update_cyl_scale(self, context):
obj = bpy.data.objects.get(f"{PREFIX}_GlidePath")
if obj:
obj.scale[0] = self.cyl_radius
obj.scale[1] = self.cyl_radius
def update_sph_scale(self, context):
obj = bpy.data.objects.get(f"{PREFIX}_Airplane")
if obj:
r = self.sph_radius
obj.scale = (r, r, r)
def update_line_scale(self, context):
obj = bpy.data.objects.get(f"{PREFIX}_Line")
if obj:
t = self.line_thickness
obj.scale = (t, t, self.line_length / 2.0)
def update_colors(self, context):
mats = {
f"{PREFIX}_GlidePath_Mat": self.cyl_color,
f"{PREFIX}_Airplane_Mat": self.sph_color,
f"{PREFIX}_Line_Mat": self.line_color
}
for mat_name, color in mats.items():
mat = bpy.data.materials.get(mat_name)
if mat and mat.use_nodes:
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
bsdf.inputs["Alpha"].default_value = color[3]
# =========================================================
# プロパティグループ
# =========================================================
class AIR_LANDING_Properties(bpy.types.PropertyGroup):
current_x: bpy.props.FloatProperty(
name="現在位置 X", default=START_X_DEFAULT, min=START_X_DEFAULT, max=0.0, update=update_airplane_and_line_pos
)
angle: bpy.props.FloatProperty(
name="降下角度", default=LANDING_ANGLE_DEFAULT, min=1.0, max=89.0, update=update_glide_path
)
line_pitch: bpy.props.FloatProperty(
name="Y軸回転 (ピッチ)", default=LINE_PITCH_DEFAULT, min=-180.0, max=180.0, update=update_airplane_and_line_pos
)
cyl_radius: bpy.props.FloatProperty(
name="軌道の太さ", default=RADIUS_CYLINDER_DEFAULT, min=0.01, max=10.0, update=update_cyl_scale
)
sph_radius: bpy.props.FloatProperty(
name="球体の大きさ", default=RADIUS_SPHERE_DEFAULT, min=0.1, max=10.0, update=update_sph_scale
)
line_length: bpy.props.FloatProperty(
name="線分の全長", default=LENGTH_LINE_DEFAULT, min=0.1, max=20.0, update=update_line_scale
)
line_thickness: bpy.props.FloatProperty(
name="線分の太さ", default=THICKNESS_LINE_DEFAULT, min=0.01, max=2.0, update=update_line_scale
)
cyl_color: bpy.props.FloatVectorProperty(
name="軌道の色", subtype='COLOR', size=4, default=COLOR_GLIDE_PATH, update=update_colors, min=0.0, max=1.0
)
sph_color: bpy.props.FloatVectorProperty(
name="球体の色", subtype='COLOR', size=4, default=COLOR_AIRPLANE, update=update_colors, min=0.0, max=1.0
)
line_color: bpy.props.FloatVectorProperty(
name="線分の色", subtype='COLOR', size=4, default=COLOR_LINE, update=update_colors, min=0.0, max=1.0
)
# =========================================================
# オペレーター
# =========================================================
class AIR_LANDING_OT_copy_text(bpy.types.Operator):
bl_idname = f"{PREFIX}.copy_text"
bl_label = "コピー"
text_to_copy: bpy.props.StringProperty()
def execute(self, context):
context.window_manager.clipboard = self.text_to_copy
self.report({'INFO'}, f"コピーしました: {self.text_to_copy}")
return {'FINISHED'}
class AIR_LANDING_OT_create_scene(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_scene"
bl_label = "シーンを生成・リセット"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.object.select_all(action='DESELECT')
for obj in bpy.context.scene.objects:
if obj.name.startswith(f"{PREFIX}_"):
obj.select_set(True)
bpy.ops.object.delete()
props = context.scene.air_landing_props
# 1. 着地点
bpy.ops.mesh.primitive_uv_sphere_add(radius=RADIUS_TARGET_DEFAULT, align='WORLD', location=(0, 0, 0))
target = context.active_object
target.name = f"{PREFIX}_Target"
target.data.materials.append(create_material(f"{PREFIX}_Target_Mat", COLOR_TARGET))
# 2. 降下軌道
bpy.ops.mesh.primitive_cylinder_add(radius=1.0, depth=2.0, align='WORLD', location=(0,0,0))
cyl = context.active_object
cyl.name = f"{PREFIX}_GlidePath"
cyl.data.materials.append(create_material(f"{PREFIX}_GlidePath_Mat", COLOR_GLIDE_PATH))
# 3. 飛行機 / 現在位置
bpy.ops.mesh.primitive_uv_sphere_add(radius=1.0, align='WORLD', location=(0,0,0))
airplane = context.active_object
airplane.name = f"{PREFIX}_Airplane"
airplane.data.materials.append(create_material(f"{PREFIX}_Airplane_Mat", COLOR_AIRPLANE))
# 4. 機体姿勢(線分)
bpy.ops.mesh.primitive_cylinder_add(radius=1.0, depth=2.0, align='WORLD', location=(0,0,0))
line = context.active_object
line.name = f"{PREFIX}_Line"
line.data.materials.append(create_material(f"{PREFIX}_Line_Mat", COLOR_LINE))
# 初期値の適用
props.current_x = START_X_DEFAULT
props.line_pitch = LINE_PITCH_DEFAULT
update_glide_path(props, context)
update_cyl_scale(props, context)
update_sph_scale(props, context)
update_line_scale(props, context)
self.report({'INFO'}, "降下シミュレーションシーンを生成しました")
return {'FINISHED'}
# =========================================================
# UI (Nパネル)
# =========================================================
class AIR_LANDING_PT_main_panel(bpy.types.Panel):
bl_label = ADDON_TITLE
bl_idname = f"{PREFIX}_PT_main_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
props = context.scene.air_landing_props
col = layout.column(align=True)
col.operator(AIR_LANDING_OT_create_scene.bl_idname, icon='PLAY')
col.separator()
col.prop(props, "angle")
col.prop(props, "current_x")
col.separator()
# 姿勢(線分)操作
box_line = layout.box()
box_line.label(text="機体姿勢(黄色の線分):", icon='DRIVER_ROTATIONAL_DIFFERENCE')
box_line.prop(props, "line_pitch")
box_line.prop(props, "line_length")
box_line.prop(props, "line_thickness")
col.separator()
# --- 計算・情報表示ブロック (0,0,0からの距離・両端距離・角度) ---
x = props.current_x
angle_rad = math.radians(props.angle)
z = -x * math.tan(angle_rad)
# 中央(0,0,0)からの距離
dist_center = math.sqrt(x**2 + 0.0**2 + z**2)
# 中央(0,0,0)からの仰角/俯角 (水平面からの角度)
# 垂直距離と水平距離から算出
horiz_dist = abs(x)
angle_from_origin = math.degrees(math.atan2(z, horiz_dist)) if horiz_dist > 0 else 90.0
# 線分の両端座標の計算
# 線分の中央位置: (x, 0, z)
# 線分の方向ベクトル(Y軸回転ピッチ + X軸平行方向)
total_pitch = math.radians(90.0 + props.line_pitch)
half_len = props.line_length / 2.0
# ローカルの方向ベクトル (回転後)
# ピッチ角はY軸まわり。X成分 = -cos(pitch), Z成分 = sin(pitch) のような関係
# ※正確な端点座標
dx = half_len * math.sin(total_pitch) # ピッチ回転によるX方向のオフセット
dz = half_len * math.cos(total_pitch) # ピッチ回転によるZ方向のオフセット
tip1 = (x + dx, 0.0, z + dz)
tip2 = (x - dx, 0.0, z - dz)
dist_tip1 = math.sqrt(tip1[0]**2 + tip1[1]**2 + tip1[2]**2)
dist_tip2 = math.sqrt(tip2[0]**2 + tip2[1]**2 + tip2[2]**2)
# 文字列データの作成
s_center = f"中央(0,0,0)距離: {dist_center:.3f} | 角度: {angle_from_origin:.2f}°"
s_tip1 = f"先端1(0,0,0)距離: {dist_tip1:.3f}"
s_tip2 = f"先端2(0,0,0)距離: {dist_tip2:.3f}"
all_text = f"{s_center}\n{s_tip1}\n{s_tip2}"
box_info = layout.box()
box_info.label(text="位置・距離・角度情報:", icon='INFO')
# 1行ずつ表示 & 各行のコピーボタン
row1 = box_info.row()
row1.label(text=s_center)
op1 = row1.operator(AIR_LANDING_OT_copy_text.bl_idname, text="", icon='COPYDOWN')
op1.text_to_copy = s_center
row2 = box_info.row()
row2.label(text=s_tip1)
op2 = row2.operator(AIR_LANDING_OT_copy_text.bl_idname, text="", icon='COPYDOWN')
op2.text_to_copy = s_tip1
row3 = box_info.row()
row3.label(text=s_tip2)
op3 = row3.operator(AIR_LANDING_OT_copy_text.bl_idname, text="", icon='COPYDOWN')
op3.text_to_copy = s_tip2
# 一括コピーボタン
row_all = box_info.row()
op_all = row_all.operator(AIR_LANDING_OT_copy_text.bl_idname, text="一括コピー", icon='COPYDOWN')
op_all.text_to_copy = all_text
col.separator()
# 方程式表示ブロック
slope = math.tan(math.radians(props.angle))
eq_str = f"Z = {-slope:.4f} * X (X <= 0, Y = 0)"
box_eq = layout.box()
box_eq.label(text="軌道の方程式:")
row_eq = box_eq.row()
row_eq.label(text=eq_str)
op = row_eq.operator(AIR_LANDING_OT_copy_text.bl_idname, text="", icon='COPYDOWN')
op.text_to_copy = eq_str
# 見た目調整ブロック
box_style = layout.box()
box_style.label(text="見た目の調整", icon='COLOR')
r_c = box_style.row()
r_c.prop(props, "cyl_radius")
r_c.prop(props, "cyl_color", text="")
r_s = box_style.row()
r_s.prop(props, "sph_radius")
r_s.prop(props, "sph_color", text="")
r_l = box_style.row()
r_l.prop(props, "line_color", text="")
# =========================================================
# 登録・解除
# =========================================================
classes = (
AIR_LANDING_Properties,
AIR_LANDING_OT_copy_text,
AIR_LANDING_OT_create_scene,
AIR_LANDING_PT_main_panel,
)
def apply_startup_view_settings():
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.show_region_ui = True
space.shading.type = 'MATERIAL'
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.air_landing_props = bpy.props.PointerProperty(type=AIR_LANDING_Properties)
bpy.app.timers.register(apply_startup_view_settings, first_interval=0.1)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
del bpy.types.Scene.air_landing_props
if __name__ == "__main__":
register()
ご指摘ありがとうございます。黄色の線分(姿勢)が誤って降下線に沿ってしまっていたように見えた問題(あるいは回転軸の挙動)を修正し、「初期値は完全にX軸と平行(水平)」になるようロジックを見直しました。
また、円柱の生成時の原点(ジオメトリの中央)を利用することで、「線分の中央を軸としてY軸回転する専用スライダー」として独立・明確化しました。
運用ルールに従い、まずは初期設定ブロックを出力します。
---
## 1. 初期設定ブロック(コピー用)
機体姿勢用の角度設定変数 `LINE_PITCH_DEFAULT` を明記し、0.0度(X軸平行)を初期値としています。
```python
# =========================================================
# 6. 初期設定
# =========================================================
# 数値・座標
START_X_DEFAULT = -100.0 # 開始地点のX座標
LANDING_ANGLE_DEFAULT = 30.0 # 初期降下角度(度)
# サイズ (半径・長さ)
RADIUS_CYLINDER_DEFAULT = 0.5 # 降下軌道(円柱)の太さ
RADIUS_SPHERE_DEFAULT = 1.5 # 飛行機(現在位置・球体)の大きさ
RADIUS_TARGET_DEFAULT = 2.0 # 着地点の大きさ
LENGTH_LINE_DEFAULT = 2.0 # 線分の初期全長
# 角度
LINE_PITCH_DEFAULT = 0.0 # 線分の初期Y軸回転角度(0.0でX軸に完全平行)
# 色 (R, G, B, A)
# ※ A(アルファ)を1.0未満にすると半透明になります
COLOR_GLIDE_PATH = (0.0, 0.5, 1.0, 0.3) # 軌道(青・半透明)
COLOR_AIRPLANE = (1.0, 0.0, 0.0, 1.0) # 飛行機(赤)
COLOR_TARGET = (0.0, 1.0, 0.0, 1.0) # 着地点(緑)
COLOR_LINE = (1.0, 1.0, 0.0, 1.0) # 姿勢線分(黄)
NパネルのUI上で「機体姿勢(線分)」の回転スライダーを独立した枠(ボックス)で囲み、スライダーを動かすと線分の中央を軸にしてY軸回転するように調整しています。
bl_info = {
"name": "Airplane Landing Visualizer",
"author": "ChatGPT",
"version": (1, 4, 0),
"blender": (5, 0, 0),
"location": "View3D > N Panel > 3D View",
"description": "線分のX軸平行化およびY軸回転(中央軸)スライダーを実装",
"warning": "",
"doc_url": "",
"category": "3D View",
}
import bpy
import math
# 作成日 2026年07月21日
# ===================================
# 4. 共通定義
# ===================================
ADDON_TITLE = "Airplane Landing Visualizer"
PREFIX_VAL = "AIR_LANDING"
ADDON_CATEGORY = "3D View"
PREFIX = PREFIX_VAL.lower()
# =========================================================
# 6. 初期設定
# =========================================================
START_X_DEFAULT = -100.0
LANDING_ANGLE_DEFAULT = 30.0
RADIUS_CYLINDER_DEFAULT = 0.5
RADIUS_SPHERE_DEFAULT = 1.5
RADIUS_TARGET_DEFAULT = 2.0
LENGTH_LINE_DEFAULT = 2.0
LINE_PITCH_DEFAULT = 0.0
COLOR_GLIDE_PATH = (0.0, 0.5, 1.0, 0.3)
COLOR_AIRPLANE = (1.0, 0.0, 0.0, 1.0)
COLOR_TARGET = (0.0, 1.0, 0.0, 1.0)
COLOR_LINE = (1.0, 1.0, 0.0, 1.0)
# =========================================================
# 処理ロジック・コールバック
# =========================================================
def create_material(name, color):
mat = bpy.data.materials.get(name)
if mat is None:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
bsdf.inputs["Alpha"].default_value = color[3]
return mat
def update_airplane_and_line_pos(self, context):
"""球体と線分の位置・回転を更新する"""
sphere = bpy.data.objects.get(f"{PREFIX}_Airplane")
line = bpy.data.objects.get(f"{PREFIX}_Line")
if sphere and line:
x = self.current_x
angle_rad = math.radians(self.angle)
z = -x * math.tan(angle_rad)
# 球体を軌道上に移動
sphere.location = (x, 0, z)
# 線分(極細の円柱)を球体の中央に配置
line.location = (x, 0, z)
# 【修正箇所】
# デフォルトの円柱はZ軸方向に伸びているため、Y軸で90度(pi/2)回転させると完全にX軸と平行になります。
# そこから、スライダーの値(line_pitch)分だけY軸周りで回転(ピッチ回転)させます。
pitch_rad = math.radians(self.line_pitch)
line.rotation_euler = (0, math.radians(90) + pitch_rad, 0)
def update_glide_path(self, context):
obj = bpy.data.objects.get(f"{PREFIX}_GlidePath")
if obj:
start_x = START_X_DEFAULT
angle_rad = math.radians(self.angle)
start_z = -start_x * math.tan(angle_rad)
obj.location = (start_x / 2.0, 0, start_z / 2.0)
length = math.sqrt(start_x**2 + start_z**2)
radius = self.cyl_radius
obj.scale = (radius, radius, length / 2.0)
Ry = math.atan2(-start_x, -start_z)
obj.rotation_euler = (0, Ry, 0)
update_airplane_and_line_pos(self, context)
def update_cyl_scale(self, context):
obj = bpy.data.objects.get(f"{PREFIX}_GlidePath")
if obj:
obj.scale[0] = self.cyl_radius
obj.scale[1] = self.cyl_radius
def update_sph_scale(self, context):
obj = bpy.data.objects.get(f"{PREFIX}_Airplane")
if obj:
r = self.sph_radius
obj.scale = (r, r, r)
def update_line_scale(self, context):
obj = bpy.data.objects.get(f"{PREFIX}_Line")
if obj:
# デフォルト円柱(深さ2.0)に対するZ軸スケールで長さを調整
obj.scale = (0.1, 0.1, self.line_length / 2.0)
def update_colors(self, context):
mats = {
f"{PREFIX}_GlidePath_Mat": self.cyl_color,
f"{PREFIX}_Airplane_Mat": self.sph_color,
f"{PREFIX}_Line_Mat": self.line_color
}
for mat_name, color in mats.items():
mat = bpy.data.materials.get(mat_name)
if mat and mat.use_nodes:
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
bsdf.inputs["Alpha"].default_value = color[3]
# =========================================================
# プロパティグループ
# =========================================================
class AIR_LANDING_Properties(bpy.types.PropertyGroup):
current_x: bpy.props.FloatProperty(
name="現在位置 X", default=START_X_DEFAULT, min=START_X_DEFAULT, max=0.0, update=update_airplane_and_line_pos
)
angle: bpy.props.FloatProperty(
name="降下角度", default=LANDING_ANGLE_DEFAULT, min=1.0, max=89.0, update=update_glide_path
)
line_pitch: bpy.props.FloatProperty(
name="Y軸回転 (ピッチ)", default=LINE_PITCH_DEFAULT, min=-180.0, max=180.0, update=update_airplane_and_line_pos
)
cyl_radius: bpy.props.FloatProperty(
name="軌道の太さ", default=RADIUS_CYLINDER_DEFAULT, min=0.01, max=10.0, update=update_cyl_scale
)
sph_radius: bpy.props.FloatProperty(
name="球体の大きさ", default=RADIUS_SPHERE_DEFAULT, min=0.1, max=10.0, update=update_sph_scale
)
line_length: bpy.props.FloatProperty(
name="線分の全長", default=LENGTH_LINE_DEFAULT, min=0.1, max=20.0, update=update_line_scale
)
cyl_color: bpy.props.FloatVectorProperty(
name="軌道の色", subtype='COLOR', size=4, default=COLOR_GLIDE_PATH, update=update_colors, min=0.0, max=1.0
)
sph_color: bpy.props.FloatVectorProperty(
name="球体の色", subtype='COLOR', size=4, default=COLOR_AIRPLANE, update=update_colors, min=0.0, max=1.0
)
line_color: bpy.props.FloatVectorProperty(
name="線分の色", subtype='COLOR', size=4, default=COLOR_LINE, update=update_colors, min=0.0, max=1.0
)
# =========================================================
# オペレーター
# =========================================================
class AIR_LANDING_OT_copy_equation(bpy.types.Operator):
bl_idname = f"{PREFIX}.copy_equation"
bl_label = "コピー"
equation_text: bpy.props.StringProperty()
def execute(self, context):
context.window_manager.clipboard = self.equation_text
self.report({'INFO'}, f"コピー完了: {self.equation_text}")
return {'FINISHED'}
class AIR_LANDING_OT_create_scene(bpy.types.Operator):
bl_idname = f"{PREFIX}.create_scene"
bl_label = "シーンを生成・リセット"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
# 既存オブジェクトの削除
bpy.ops.object.select_all(action='DESELECT')
for obj in bpy.context.scene.objects:
if obj.name.startswith(f"{PREFIX}_"):
obj.select_set(True)
bpy.ops.object.delete()
props = context.scene.air_landing_props
# 1. 着地点
bpy.ops.mesh.primitive_uv_sphere_add(radius=RADIUS_TARGET_DEFAULT, align='WORLD', location=(0, 0, 0))
target = context.active_object
target.name = f"{PREFIX}_Target"
target.data.materials.append(create_material(f"{PREFIX}_Target_Mat", COLOR_TARGET))
# 2. 降下軌道
bpy.ops.mesh.primitive_cylinder_add(radius=1.0, depth=2.0, align='WORLD', location=(0,0,0))
cyl = context.active_object
cyl.name = f"{PREFIX}_GlidePath"
cyl.data.materials.append(create_material(f"{PREFIX}_GlidePath_Mat", COLOR_GLIDE_PATH))
# 3. 飛行機 / 現在位置
bpy.ops.mesh.primitive_uv_sphere_add(radius=1.0, align='WORLD', location=(0,0,0))
airplane = context.active_object
airplane.name = f"{PREFIX}_Airplane"
airplane.data.materials.append(create_material(f"{PREFIX}_Airplane_Mat", COLOR_AIRPLANE))
# 4. 機体姿勢(線分:原点は中央のまま生成される)
bpy.ops.mesh.primitive_cylinder_add(radius=1.0, depth=2.0, align='WORLD', location=(0,0,0))
line = context.active_object
line.name = f"{PREFIX}_Line"
line.data.materials.append(create_material(f"{PREFIX}_Line_Mat", COLOR_LINE))
# 初期値の適用とUIの連動更新
props.current_x = START_X_DEFAULT
props.line_pitch = LINE_PITCH_DEFAULT
update_glide_path(props, context)
update_cyl_scale(props, context)
update_sph_scale(props, context)
update_line_scale(props, context)
self.report({'INFO'}, "降下シミュレーションシーンを生成しました")
return {'FINISHED'}
# =========================================================
# UI (Nパネル)
# =========================================================
class AIR_LANDING_PT_main_panel(bpy.types.Panel):
bl_label = ADDON_TITLE
bl_idname = f"{PREFIX}_PT_main_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = ADDON_CATEGORY
def draw(self, context):
layout = self.layout
props = context.scene.air_landing_props
col = layout.column(align=True)
col.operator(AIR_LANDING_OT_create_scene.bl_idname, icon='PLAY')
col.separator()
# 全体の動きに関する設定
col.prop(props, "angle")
col.prop(props, "current_x")
col.separator()
# 【追加】姿勢(線分)専用の回転操作ブロック
box_line = layout.box()
box_line.label(text="機体姿勢(黄色の線分):", icon='DRIVER_ROTATIONAL_DIFFERENCE')
box_line.prop(props, "line_pitch")
col.separator()
# 方程式表示ブロック
slope = math.tan(math.radians(props.angle))
eq_str = f"Z = {-slope:.4f} * X (X <= 0, Y = 0)"
box_eq = layout.box()
box_eq.label(text="軌道の方程式:")
row_eq = box_eq.row()
row_eq.label(text=eq_str)
op = row_eq.operator(AIR_LANDING_OT_copy_equation.bl_idname, text="", icon='COPYDOWN')
op.equation_text = eq_str
# 見た目調整ブロック
box_style = layout.box()
box_style.label(text="見た目の調整", icon='COLOR')
row1 = box_style.row()
row1.prop(props, "cyl_radius")
row1.prop(props, "cyl_color", text="")
row2 = box_style.row()
row2.prop(props, "sph_radius")
row2.prop(props, "sph_color", text="")
row3 = box_style.row()
row3.prop(props, "line_length")
row3.prop(props, "line_color", text="")
# =========================================================
# 登録・解除
# =========================================================
classes = (
AIR_LANDING_Properties,
AIR_LANDING_OT_copy_equation,
AIR_LANDING_OT_create_scene,
AIR_LANDING_PT_main_panel,
)
def apply_startup_view_settings():
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.show_region_ui = True
space.shading.type = 'MATERIAL'
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.air_landing_props = bpy.props.PointerProperty(type=AIR_LANDING_Properties)
bpy.app.timers.register(apply_startup_view_settings, first_interval=0.1)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
del bpy.types.Scene.air_landing_props
if __name__ == "__main__":
register()