prefix 一次方程式 楕円変形トーラス20260408 (1)
# Copied: 2026-04-08 12:00:00
import bpy
import time
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime
# ==============================================================================
# 【 基本設定エリア 】
# ==============================================================================
PREFIX = "EqGen"
ADDON_NAME = "[ Equation Gen ]"
TAB_NAME = "[ Equation Gen ]"
PANEL_TITLE = "Equation Lines"
AUTHOR = "zionadchat"
# ★ このスクリプト自身のID(絶対に消さないこと)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: EQ_LINES_2026_04_08_V6_1 ###"
# ==============================================================================
# システム初期化 & ID管理
# ==============================================================================
bl_info = {
"name": f"{ADDON_NAME} {PREFIX}",
"author": AUTHOR,
"version": (6, 1, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": "Lines, Torus, Elliptic Torus, and Cross Cylinders (z=x)",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
ADDON_LINKS = (
{"label": "Prefix 20260408", "url": "<https://www.notion.so/Prefix-20260408-33cf5dacaf43807e9e35ff8cdbbc39c6>"},
)
# ==============================================================================
# デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
# Line
"enable_preview": False,
"val_a": 0.6000, "val_b": 1.0000, "val_d": 10.0000,
"x_min": -50.0, "x_max": 50.0,
"y_min": -50.0, "y_max": 50.0,
"z_min": -50.0, "z_max": 50.0,
"thickness": 0.5000, "draw_plane": "XZ",
"show_eq1": True, "show_eq2": True, "show_eq3": True,
"color1": (1.0000, 0.2000, 0.2000, 1.0000),
"color2": (0.2000, 1.0000, 0.2000, 1.0000),
"color3": (0.2000, 0.2000, 1.0000, 1.0000),
# Torus 1 (Normal)
"t1_enable_preview": False,
"t1_mode": "INTERVAL",
"t1_val_a": 0.6000, "t1_val_b": 1.0000,
"t1_z_min": -50.0, "t1_z_max": 50.0, "t1_count": 11,
"t1_z_center": 0.0, "t1_z_interval": 1.0, "t1_up_down_count": 5,
"t1_major_radius": 5.0, "t1_minor_radius": 1.0,
"t1_color": (0.2000, 0.8000, 0.8000, 1.0000),
# Torus 2 (Elliptic / Lorentz)
"t2_enable_preview": False,
"t2_mode": "INTERVAL",
"t2_val_a": 0.6000, "t2_val_b": 1.0000,
"t2_z_min": -50.0, "t2_z_max": 50.0, "t2_count": 11,
"t2_z_center": 0.0, "t2_z_interval": 1.0, "t2_up_down_count": 5,
"t2_major_radius": 5.0, "t2_minor_radius": 1.0,
"t2_f": 1.0000, "t2_g": 1.6000, # 収縮率 f/g
"t2_color": (0.8000, 0.2000, 0.8000, 1.0000),
# Cross 1 (z=x, z=-x)
"c1_enable_preview": False,
"c1_center": (0.0, 0.0, 0.0),
"c1_length": 50.0, "c1_thickness": 0.5000,
"c1_color1": (1.0000, 1.0000, 0.2000, 1.0000),
"c1_color2": (1.0000, 0.5000, 0.2000, 1.0000),
# Cross 2 (z=x, z=-x)
"c2_enable_preview": False,
"c2_center": (10.0, 0.0, 10.0),
"c2_length": 30.0, "c2_thickness": 0.5000,
"c2_color1": (0.2000, 1.0000, 0.2000, 1.0000),
"c2_color2": (0.2000, 0.5000, 1.0000, 1.0000),
}
# <END_DICT>
PREVIEW_COL_LINE = f"{PREFIX}_Line_Preview"
PREVIEW_COL_TORUS1 = f"{PREFIX}_Torus1_Preview"
PREVIEW_COL_TORUS2 = f"{PREFIX}_Torus2_Preview"
PREVIEW_COL_CROSS1 = f"{PREFIX}_Cross1_Preview"
PREVIEW_COL_CROSS2 = f"{PREFIX}_Cross2_Preview"
# ==============================================================================
# 共通マテリアル・データ管理 ロジック
# ==============================================================================
def cleanup_preview_data():
for name in [PREVIEW_COL_LINE, PREVIEW_COL_TORUS1, PREVIEW_COL_TORUS2, PREVIEW_COL_CROSS1, PREVIEW_COL_CROSS2]:
col = bpy.data.collections.get(name)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0:
if isinstance(data, bpy.types.Curve):
bpy.data.curves.remove(data)
if len(col.objects) == 0:
bpy.data.collections.remove(col)
def apply_material_settings(mat, color):
mat.use_nodes = True
mat.blend_method = 'BLEND'
mat.diffuse_color = color
tree = mat.node_tree
bsdf = tree.nodes.get("Principled BSDF")
if not bsdf:
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.name = "Principled BSDF"
out = tree.nodes.new("ShaderNodeOutputMaterial")
tree.links.new(bsdf.outputs[0], out.inputs[0])
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
def get_preview_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name=name)
apply_material_settings(mat, color)
return mat
def build_curve_circle(curve, radius, segments=32):
if len(curve.splines) == 0:
spline = curve.splines.new('POLY')
spline.points.add(segments - 1)
spline.use_cyclic_u = True
else:
spline = curve.splines[0]
if len(spline.points) != segments:
curve.splines.clear()
spline = curve.splines.new('POLY')
spline.points.add(segments - 1)
spline.use_cyclic_u = True
for i in range(segments):
angle = 2 * math.pi * i / segments
spline.points[i].co = (radius * math.cos(angle), radius * math.sin(angle), 0.0, 1.0)
# ==============================================================================
# Line プレビューロジック
# ==============================================================================
def get_clipped_segment(m, c, x_min, x_max, v_min, v_max):
if m == 0:
if v_min <= c <= v_max: return x_min, x_max
return None, None
else:
x_from_v1, x_from_v2 = (v_min - c) / m, (v_max - c) / m
valid_x_min, valid_x_max = min(x_from_v1, x_from_v2), max(x_from_v1, x_from_v2)
act_x_min, act_x_max = max(x_min, valid_x_min), min(x_max, valid_x_max)
if act_x_min > act_x_max: return None, None
return act_x_min, act_x_max
def calc_points(props, m, c):
x_min, x_max = min(props.x_min, props.x_max), max(props.x_min, props.x_max)
y_min, y_max = min(props.y_min, props.y_max), max(props.y_min, props.y_max)
z_min, z_max = min(props.z_min, props.z_max), max(props.z_min, props.z_max)
if props.draw_plane == 'XZ':
act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, z_min, z_max)
if act_x_min is None: return None, None
return (act_x_min, 0.0, m * act_x_min + c), (act_x_max, 0.0, m * act_x_max + c)
else:
act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, y_min, y_max)
if act_x_min is None: return None, None
return (act_x_min, m * act_x_min + c, 0.0), (act_x_max, m * act_x_max + c, 0.0)
def update_line_preview(context, props):
if not props.enable_preview:
col = bpy.data.collections.get(PREVIEW_COL_LINE)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
bpy.data.collections.remove(col)
return
col = bpy.data.collections.get(PREVIEW_COL_LINE)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_LINE)
context.scene.collection.children.link(col)
a = props.val_a if abs(props.val_a) > 0.0001 else 0.0001
b, d = props.val_b, props.val_d
m = b / a
equations = [
{"id": 1, "show": props.show_eq1, "color": props.color1, "offset": 0.0},
{"id": 2, "show": props.show_eq2, "color": props.color2, "offset": -d},
{"id": 3, "show": props.show_eq3, "color": props.color3, "offset": d},
]
for eq in equations:
obj_name = f"[Preview] EqLine_{eq['id']}"
obj = bpy.data.objects.get(obj_name)
if not eq["show"]:
if obj: obj.hide_viewport = obj.hide_render = True
continue
p1, p2 = calc_points(props, m, eq["offset"])
if p1 is None:
if obj: obj.hide_viewport = obj.hide_render = True
continue
if not obj:
curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
curve.dimensions = '3D'; curve.fill_mode = 'FULL'
spline = curve.splines.new('POLY'); spline.points.add(1)
obj = bpy.data.objects.new(obj_name, curve)
col.objects.link(obj)
else:
curve = obj.data; spline = curve.splines[0]
curve.bevel_depth = props.thickness; curve.bevel_resolution = 6
spline.points[0].co = (*p1, 1.0); spline.points[1].co = (*p2, 1.0)
obj.hide_viewport = obj.hide_render = False
mat = get_preview_material(f"Preview_Mat_EqLine{eq['id']}", eq["color"])
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
# ==============================================================================
# Torus プレビューロジック (Normal & Elliptic)
# ==============================================================================
def update_torus1_preview(context, props):
if not props.t1_enable_preview:
col = bpy.data.collections.get(PREVIEW_COL_TORUS1)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
bpy.data.collections.remove(col)
return
col = bpy.data.collections.get(PREVIEW_COL_TORUS1)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_TORUS1)
context.scene.collection.children.link(col)
a = props.t1_val_a
b_val = props.t1_val_b if abs(props.t1_val_b) > 0.0001 else 0.0001
z_list = []
if props.t1_mode == 'RANGE':
count = props.t1_count
for i in range(count):
t = i / (count - 1) if count > 1 else 0.5
z_list.append(props.t1_z_min + t * (props.t1_z_max - props.t1_z_min))
else:
c, interval, ud = props.t1_z_center, props.t1_z_interval, props.t1_up_down_count
for i in range(-ud, ud + 1): z_list.append(c + i * interval)
existing_objs = list(col.objects)
mat = get_preview_material("Preview_Mat_Torus1", props.t1_color)
for i, z in enumerate(z_list):
obj_name = f"[Preview] Normal_Torus_{i+1}"
x = z * (a / b_val); y = 0.0
if i < len(existing_objs):
obj = existing_objs[i]
curve = obj.data
else:
curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
curve.dimensions = '3D'; curve.fill_mode = 'FULL'
obj = bpy.data.objects.new(obj_name, curve)
col.objects.link(obj)
curve.bevel_depth = props.t1_minor_radius; curve.bevel_resolution = 8
build_curve_circle(curve, props.t1_major_radius)
obj.location = (x, y, z); obj.scale = (1.0, 1.0, 1.0)
obj.hide_viewport = obj.hide_render = False
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
if len(existing_objs) > len(z_list):
for obj in existing_objs[len(z_list):]:
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
def update_torus2_preview(context, props):
if not props.t2_enable_preview:
col = bpy.data.collections.get(PREVIEW_COL_TORUS2)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
bpy.data.collections.remove(col)
return
col = bpy.data.collections.get(PREVIEW_COL_TORUS2)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_TORUS2)
context.scene.collection.children.link(col)
a = props.t2_val_a
b_val = props.t2_val_b if abs(props.t2_val_b) > 0.0001 else 0.0001
# 収縮率計算
g_val = props.t2_g if abs(props.t2_g) > 0.0001 else 0.0001
scale_x = props.t2_f / g_val
z_list = []
if props.t2_mode == 'RANGE':
count = props.t2_count
for i in range(count):
t = i / (count - 1) if count > 1 else 0.5
z_list.append(props.t2_z_min + t * (props.t2_z_max - props.t2_z_min))
else:
c, interval, ud = props.t2_z_center, props.t2_z_interval, props.t2_up_down_count
for i in range(-ud, ud + 1): z_list.append(c + i * interval)
existing_objs = list(col.objects)
mat = get_preview_material("Preview_Mat_Torus2", props.t2_color)
for i, z in enumerate(z_list):
obj_name = f"[Preview] Elliptic_Torus_{i+1}"
x = z * (a / b_val); y = 0.0
if i < len(existing_objs):
obj = existing_objs[i]
curve = obj.data
else:
curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
curve.dimensions = '3D'; curve.fill_mode = 'FULL'
obj = bpy.data.objects.new(obj_name, curve)
col.objects.link(obj)
curve.bevel_depth = props.t2_minor_radius; curve.bevel_resolution = 8
build_curve_circle(curve, props.t2_major_radius)
obj.location = (x, y, z)
obj.scale = (scale_x, 1.0, 1.0) # Lorentz Contraction
obj.hide_viewport = obj.hide_render = False
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
if len(existing_objs) > len(z_list):
for obj in existing_objs[len(z_list):]:
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
# ==============================================================================
# Cross (z=x, z=-x) プレビューロジック
# ==============================================================================
def draw_single_cross(context, props, prefix):
enable = getattr(props, f"{prefix}_enable_preview")
col_name = PREVIEW_COL_CROSS1 if prefix == "c1" else PREVIEW_COL_CROSS2
if not enable:
col = bpy.data.collections.get(col_name)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
bpy.data.collections.remove(col)
return
col = bpy.data.collections.get(col_name)
if not col:
col = bpy.data.collections.new(col_name)
context.scene.collection.children.link(col)
center = getattr(props, f"{prefix}_center")
length = getattr(props, f"{prefix}_length")
thickness = getattr(props, f"{prefix}_thickness")
c1 = getattr(props, f"{prefix}_color1")
c2 = getattr(props, f"{prefix}_color2")
cx, cy, cz = center[0], center[1], center[2]
# 線分の計算 (XZ平面上で z=x, z=-x)
pts = [
((cx-length, cy, cz-length), (cx+length, cy, cz+length)), # z = x
((cx-length, cy, cz+length), (cx+length, cy, cz-length)) # z = -x
]
colors = [c1, c2]
for i in range(2):
obj_name = f"[Preview] {prefix.capitalize()}_Line{i+1}"
obj = bpy.data.objects.get(obj_name)
if not obj:
curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
curve.dimensions = '3D'; curve.fill_mode = 'FULL'
spline = curve.splines.new('POLY'); spline.points.add(1)
obj = bpy.data.objects.new(obj_name, curve)
col.objects.link(obj)
else:
curve = obj.data; spline = curve.splines[0]
curve.bevel_depth = thickness; curve.bevel_resolution = 6
spline.points[0].co = (*pts[i][0], 1.0)
spline.points[1].co = (*pts[i][1], 1.0)
obj.hide_viewport = obj.hide_render = False
mat = get_preview_material(f"Preview_Mat_{prefix.capitalize()}_L{i+1}", colors[i])
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
# ==============================================================================
# タイマー管理
# ==============================================================================
_timer = None
_last_update_time = 0
def delayed_update():
global _timer, _last_update_time
_timer = None
now = time.time()
if now - _last_update_time < 0.05:
if _timer is None:
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
return None
_last_update_time = now
ctx = bpy.context
if not ctx or not ctx.scene: return None
if ctx.object and ctx.object.mode != 'OBJECT': return None
props = getattr(ctx.scene, PROPS_NAME, None)
if props:
update_line_preview(ctx, props)
update_torus1_preview(ctx, props)
update_torus2_preview(ctx, props)
draw_single_cross(ctx, props, "c1")
draw_single_cross(ctx, props, "c2")
return None
def on_update(self, context):
global _timer
if _timer is None:
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_EquationProps(PropertyGroup):
# Line Properties
enable_preview: BoolProperty(name="Enable Line Preview", default=CURRENT_DEFAULTS['enable_preview'], update=on_update)
val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['val_a'], update=on_update)
val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['val_b'], update=on_update)
val_d: FloatProperty(name="Value d", default=CURRENT_DEFAULTS['val_d'], update=on_update)
x_min: FloatProperty(name="X Min", default=CURRENT_DEFAULTS['x_min'], update=on_update)
x_max: FloatProperty(name="X Max", default=CURRENT_DEFAULTS['x_max'], update=on_update)
y_min: FloatProperty(name="Y Min", default=CURRENT_DEFAULTS['y_min'], update=on_update)
y_max: FloatProperty(name="Y Max", default=CURRENT_DEFAULTS['y_max'], update=on_update)
z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['z_min'], update=on_update)
z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['z_max'], update=on_update)
thickness: FloatProperty(name="Line Thickness", default=CURRENT_DEFAULTS['thickness'], min=0.01, max=10.0, update=on_update)
draw_plane: EnumProperty(name="Draw Plane", items=[('XZ', "Front (XZ)", "XZ Plane"), ('XY', "Top (XY)", "XY Plane")], default=CURRENT_DEFAULTS['draw_plane'], update=on_update)
show_eq1: BoolProperty(name="Show Eq 1", default=CURRENT_DEFAULTS['show_eq1'], update=on_update)
show_eq2: BoolProperty(name="Show Eq 2", default=CURRENT_DEFAULTS['show_eq2'], update=on_update)
show_eq3: BoolProperty(name="Show Eq 3", default=CURRENT_DEFAULTS['show_eq3'], update=on_update)
color1: FloatVectorProperty(name="Color 1", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color1'], update=on_update)
color2: FloatVectorProperty(name="Color 2", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color2'], update=on_update)
color3: FloatVectorProperty(name="Color 3", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color3'], update=on_update)
# Torus 1 (Normal)
t1_enable_preview: BoolProperty(name="Enable Torus 1 Preview", default=CURRENT_DEFAULTS['t1_enable_preview'], update=on_update)
t1_mode: EnumProperty(name="Mode", items=[('INTERVAL', "Interval Mode", ""), ('RANGE', "Range Mode", "")], default=CURRENT_DEFAULTS['t1_mode'], update=on_update)
t1_val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['t1_val_a'], update=on_update)
t1_val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['t1_val_b'], update=on_update)
t1_z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['t1_z_min'], update=on_update)
t1_z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['t1_z_max'], update=on_update)
t1_count: IntProperty(name="Total Count", default=CURRENT_DEFAULTS['t1_count'], min=1, max=500, update=on_update)
t1_z_center: FloatProperty(name="Z Center", default=CURRENT_DEFAULTS['t1_z_center'], update=on_update)
t1_z_interval: FloatProperty(name="Z Interval", default=CURRENT_DEFAULTS['t1_z_interval'], update=on_update)
t1_up_down_count: IntProperty(name="Up/Down Count", default=CURRENT_DEFAULTS['t1_up_down_count'], min=0, max=100, update=on_update)
t1_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['t1_major_radius'], min=0.1, max=100.0, update=on_update)
t1_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['t1_minor_radius'], min=0.01, max=50.0, update=on_update)
t1_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['t1_color'], update=on_update)
# Torus 2 (Elliptic)
t2_enable_preview: BoolProperty(name="Enable Torus 2 Preview", default=CURRENT_DEFAULTS['t2_enable_preview'], update=on_update)
t2_mode: EnumProperty(name="Mode", items=[('INTERVAL', "Interval Mode", ""), ('RANGE', "Range Mode", "")], default=CURRENT_DEFAULTS['t2_mode'], update=on_update)
t2_val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['t2_val_a'], update=on_update)
t2_val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['t2_val_b'], update=on_update)
t2_z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['t2_z_min'], update=on_update)
t2_z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['t2_z_max'], update=on_update)
t2_count: IntProperty(name="Total Count", default=CURRENT_DEFAULTS['t2_count'], min=1, max=500, update=on_update)
t2_z_center: FloatProperty(name="Z Center", default=CURRENT_DEFAULTS['t2_z_center'], update=on_update)
t2_z_interval: FloatProperty(name="Z Interval", default=CURRENT_DEFAULTS['t2_z_interval'], update=on_update)
t2_up_down_count: IntProperty(name="Up/Down Count", default=CURRENT_DEFAULTS['t2_up_down_count'], min=0, max=100, update=on_update)
t2_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['t2_major_radius'], min=0.1, max=100.0, update=on_update)
t2_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['t2_minor_radius'], min=0.01, max=50.0, update=on_update)
t2_f: FloatProperty(name="f (Numerator)", default=CURRENT_DEFAULTS['t2_f'], min=0.01, update=on_update)
t2_g: FloatProperty(name="g (Denominator)", default=CURRENT_DEFAULTS['t2_g'], min=0.01, update=on_update)
t2_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['t2_color'], update=on_update)
# Cross 1
c1_enable_preview: BoolProperty(name="Enable Cross 1 Preview", default=CURRENT_DEFAULTS['c1_enable_preview'], update=on_update)
c1_center: FloatVectorProperty(name="Center Position", size=3, default=CURRENT_DEFAULTS['c1_center'], update=on_update)
c1_length: FloatProperty(name="Length (Radius)", default=CURRENT_DEFAULTS['c1_length'], min=0.1, update=on_update)
c1_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['c1_thickness'], min=0.01, update=on_update)
c1_color1: FloatVectorProperty(name="Color z=x", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['c1_color1'], update=on_update)
c1_color2: FloatVectorProperty(name="Color z=-x", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['c1_color2'], update=on_update)
# Cross 2
c2_enable_preview: BoolProperty(name="Enable Cross 2 Preview", default=CURRENT_DEFAULTS['c2_enable_preview'], update=on_update)
c2_center: FloatVectorProperty(name="Center Position", size=3, default=CURRENT_DEFAULTS['c2_center'], update=on_update)
c2_length: FloatProperty(name="Length (Radius)", default=CURRENT_DEFAULTS['c2_length'], min=0.1, update=on_update)
c2_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['c2_thickness'], min=0.01, update=on_update)
c2_color1: FloatVectorProperty(name="Color z=x", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['c2_color1'], update=on_update)
c2_color2: FloatVectorProperty(name="Color z=-x", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['c2_color2'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_ShowLinePreview(Operator):
bl_idname = f"{OP_PREFIX}.show_line_preview"; bl_label = "Show Line Preview"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props: props.enable_preview = True; update_line_preview(context, props)
return {'FINISHED'}
class OT_DetachLines(Operator):
bl_idname = f"{OP_PREFIX}.detach_lines"; bl_label = "Detach Lines"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col_preview = bpy.data.collections.get(PREVIEW_COL_LINE)
if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
bpy.ops.object.select_all(action='DESELECT')
for obj in list(col_preview.objects):
if obj.hide_viewport: continue
if obj.name not in target_col.objects: target_col.objects.link(obj)
col_preview.objects.unlink(obj)
obj.name = obj.name.replace("[Preview]", "Solid_EqLine") + f"_{timestamp}"
if obj.data.materials:
mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
obj.data.materials[0] = new_mat
obj.select_set(True)
props = getattr(context.scene, PROPS_NAME, None)
if props: update_line_preview(context, props)
return {'FINISHED'}
class OT_ShowTorus1Preview(Operator):
bl_idname = f"{OP_PREFIX}.show_torus1_preview"; bl_label = "Show Normal Torus"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props: props.t1_enable_preview = True; update_torus1_preview(context, props)
return {'FINISHED'}
class OT_DetachTorus1(Operator):
bl_idname = f"{OP_PREFIX}.detach_torus1"; bl_label = "Detach Normal Torus"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col_preview = bpy.data.collections.get(PREVIEW_COL_TORUS1)
if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
bpy.ops.object.select_all(action='DESELECT')
for obj in list(col_preview.objects):
if obj.name not in target_col.objects: target_col.objects.link(obj)
col_preview.objects.unlink(obj)
obj.name = obj.name.replace("[Preview]", "Solid_Normal_Torus") + f"_{timestamp}"
if obj.data.materials:
mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
obj.data.materials[0] = new_mat
bpy.context.view_layer.objects.active = obj; obj.select_set(True)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
obj.select_set(False)
props = getattr(context.scene, PROPS_NAME, None)
if props: update_torus1_preview(context, props)
return {'FINISHED'}
class OT_ShowTorus2Preview(Operator):
bl_idname = f"{OP_PREFIX}.show_torus2_preview"; bl_label = "Show Elliptic Torus"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props: props.t2_enable_preview = True; update_torus2_preview(context, props)
return {'FINISHED'}
class OT_DetachTorus2(Operator):
bl_idname = f"{OP_PREFIX}.detach_torus2"; bl_label = "Detach Elliptic Torus"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col_preview = bpy.data.collections.get(PREVIEW_COL_TORUS2)
if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
bpy.ops.object.select_all(action='DESELECT')
for obj in list(col_preview.objects):
if obj.name not in target_col.objects: target_col.objects.link(obj)
col_preview.objects.unlink(obj)
obj.name = obj.name.replace("[Preview]", "Solid_Elliptic_Torus") + f"_{timestamp}"
if obj.data.materials:
mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
obj.data.materials[0] = new_mat
bpy.context.view_layer.objects.active = obj; obj.select_set(True)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
obj.select_set(False)
props = getattr(context.scene, PROPS_NAME, None)
if props: update_torus2_preview(context, props)
return {'FINISHED'}
class OT_ShowCross1Preview(Operator):
bl_idname = f"{OP_PREFIX}.show_cross1_preview"; bl_label = "Show Cross 1"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props: props.c1_enable_preview = True; draw_single_cross(context, props, "c1")
return {'FINISHED'}
class OT_DetachCross1(Operator):
bl_idname = f"{OP_PREFIX}.detach_cross1"; bl_label = "Detach Cross 1"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col_preview = bpy.data.collections.get(PREVIEW_COL_CROSS1)
if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
bpy.ops.object.select_all(action='DESELECT')
for obj in list(col_preview.objects):
if obj.name not in target_col.objects: target_col.objects.link(obj)
col_preview.objects.unlink(obj)
obj.name = obj.name.replace("[Preview]", "Solid_Cross1") + f"_{timestamp}"
if obj.data.materials:
mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
obj.data.materials[0] = new_mat
props = getattr(context.scene, PROPS_NAME, None)
if props: draw_single_cross(context, props, "c1")
return {'FINISHED'}
class OT_ShowCross2Preview(Operator):
bl_idname = f"{OP_PREFIX}.show_cross2_preview"; bl_label = "Show Cross 2"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props: props.c2_enable_preview = True; draw_single_cross(context, props, "c2")
return {'FINISHED'}
class OT_DetachCross2(Operator):
bl_idname = f"{OP_PREFIX}.detach_cross2"; bl_label = "Detach Cross 2"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col_preview = bpy.data.collections.get(PREVIEW_COL_CROSS2)
if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
bpy.ops.object.select_all(action='DESELECT')
for obj in list(col_preview.objects):
if obj.name not in target_col.objects: target_col.objects.link(obj)
col_preview.objects.unlink(obj)
obj.name = obj.name.replace("[Preview]", "Solid_Cross2") + f"_{timestamp}"
if obj.data.materials:
mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
obj.data.materials[0] = new_mat
props = getattr(context.scene, PROPS_NAME, None)
if props: draw_single_cross(context, props, "c2")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string(): target_text = t; break
if not target_text: return {'CANCELLED'}
code = target_text.as_string()
c1, c2, c3 = props.color1, props.color2, props.color3
t1c, t2c = props.t1_color, props.t2_color
cc1_1, cc1_2 = props.c1_color1, props.c1_color2
cc2_1, cc2_2 = props.c2_color1, props.c2_color2
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' # Line\n'
new_dict += f' "enable_preview": {props.enable_preview},\n'
new_dict += f' "val_a": {props.val_a:.4f}, "val_b": {props.val_b:.4f}, "val_d": {props.val_d:.4f},\n'
new_dict += f' "x_min": {props.x_min:.4f}, "x_max": {props.x_max:.4f},\n'
new_dict += f' "y_min": {props.y_min:.4f}, "y_max": {props.y_max:.4f},\n'
new_dict += f' "z_min": {props.z_min:.4f}, "z_max": {props.z_max:.4f},\n'
new_dict += f' "thickness": {props.thickness:.4f}, "draw_plane": "{props.draw_plane}",\n'
new_dict += f' "show_eq1": {props.show_eq1}, "show_eq2": {props.show_eq2}, "show_eq3": {props.show_eq3},\n'
new_dict += f' "color1": ({c1[0]:.4f}, {c1[1]:.4f}, {c1[2]:.4f}, {c1[3]:.4f}),\n'
new_dict += f' "color2": ({c2[0]:.4f}, {c2[1]:.4f}, {c2[2]:.4f}, {c2[3]:.4f}),\n'
new_dict += f' "color3": ({c3[0]:.4f}, {c3[1]:.4f}, {c3[2]:.4f}, {c3[3]:.4f}),\n\n'
new_dict += f' # Torus 1 (Normal)\n'
new_dict += f' "t1_enable_preview": {props.t1_enable_preview}, "t1_mode": "{props.t1_mode}",\n'
new_dict += f' "t1_val_a": {props.t1_val_a:.4f}, "t1_val_b": {props.t1_val_b:.4f},\n'
new_dict += f' "t1_z_min": {props.t1_z_min:.4f}, "t1_z_max": {props.t1_z_max:.4f}, "t1_count": {props.t1_count},\n'
new_dict += f' "t1_z_center": {props.t1_z_center:.4f}, "t1_z_interval": {props.t1_z_interval:.4f}, "t1_up_down_count": {props.t1_up_down_count},\n'
new_dict += f' "t1_major_radius": {props.t1_major_radius:.4f}, "t1_minor_radius": {props.t1_minor_radius:.4f},\n'
new_dict += f' "t1_color": ({t1c[0]:.4f}, {t1c[1]:.4f}, {t1c[2]:.4f}, {t1c[3]:.4f}),\n\n'
new_dict += f' # Torus 2 (Elliptic / Lorentz)\n'
new_dict += f' "t2_enable_preview": {props.t2_enable_preview}, "t2_mode": "{props.t2_mode}",\n'
new_dict += f' "t2_val_a": {props.t2_val_a:.4f}, "t2_val_b": {props.t2_val_b:.4f},\n'
new_dict += f' "t2_z_min": {props.t2_z_min:.4f}, "t2_z_max": {props.t2_z_max:.4f}, "t2_count": {props.t2_count},\n'
new_dict += f' "t2_z_center": {props.t2_z_center:.4f}, "t2_z_interval": {props.t2_z_interval:.4f}, "t2_up_down_count": {props.t2_up_down_count},\n'
new_dict += f' "t2_major_radius": {props.t2_major_radius:.4f}, "t2_minor_radius": {props.t2_minor_radius:.4f},\n'
new_dict += f' "t2_f": {props.t2_f:.4f}, "t2_g": {props.t2_g:.4f},\n'
new_dict += f' "t2_color": ({t2c[0]:.4f}, {t2c[1]:.4f}, {t2c[2]:.4f}, {t2c[3]:.4f}),\n\n'
new_dict += f' # Cross 1\n'
new_dict += f' "c1_enable_preview": {props.c1_enable_preview},\n'
new_dict += f' "c1_center": ({props.c1_center[0]:.4f}, {props.c1_center[1]:.4f}, {props.c1_center[2]:.4f}),\n'
new_dict += f' "c1_length": {props.c1_length:.4f}, "c1_thickness": {props.c1_thickness:.4f},\n'
new_dict += f' "c1_color1": ({cc1_1[0]:.4f}, {cc1_1[1]:.4f}, {cc1_1[2]:.4f}, {cc1_1[3]:.4f}),\n'
new_dict += f' "c1_color2": ({cc1_2[0]:.4f}, {cc1_2[1]:.4f}, {cc1_2[2]:.4f}, {cc1_2[3]:.4f}),\n\n'
new_dict += f' # Cross 2\n'
new_dict += f' "c2_enable_preview": {props.c2_enable_preview},\n'
new_dict += f' "c2_center": ({props.c2_center[0]:.4f}, {props.c2_center[1]:.4f}, {props.c2_center[2]:.4f}),\n'
new_dict += f' "c2_length": {props.c2_length:.4f}, "c2_thickness": {props.c2_thickness:.4f},\n'
new_dict += f' "c2_color1": ({cc2_1[0]:.4f}, {cc2_1[1]:.4f}, {cc2_1[2]:.4f}, {cc2_1[3]:.4f}),\n'
new_dict += f' "c2_color2": ({cc2_2[0]:.4f}, {cc2_2[1]:.4f}, {cc2_2[2]:.4f}, {cc2_2[3]:.4f}),\n'
new_dict += "}\n"
try:
tag_start = "# <BEGIN" + "_DICT>"
tag_end = "# <END" + "_DICT>"
pre_code, rest = code.split(tag_start, 1)
_, post_code = rest.split(tag_end, 1)
final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
context.window_manager.clipboard = final_code
self.report({'INFO'}, "Code copied!")
except: return {'CANCELLED'}
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 = "Close Addon"
def execute(self, context):
bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
self.report({'INFO'}, "アドオンを終了しました。")
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_MainPanel(Panel):
bl_label = PANEL_TITLE
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: return
row_prev = layout.row(); row_prev.scale_y = 1.5
if not props.enable_preview: row_prev.operator(OT_ShowLinePreview.bl_idname, icon='PLAY', text="Show Line Preview")
else: row_prev.prop(props, "enable_preview", text="Line Preview Active", toggle=True, icon='PAUSE')
box_info = layout.box()
box_info.label(text="【 Line Info 】", icon='INFO')
a_str = f"{props.val_a:.2f}" if abs(props.val_a) > 0.0001 else "0.00(Err)"
box_info.label(text=f"y = ({props.val_b:.2f} / {a_str}) x")
box_values = layout.box()
col_v = box_values.column(align=True)
col_v.prop(props, "val_a"); col_v.prop(props, "val_b"); col_v.prop(props, "val_d")
box_limits = layout.box()
box_limits.label(text="Limits (X, Y, Z)", icon='MOD_HULL')
for axis in ['x', 'y', 'z']:
r = box_limits.row(align=True)
r.prop(props, f"{axis}_min", text=f"{axis.upper()} Min")
r.prop(props, f"{axis}_max", text="Max")
class PT_VisibilityPanel(Panel):
bl_label = "Line Design & Detach"
bl_idname = f"{PREFIX}_PT_visibility"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_parent_id = f"{PREFIX}_PT_main"
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
layout.prop(props, "thickness"); layout.prop(props, "draw_plane"); layout.separator()
for i in range(1, 4):
r = layout.row(align=True)
r.prop(props, f"show_eq{i}", text=f"Eq {i}", toggle=True); r.prop(props, f"color{i}", text="")
layout.separator()
col_exec = layout.column(); col_exec.scale_y = 1.5
col_exec.operator(OT_DetachLines.bl_idname, icon='MESH_CYLINDER', text="Detach Lines (固定化)")
class PT_Torus1Panel(Panel):
bl_label = "Torus (Normal)"
bl_idname = f"{PREFIX}_PT_torus1"
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
row_prev = layout.row(); row_prev.scale_y = 1.5
if not props.t1_enable_preview: row_prev.operator(OT_ShowTorus1Preview.bl_idname, icon='PLAY', text="Show Normal Torus")
else: row_prev.prop(props, "t1_enable_preview", text="Normal Torus Active", toggle=True, icon='PAUSE')
box_eq = layout.box()
box_eq.label(text="Center Line: z = (b/a)x", icon='IPO_CONSTANT')
col = box_eq.column(align=True)
col.prop(props, "t1_val_a"); col.prop(props, "t1_val_b")
box_r = layout.box()
box_r.prop(props, "t1_mode", text="")
if props.t1_mode == 'INTERVAL':
c_int = box_r.column(align=True)
c_int.prop(props, "t1_z_center"); c_int.prop(props, "t1_z_interval"); c_int.prop(props, "t1_up_down_count")
else:
c_rng = box_r.column(align=True)
c_rng.prop(props, "t1_z_min"); c_rng.prop(props, "t1_z_max"); c_rng.prop(props, "t1_count")
box_s = layout.box()
box_s.prop(props, "t1_major_radius"); box_s.prop(props, "t1_minor_radius"); box_s.prop(props, "t1_color")
col_exec = layout.column(); col_exec.scale_y = 1.5
col_exec.operator(OT_DetachTorus1.bl_idname, icon='MESH_TORUS', text="Detach Normal Torus")
class PT_Torus2Panel(Panel):
bl_label = "Torus (Elliptic / Lorentz)"
bl_idname = f"{PREFIX}_PT_torus2"
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
row_prev = layout.row(); row_prev.scale_y = 1.5
if not props.t2_enable_preview: row_prev.operator(OT_ShowTorus2Preview.bl_idname, icon='PLAY', text="Show Elliptic Torus")
else: row_prev.prop(props, "t2_enable_preview", text="Elliptic Torus Active", toggle=True, icon='PAUSE')
box_eq = layout.box()
box_eq.label(text="Center Line: z = (b/a)x", icon='IPO_CONSTANT')
col = box_eq.column(align=True)
col.prop(props, "t2_val_a"); col.prop(props, "t2_val_b")
box_r = layout.box()
box_r.prop(props, "t2_mode", text="")
if props.t2_mode == 'INTERVAL':
c_int = box_r.column(align=True)
c_int.prop(props, "t2_z_center"); c_int.prop(props, "t2_z_interval"); c_int.prop(props, "t2_up_down_count")
else:
c_rng = box_r.column(align=True)
c_rng.prop(props, "t2_z_min"); c_rng.prop(props, "t2_z_max"); c_rng.prop(props, "t2_count")
box_s = layout.box()
box_s.prop(props, "t2_major_radius"); box_s.prop(props, "t2_minor_radius")
box_s.separator()
box_s.label(text="楕円トーラスの収縮割合 (f/g)", icon='CON_SIZELIKE')
col_fg = box_s.column(align=True)
col_fg.prop(props, "t2_f")
col_fg.prop(props, "t2_g")
g_val = props.t2_g if abs(props.t2_g) > 0.0001 else 0.0001
box_s.label(text=f"収縮率: {props.t2_f / g_val:.4f}")
box_s.prop(props, "t2_color")
col_exec = layout.column(); col_exec.scale_y = 1.5
col_exec.operator(OT_DetachTorus2.bl_idname, icon='MESH_TORUS', text="Detach Elliptic Torus")
class PT_Cross1Panel(Panel):
bl_label = "Cross Cylinders 1"
bl_idname = f"{PREFIX}_PT_cross1"
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
row_prev = layout.row(); row_prev.scale_y = 1.5
if not props.c1_enable_preview: row_prev.operator(OT_ShowCross1Preview.bl_idname, icon='PLAY', text="Show Cross 1")
else: row_prev.prop(props, "c1_enable_preview", text="Cross 1 Active", toggle=True, icon='PAUSE')
box_s = layout.box()
box_s.prop(props, "c1_center")
box_s.prop(props, "c1_length")
box_s.prop(props, "c1_thickness")
box_s.separator()
r1 = box_s.row(align=True)
r1.label(text="z = x"); r1.prop(props, "c1_color1", text="")
r2 = box_s.row(align=True)
r2.label(text="z = -x"); r2.prop(props, "c1_color2", text="")
col_exec = layout.column(); col_exec.scale_y = 1.5
col_exec.operator(OT_DetachCross1.bl_idname, icon='MESH_DATA', text="Detach Cross 1")
class PT_Cross2Panel(Panel):
bl_label = "Cross Cylinders 2"
bl_idname = f"{PREFIX}_PT_cross2"
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
row_prev = layout.row(); row_prev.scale_y = 1.5
if not props.c2_enable_preview: row_prev.operator(OT_ShowCross2Preview.bl_idname, icon='PLAY', text="Show Cross 2")
else: row_prev.prop(props, "c2_enable_preview", text="Cross 2 Active", toggle=True, icon='PAUSE')
box_s = layout.box()
box_s.prop(props, "c2_center")
box_s.prop(props, "c2_length")
box_s.prop(props, "c2_thickness")
box_s.separator()
r1 = box_s.row(align=True)
r1.label(text="z = x"); r1.prop(props, "c2_color1", text="")
r2 = box_s.row(align=True)
r2.label(text="z = -x"); r2.prop(props, "c2_color2", text="")
col_exec = layout.column(); col_exec.scale_y = 1.5
col_exec.operator(OT_DetachCross2.bl_idname, icon='MESH_DATA', text="Detach Cross 2")
class PT_SystemPanel(Panel):
bl_label = "System (Copy / Close)"
bl_idname = f"{PREFIX}_PT_system"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
r_c = layout.row(); r_c.scale_y = 1.2
r_c.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
r_r = layout.row(); r_r.scale_y = 1.2; r_r.alert = True
r_r.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Close Addon (完全終了)")
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
def draw(self, context):
for l in ADDON_LINKS:
self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"], icon='URL').url = l["url"]
# ==============================================================================
# REGISTER
# ==============================================================================
classes = (
PG_EquationProps,
OT_ShowLinePreview, OT_DetachLines,
OT_ShowTorus1Preview, OT_DetachTorus1,
OT_ShowTorus2Preview, OT_DetachTorus2,
OT_ShowCross1Preview, OT_DetachCross1,
OT_ShowCross2Preview, OT_DetachCross2,
OT_CopyFullScript, OT_OpenUrl, OT_RemoveAddon,
PT_MainPanel, PT_VisibilityPanel,
PT_Torus1Panel, PT_Torus2Panel,
PT_Cross1Panel, PT_Cross2Panel,
PT_SystemPanel, PT_LinksPanel
)
def auto_open_sidebar():
try:
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':
if not space.show_region_ui: space.show_region_ui = True
except: pass
return None
def register():
for c in classes:
try: bpy.utils.register_class(c)
except ValueError: pass
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_EquationProps))
bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)
def unregister():
global _timer
if _timer is not None:
try: bpy.app.timers.unregister(_timer)
except Exception: pass
_timer = None
cleanup_preview_data()
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except ValueError: pass
if __name__ == "__main__":
register()
# Copied: 2026-04-08 12:00:00
import bpy
import time
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime
# ==============================================================================
# 【 基本設定エリア 】
# ==============================================================================
PREFIX = "EqGen"
ADDON_NAME = "[ Equation Gen ]"
TAB_NAME = "[ Equation Gen ]"
PANEL_TITLE = "Equation Lines"
AUTHOR = "zionadchat"
# ★ このスクリプト自身のID(絶対に消さないこと)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: EQ_LINES_2026_04_08_V6 ###"
# ==============================================================================
# システム初期化 & ID管理
# ==============================================================================
bl_info = {
"name": f"{ADDON_NAME} {PREFIX}",
"author": AUTHOR,
"version": (6, 0, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": "Lines, Torus, Elliptic Torus, and Cross Cylinders Generators",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
ADDON_LINKS = (
{"label": "Prefix 20260408", "url": "<https://www.notion.so/Prefix-20260408-33cf5dacaf43807e9e35ff8cdbbc39c6>"},
)
# ==============================================================================
# デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
# Line
"enable_preview": False,
"val_a": 0.6000, "val_b": 1.0000, "val_d": 10.0000,
"x_min": -50.0, "x_max": 50.0,
"y_min": -50.0, "y_max": 50.0,
"z_min": -50.0, "z_max": 50.0,
"thickness": 0.5000, "draw_plane": "XZ",
"show_eq1": True, "show_eq2": True, "show_eq3": True,
"color1": (1.0000, 0.2000, 0.2000, 1.0000),
"color2": (0.2000, 1.0000, 0.2000, 1.0000),
"color3": (0.2000, 0.2000, 1.0000, 1.0000),
# Torus 1 (Normal)
"t1_enable_preview": False,
"t1_mode": "INTERVAL",
"t1_val_a": 0.6000, "t1_val_b": 1.0000,
"t1_z_min": -50.0, "t1_z_max": 50.0, "t1_count": 11,
"t1_z_center": 0.0, "t1_z_interval": 1.0, "t1_up_down_count": 5,
"t1_major_radius": 5.0, "t1_minor_radius": 1.0,
"t1_color": (0.2000, 0.8000, 0.8000, 1.0000),
# Torus 2 (Elliptic / Lorentz)
"t2_enable_preview": False,
"t2_mode": "INTERVAL",
"t2_val_a": 0.6000, "t2_val_b": 1.0000,
"t2_z_min": -50.0, "t2_z_max": 50.0, "t2_count": 11,
"t2_z_center": 0.0, "t2_z_interval": 1.0, "t2_up_down_count": 5,
"t2_major_radius": 5.0, "t2_minor_radius": 1.0,
"t2_f": 1.0000, "t2_g": 1.6000, # 収縮率 f/g
"t2_color": (0.8000, 0.2000, 0.8000, 1.0000),
# Cross 1 (y=x, y=-x)
"c1_enable_preview": False,
"c1_center": (0.0, 0.0, 0.0),
"c1_length": 50.0, "c1_thickness": 0.5000,
"c1_plane": "XY",
"c1_color1": (1.0000, 1.0000, 0.2000, 1.0000),
"c1_color2": (1.0000, 0.5000, 0.2000, 1.0000),
# Cross 2 (y=x, y=-x)
"c2_enable_preview": False,
"c2_center": (10.0, 10.0, 0.0),
"c2_length": 30.0, "c2_thickness": 0.5000,
"c2_plane": "XY",
"c2_color1": (0.2000, 1.0000, 0.2000, 1.0000),
"c2_color2": (0.2000, 0.5000, 1.0000, 1.0000),
}
# <END_DICT>
PREVIEW_COL_LINE = f"{PREFIX}_Line_Preview"
PREVIEW_COL_TORUS1 = f"{PREFIX}_Torus1_Preview"
PREVIEW_COL_TORUS2 = f"{PREFIX}_Torus2_Preview"
PREVIEW_COL_CROSS1 = f"{PREFIX}_Cross1_Preview"
PREVIEW_COL_CROSS2 = f"{PREFIX}_Cross2_Preview"
# ==============================================================================
# 共通マテリアル・データ管理 ロジック
# ==============================================================================
def cleanup_preview_data():
for name in [PREVIEW_COL_LINE, PREVIEW_COL_TORUS1, PREVIEW_COL_TORUS2, PREVIEW_COL_CROSS1, PREVIEW_COL_CROSS2]:
col = bpy.data.collections.get(name)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0:
if isinstance(data, bpy.types.Curve):
bpy.data.curves.remove(data)
if len(col.objects) == 0:
bpy.data.collections.remove(col)
def apply_material_settings(mat, color):
mat.use_nodes = True
mat.blend_method = 'BLEND'
mat.diffuse_color = color
tree = mat.node_tree
bsdf = tree.nodes.get("Principled BSDF")
if not bsdf:
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.name = "Principled BSDF"
out = tree.nodes.new("ShaderNodeOutputMaterial")
tree.links.new(bsdf.outputs[0], out.inputs[0])
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
def get_preview_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name=name)
apply_material_settings(mat, color)
return mat
def build_curve_circle(curve, radius, segments=32):
if len(curve.splines) == 0:
spline = curve.splines.new('POLY')
spline.points.add(segments - 1)
spline.use_cyclic_u = True
else:
spline = curve.splines[0]
if len(spline.points) != segments:
curve.splines.clear()
spline = curve.splines.new('POLY')
spline.points.add(segments - 1)
spline.use_cyclic_u = True
for i in range(segments):
angle = 2 * math.pi * i / segments
spline.points[i].co = (radius * math.cos(angle), radius * math.sin(angle), 0.0, 1.0)
# ==============================================================================
# Line プレビューロジック
# ==============================================================================
def get_clipped_segment(m, c, x_min, x_max, v_min, v_max):
if m == 0:
if v_min <= c <= v_max: return x_min, x_max
return None, None
else:
x_from_v1, x_from_v2 = (v_min - c) / m, (v_max - c) / m
valid_x_min, valid_x_max = min(x_from_v1, x_from_v2), max(x_from_v1, x_from_v2)
act_x_min, act_x_max = max(x_min, valid_x_min), min(x_max, valid_x_max)
if act_x_min > act_x_max: return None, None
return act_x_min, act_x_max
def calc_points(props, m, c):
x_min, x_max = min(props.x_min, props.x_max), max(props.x_min, props.x_max)
y_min, y_max = min(props.y_min, props.y_max), max(props.y_min, props.y_max)
z_min, z_max = min(props.z_min, props.z_max), max(props.z_min, props.z_max)
if props.draw_plane == 'XZ':
act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, z_min, z_max)
if act_x_min is None: return None, None
return (act_x_min, 0.0, m * act_x_min + c), (act_x_max, 0.0, m * act_x_max + c)
else:
act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, y_min, y_max)
if act_x_min is None: return None, None
return (act_x_min, m * act_x_min + c, 0.0), (act_x_max, m * act_x_max + c, 0.0)
def update_line_preview(context, props):
if not props.enable_preview:
col = bpy.data.collections.get(PREVIEW_COL_LINE)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
bpy.data.collections.remove(col)
return
col = bpy.data.collections.get(PREVIEW_COL_LINE)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_LINE)
context.scene.collection.children.link(col)
a = props.val_a if abs(props.val_a) > 0.0001 else 0.0001
b, d = props.val_b, props.val_d
m = b / a
equations = [
{"id": 1, "show": props.show_eq1, "color": props.color1, "offset": 0.0},
{"id": 2, "show": props.show_eq2, "color": props.color2, "offset": -d},
{"id": 3, "show": props.show_eq3, "color": props.color3, "offset": d},
]
for eq in equations:
obj_name = f"[Preview] EqLine_{eq['id']}"
obj = bpy.data.objects.get(obj_name)
if not eq["show"]:
if obj: obj.hide_viewport = obj.hide_render = True
continue
p1, p2 = calc_points(props, m, eq["offset"])
if p1 is None:
if obj: obj.hide_viewport = obj.hide_render = True
continue
if not obj:
curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
curve.dimensions = '3D'; curve.fill_mode = 'FULL'
spline = curve.splines.new('POLY'); spline.points.add(1)
obj = bpy.data.objects.new(obj_name, curve)
col.objects.link(obj)
else:
curve = obj.data; spline = curve.splines[0]
curve.bevel_depth = props.thickness; curve.bevel_resolution = 6
spline.points[0].co = (*p1, 1.0); spline.points[1].co = (*p2, 1.0)
obj.hide_viewport = obj.hide_render = False
mat = get_preview_material(f"Preview_Mat_EqLine{eq['id']}", eq["color"])
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
# ==============================================================================
# Torus プレビューロジック (Normal & Elliptic)
# ==============================================================================
def update_torus1_preview(context, props):
if not props.t1_enable_preview:
col = bpy.data.collections.get(PREVIEW_COL_TORUS1)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
bpy.data.collections.remove(col)
return
col = bpy.data.collections.get(PREVIEW_COL_TORUS1)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_TORUS1)
context.scene.collection.children.link(col)
a = props.t1_val_a
b_val = props.t1_val_b if abs(props.t1_val_b) > 0.0001 else 0.0001
z_list = []
if props.t1_mode == 'RANGE':
count = props.t1_count
for i in range(count):
t = i / (count - 1) if count > 1 else 0.5
z_list.append(props.t1_z_min + t * (props.t1_z_max - props.t1_z_min))
else:
c, interval, ud = props.t1_z_center, props.t1_z_interval, props.t1_up_down_count
for i in range(-ud, ud + 1): z_list.append(c + i * interval)
existing_objs = list(col.objects)
mat = get_preview_material("Preview_Mat_Torus1", props.t1_color)
for i, z in enumerate(z_list):
obj_name = f"[Preview] Normal_Torus_{i+1}"
x = z * (a / b_val); y = 0.0
if i < len(existing_objs):
obj = existing_objs[i]
curve = obj.data
else:
curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
curve.dimensions = '3D'; curve.fill_mode = 'FULL'
obj = bpy.data.objects.new(obj_name, curve)
col.objects.link(obj)
curve.bevel_depth = props.t1_minor_radius; curve.bevel_resolution = 8
build_curve_circle(curve, props.t1_major_radius)
obj.location = (x, y, z); obj.scale = (1.0, 1.0, 1.0)
obj.hide_viewport = obj.hide_render = False
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
if len(existing_objs) > len(z_list):
for obj in existing_objs[len(z_list):]:
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
def update_torus2_preview(context, props):
if not props.t2_enable_preview:
col = bpy.data.collections.get(PREVIEW_COL_TORUS2)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
bpy.data.collections.remove(col)
return
col = bpy.data.collections.get(PREVIEW_COL_TORUS2)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_TORUS2)
context.scene.collection.children.link(col)
a = props.t2_val_a
b_val = props.t2_val_b if abs(props.t2_val_b) > 0.0001 else 0.0001
# 収縮率計算
g_val = props.t2_g if abs(props.t2_g) > 0.0001 else 0.0001
scale_x = props.t2_f / g_val
z_list = []
if props.t2_mode == 'RANGE':
count = props.t2_count
for i in range(count):
t = i / (count - 1) if count > 1 else 0.5
z_list.append(props.t2_z_min + t * (props.t2_z_max - props.t2_z_min))
else:
c, interval, ud = props.t2_z_center, props.t2_z_interval, props.t2_up_down_count
for i in range(-ud, ud + 1): z_list.append(c + i * interval)
existing_objs = list(col.objects)
mat = get_preview_material("Preview_Mat_Torus2", props.t2_color)
for i, z in enumerate(z_list):
obj_name = f"[Preview] Elliptic_Torus_{i+1}"
x = z * (a / b_val); y = 0.0
if i < len(existing_objs):
obj = existing_objs[i]
curve = obj.data
else:
curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
curve.dimensions = '3D'; curve.fill_mode = 'FULL'
obj = bpy.data.objects.new(obj_name, curve)
col.objects.link(obj)
curve.bevel_depth = props.t2_minor_radius; curve.bevel_resolution = 8
build_curve_circle(curve, props.t2_major_radius)
obj.location = (x, y, z)
obj.scale = (scale_x, 1.0, 1.0) # Lorentz Contraction
obj.hide_viewport = obj.hide_render = False
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
if len(existing_objs) > len(z_list):
for obj in existing_objs[len(z_list):]:
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
# ==============================================================================
# Cross (y=x, y=-x) プレビューロジック
# ==============================================================================
def draw_single_cross(context, props, prefix):
enable = getattr(props, f"{prefix}_enable_preview")
col_name = PREVIEW_COL_CROSS1 if prefix == "c1" else PREVIEW_COL_CROSS2
if not enable:
col = bpy.data.collections.get(col_name)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
bpy.data.collections.remove(col)
return
col = bpy.data.collections.get(col_name)
if not col:
col = bpy.data.collections.new(col_name)
context.scene.collection.children.link(col)
center = getattr(props, f"{prefix}_center")
length = getattr(props, f"{prefix}_length")
thickness = getattr(props, f"{prefix}_thickness")
plane = getattr(props, f"{prefix}_plane")
c1 = getattr(props, f"{prefix}_color1")
c2 = getattr(props, f"{prefix}_color2")
cx, cy, cz = center[0], center[1], center[2]
# 線分の計算
if plane == 'XY':
pts = [
((cx-length, cy-length, cz), (cx+length, cy+length, cz)), # y = x
((cx-length, cy+length, cz), (cx+length, cy-length, cz)) # y = -x
]
else: # XZ
pts = [
((cx-length, cy, cz-length), (cx+length, cy, cz+length)), # z = x
((cx-length, cy, cz+length), (cx+length, cy, cz-length)) # z = -x
]
colors = [c1, c2]
for i in range(2):
obj_name = f"[Preview] {prefix.capitalize()}_Line{i+1}"
obj = bpy.data.objects.get(obj_name)
if not obj:
curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
curve.dimensions = '3D'; curve.fill_mode = 'FULL'
spline = curve.splines.new('POLY'); spline.points.add(1)
obj = bpy.data.objects.new(obj_name, curve)
col.objects.link(obj)
else:
curve = obj.data; spline = curve.splines[0]
curve.bevel_depth = thickness; curve.bevel_resolution = 6
spline.points[0].co = (*pts[i][0], 1.0)
spline.points[1].co = (*pts[i][1], 1.0)
obj.hide_viewport = obj.hide_render = False
mat = get_preview_material(f"Preview_Mat_{prefix.capitalize()}_L{i+1}", colors[i])
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
# ==============================================================================
# タイマー管理
# ==============================================================================
_timer = None
_last_update_time = 0
def delayed_update():
global _timer, _last_update_time
_timer = None
now = time.time()
if now - _last_update_time < 0.05:
if _timer is None:
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
return None
_last_update_time = now
ctx = bpy.context
if not ctx or not ctx.scene: return None
if ctx.object and ctx.object.mode != 'OBJECT': return None
props = getattr(ctx.scene, PROPS_NAME, None)
if props:
update_line_preview(ctx, props)
update_torus1_preview(ctx, props)
update_torus2_preview(ctx, props)
draw_single_cross(ctx, props, "c1")
draw_single_cross(ctx, props, "c2")
return None
def on_update(self, context):
global _timer
if _timer is None:
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_EquationProps(PropertyGroup):
# Line Properties
enable_preview: BoolProperty(name="Enable Line Preview", default=CURRENT_DEFAULTS['enable_preview'], update=on_update)
val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['val_a'], update=on_update)
val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['val_b'], update=on_update)
val_d: FloatProperty(name="Value d", default=CURRENT_DEFAULTS['val_d'], update=on_update)
x_min: FloatProperty(name="X Min", default=CURRENT_DEFAULTS['x_min'], update=on_update)
x_max: FloatProperty(name="X Max", default=CURRENT_DEFAULTS['x_max'], update=on_update)
y_min: FloatProperty(name="Y Min", default=CURRENT_DEFAULTS['y_min'], update=on_update)
y_max: FloatProperty(name="Y Max", default=CURRENT_DEFAULTS['y_max'], update=on_update)
z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['z_min'], update=on_update)
z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['z_max'], update=on_update)
thickness: FloatProperty(name="Line Thickness", default=CURRENT_DEFAULTS['thickness'], min=0.01, max=10.0, update=on_update)
draw_plane: EnumProperty(name="Draw Plane", items=[('XZ', "Front (XZ)", "XZ Plane"), ('XY', "Top (XY)", "XY Plane")], default=CURRENT_DEFAULTS['draw_plane'], update=on_update)
show_eq1: BoolProperty(name="Show Eq 1", default=CURRENT_DEFAULTS['show_eq1'], update=on_update)
show_eq2: BoolProperty(name="Show Eq 2", default=CURRENT_DEFAULTS['show_eq2'], update=on_update)
show_eq3: BoolProperty(name="Show Eq 3", default=CURRENT_DEFAULTS['show_eq3'], update=on_update)
color1: FloatVectorProperty(name="Color 1", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color1'], update=on_update)
color2: FloatVectorProperty(name="Color 2", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color2'], update=on_update)
color3: FloatVectorProperty(name="Color 3", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color3'], update=on_update)
# Torus 1 (Normal)
t1_enable_preview: BoolProperty(name="Enable Torus 1 Preview", default=CURRENT_DEFAULTS['t1_enable_preview'], update=on_update)
t1_mode: EnumProperty(name="Mode", items=[('INTERVAL', "Interval Mode", ""), ('RANGE', "Range Mode", "")], default=CURRENT_DEFAULTS['t1_mode'], update=on_update)
t1_val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['t1_val_a'], update=on_update)
t1_val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['t1_val_b'], update=on_update)
t1_z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['t1_z_min'], update=on_update)
t1_z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['t1_z_max'], update=on_update)
t1_count: IntProperty(name="Total Count", default=CURRENT_DEFAULTS['t1_count'], min=1, max=500, update=on_update)
t1_z_center: FloatProperty(name="Z Center", default=CURRENT_DEFAULTS['t1_z_center'], update=on_update)
t1_z_interval: FloatProperty(name="Z Interval", default=CURRENT_DEFAULTS['t1_z_interval'], update=on_update)
t1_up_down_count: IntProperty(name="Up/Down Count", default=CURRENT_DEFAULTS['t1_up_down_count'], min=0, max=100, update=on_update)
t1_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['t1_major_radius'], min=0.1, max=100.0, update=on_update)
t1_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['t1_minor_radius'], min=0.01, max=50.0, update=on_update)
t1_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['t1_color'], update=on_update)
# Torus 2 (Elliptic)
t2_enable_preview: BoolProperty(name="Enable Torus 2 Preview", default=CURRENT_DEFAULTS['t2_enable_preview'], update=on_update)
t2_mode: EnumProperty(name="Mode", items=[('INTERVAL', "Interval Mode", ""), ('RANGE', "Range Mode", "")], default=CURRENT_DEFAULTS['t2_mode'], update=on_update)
t2_val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['t2_val_a'], update=on_update)
t2_val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['t2_val_b'], update=on_update)
t2_z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['t2_z_min'], update=on_update)
t2_z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['t2_z_max'], update=on_update)
t2_count: IntProperty(name="Total Count", default=CURRENT_DEFAULTS['t2_count'], min=1, max=500, update=on_update)
t2_z_center: FloatProperty(name="Z Center", default=CURRENT_DEFAULTS['t2_z_center'], update=on_update)
t2_z_interval: FloatProperty(name="Z Interval", default=CURRENT_DEFAULTS['t2_z_interval'], update=on_update)
t2_up_down_count: IntProperty(name="Up/Down Count", default=CURRENT_DEFAULTS['t2_up_down_count'], min=0, max=100, update=on_update)
t2_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['t2_major_radius'], min=0.1, max=100.0, update=on_update)
t2_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['t2_minor_radius'], min=0.01, max=50.0, update=on_update)
t2_f: FloatProperty(name="f (Numerator)", default=CURRENT_DEFAULTS['t2_f'], min=0.01, update=on_update)
t2_g: FloatProperty(name="g (Denominator)", default=CURRENT_DEFAULTS['t2_g'], min=0.01, update=on_update)
t2_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['t2_color'], update=on_update)
# Cross 1
c1_enable_preview: BoolProperty(name="Enable Cross 1 Preview", default=CURRENT_DEFAULTS['c1_enable_preview'], update=on_update)
c1_center: FloatVectorProperty(name="Center Position", size=3, default=CURRENT_DEFAULTS['c1_center'], update=on_update)
c1_length: FloatProperty(name="Length (Radius)", default=CURRENT_DEFAULTS['c1_length'], min=0.1, update=on_update)
c1_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['c1_thickness'], min=0.01, update=on_update)
c1_plane: EnumProperty(name="Draw Plane", items=[('XY', "XY Plane (y=x)", ""), ('XZ', "XZ Plane (z=x)", "")], default=CURRENT_DEFAULTS['c1_plane'], update=on_update)
c1_color1: FloatVectorProperty(name="Color y=x", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['c1_color1'], update=on_update)
c1_color2: FloatVectorProperty(name="Color y=-x", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['c1_color2'], update=on_update)
# Cross 2
c2_enable_preview: BoolProperty(name="Enable Cross 2 Preview", default=CURRENT_DEFAULTS['c2_enable_preview'], update=on_update)
c2_center: FloatVectorProperty(name="Center Position", size=3, default=CURRENT_DEFAULTS['c2_center'], update=on_update)
c2_length: FloatProperty(name="Length (Radius)", default=CURRENT_DEFAULTS['c2_length'], min=0.1, update=on_update)
c2_thickness: FloatProperty(name="Thickness", default=CURRENT_DEFAULTS['c2_thickness'], min=0.01, update=on_update)
c2_plane: EnumProperty(name="Draw Plane", items=[('XY', "XY Plane (y=x)", ""), ('XZ', "XZ Plane (z=x)", "")], default=CURRENT_DEFAULTS['c2_plane'], update=on_update)
c2_color1: FloatVectorProperty(name="Color y=x", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['c2_color1'], update=on_update)
c2_color2: FloatVectorProperty(name="Color y=-x", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['c2_color2'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_ShowLinePreview(Operator):
bl_idname = f"{OP_PREFIX}.show_line_preview"; bl_label = "Show Line Preview"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props: props.enable_preview = True; update_line_preview(context, props)
return {'FINISHED'}
class OT_DetachLines(Operator):
bl_idname = f"{OP_PREFIX}.detach_lines"; bl_label = "Detach Lines"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col_preview = bpy.data.collections.get(PREVIEW_COL_LINE)
if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
bpy.ops.object.select_all(action='DESELECT')
for obj in list(col_preview.objects):
if obj.hide_viewport: continue
if obj.name not in target_col.objects: target_col.objects.link(obj)
col_preview.objects.unlink(obj)
obj.name = obj.name.replace("[Preview]", "Solid_EqLine") + f"_{timestamp}"
if obj.data.materials:
mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
obj.data.materials[0] = new_mat
obj.select_set(True)
props = getattr(context.scene, PROPS_NAME, None)
if props: update_line_preview(context, props)
return {'FINISHED'}
class OT_ShowTorus1Preview(Operator):
bl_idname = f"{OP_PREFIX}.show_torus1_preview"; bl_label = "Show Normal Torus"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props: props.t1_enable_preview = True; update_torus1_preview(context, props)
return {'FINISHED'}
class OT_DetachTorus1(Operator):
bl_idname = f"{OP_PREFIX}.detach_torus1"; bl_label = "Detach Normal Torus"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col_preview = bpy.data.collections.get(PREVIEW_COL_TORUS1)
if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
bpy.ops.object.select_all(action='DESELECT')
for obj in list(col_preview.objects):
if obj.name not in target_col.objects: target_col.objects.link(obj)
col_preview.objects.unlink(obj)
obj.name = obj.name.replace("[Preview]", "Solid_Normal_Torus") + f"_{timestamp}"
if obj.data.materials:
mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
obj.data.materials[0] = new_mat
bpy.context.view_layer.objects.active = obj; obj.select_set(True)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
obj.select_set(False)
props = getattr(context.scene, PROPS_NAME, None)
if props: update_torus1_preview(context, props)
return {'FINISHED'}
class OT_ShowTorus2Preview(Operator):
bl_idname = f"{OP_PREFIX}.show_torus2_preview"; bl_label = "Show Elliptic Torus"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props: props.t2_enable_preview = True; update_torus2_preview(context, props)
return {'FINISHED'}
class OT_DetachTorus2(Operator):
bl_idname = f"{OP_PREFIX}.detach_torus2"; bl_label = "Detach Elliptic Torus"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col_preview = bpy.data.collections.get(PREVIEW_COL_TORUS2)
if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
bpy.ops.object.select_all(action='DESELECT')
for obj in list(col_preview.objects):
if obj.name not in target_col.objects: target_col.objects.link(obj)
col_preview.objects.unlink(obj)
obj.name = obj.name.replace("[Preview]", "Solid_Elliptic_Torus") + f"_{timestamp}"
if obj.data.materials:
mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
obj.data.materials[0] = new_mat
bpy.context.view_layer.objects.active = obj; obj.select_set(True)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
obj.select_set(False)
props = getattr(context.scene, PROPS_NAME, None)
if props: update_torus2_preview(context, props)
return {'FINISHED'}
class OT_ShowCross1Preview(Operator):
bl_idname = f"{OP_PREFIX}.show_cross1_preview"; bl_label = "Show Cross 1"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props: props.c1_enable_preview = True; draw_single_cross(context, props, "c1")
return {'FINISHED'}
class OT_DetachCross1(Operator):
bl_idname = f"{OP_PREFIX}.detach_cross1"; bl_label = "Detach Cross 1"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col_preview = bpy.data.collections.get(PREVIEW_COL_CROSS1)
if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
bpy.ops.object.select_all(action='DESELECT')
for obj in list(col_preview.objects):
if obj.name not in target_col.objects: target_col.objects.link(obj)
col_preview.objects.unlink(obj)
obj.name = obj.name.replace("[Preview]", "Solid_Cross1") + f"_{timestamp}"
if obj.data.materials:
mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
obj.data.materials[0] = new_mat
props = getattr(context.scene, PROPS_NAME, None)
if props: draw_single_cross(context, props, "c1")
return {'FINISHED'}
class OT_ShowCross2Preview(Operator):
bl_idname = f"{OP_PREFIX}.show_cross2_preview"; bl_label = "Show Cross 2"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props: props.c2_enable_preview = True; draw_single_cross(context, props, "c2")
return {'FINISHED'}
class OT_DetachCross2(Operator):
bl_idname = f"{OP_PREFIX}.detach_cross2"; bl_label = "Detach Cross 2"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col_preview = bpy.data.collections.get(PREVIEW_COL_CROSS2)
if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
target_col = context.collection; timestamp = datetime.now().strftime('%H%M%S')
bpy.ops.object.select_all(action='DESELECT')
for obj in list(col_preview.objects):
if obj.name not in target_col.objects: target_col.objects.link(obj)
col_preview.objects.unlink(obj)
obj.name = obj.name.replace("[Preview]", "Solid_Cross2") + f"_{timestamp}"
if obj.data.materials:
mat = obj.data.materials[0]; new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
obj.data.materials[0] = new_mat
props = getattr(context.scene, PROPS_NAME, None)
if props: draw_single_cross(context, props, "c2")
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string(): target_text = t; break
if not target_text: return {'CANCELLED'}
code = target_text.as_string()
c1, c2, c3 = props.color1, props.color2, props.color3
t1c, t2c = props.t1_color, props.t2_color
cc1_1, cc1_2 = props.c1_color1, props.c1_color2
cc2_1, cc2_2 = props.c2_color1, props.c2_color2
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' # Line\n'
new_dict += f' "enable_preview": {props.enable_preview},\n'
new_dict += f' "val_a": {props.val_a:.4f}, "val_b": {props.val_b:.4f}, "val_d": {props.val_d:.4f},\n'
new_dict += f' "x_min": {props.x_min:.4f}, "x_max": {props.x_max:.4f},\n'
new_dict += f' "y_min": {props.y_min:.4f}, "y_max": {props.y_max:.4f},\n'
new_dict += f' "z_min": {props.z_min:.4f}, "z_max": {props.z_max:.4f},\n'
new_dict += f' "thickness": {props.thickness:.4f}, "draw_plane": "{props.draw_plane}",\n'
new_dict += f' "show_eq1": {props.show_eq1}, "show_eq2": {props.show_eq2}, "show_eq3": {props.show_eq3},\n'
new_dict += f' "color1": ({c1[0]:.4f}, {c1[1]:.4f}, {c1[2]:.4f}, {c1[3]:.4f}),\n'
new_dict += f' "color2": ({c2[0]:.4f}, {c2[1]:.4f}, {c2[2]:.4f}, {c2[3]:.4f}),\n'
new_dict += f' "color3": ({c3[0]:.4f}, {c3[1]:.4f}, {c3[2]:.4f}, {c3[3]:.4f}),\n\n'
new_dict += f' # Torus 1 (Normal)\n'
new_dict += f' "t1_enable_preview": {props.t1_enable_preview}, "t1_mode": "{props.t1_mode}",\n'
new_dict += f' "t1_val_a": {props.t1_val_a:.4f}, "t1_val_b": {props.t1_val_b:.4f},\n'
new_dict += f' "t1_z_min": {props.t1_z_min:.4f}, "t1_z_max": {props.t1_z_max:.4f}, "t1_count": {props.t1_count},\n'
new_dict += f' "t1_z_center": {props.t1_z_center:.4f}, "t1_z_interval": {props.t1_z_interval:.4f}, "t1_up_down_count": {props.t1_up_down_count},\n'
new_dict += f' "t1_major_radius": {props.t1_major_radius:.4f}, "t1_minor_radius": {props.t1_minor_radius:.4f},\n'
new_dict += f' "t1_color": ({t1c[0]:.4f}, {t1c[1]:.4f}, {t1c[2]:.4f}, {t1c[3]:.4f}),\n\n'
new_dict += f' # Torus 2 (Elliptic / Lorentz)\n'
new_dict += f' "t2_enable_preview": {props.t2_enable_preview}, "t2_mode": "{props.t2_mode}",\n'
new_dict += f' "t2_val_a": {props.t2_val_a:.4f}, "t2_val_b": {props.t2_val_b:.4f},\n'
new_dict += f' "t2_z_min": {props.t2_z_min:.4f}, "t2_z_max": {props.t2_z_max:.4f}, "t2_count": {props.t2_count},\n'
new_dict += f' "t2_z_center": {props.t2_z_center:.4f}, "t2_z_interval": {props.t2_z_interval:.4f}, "t2_up_down_count": {props.t2_up_down_count},\n'
new_dict += f' "t2_major_radius": {props.t2_major_radius:.4f}, "t2_minor_radius": {props.t2_minor_radius:.4f},\n'
new_dict += f' "t2_f": {props.t2_f:.4f}, "t2_g": {props.t2_g:.4f},\n'
new_dict += f' "t2_color": ({t2c[0]:.4f}, {t2c[1]:.4f}, {t2c[2]:.4f}, {t2c[3]:.4f}),\n\n'
new_dict += f' # Cross 1\n'
new_dict += f' "c1_enable_preview": {props.c1_enable_preview},\n'
new_dict += f' "c1_center": ({props.c1_center[0]:.4f}, {props.c1_center[1]:.4f}, {props.c1_center[2]:.4f}),\n'
new_dict += f' "c1_length": {props.c1_length:.4f}, "c1_thickness": {props.c1_thickness:.4f}, "c1_plane": "{props.c1_plane}",\n'
new_dict += f' "c1_color1": ({cc1_1[0]:.4f}, {cc1_1[1]:.4f}, {cc1_1[2]:.4f}, {cc1_1[3]:.4f}),\n'
new_dict += f' "c1_color2": ({cc1_2[0]:.4f}, {cc1_2[1]:.4f}, {cc1_2[2]:.4f}, {cc1_2[3]:.4f}),\n\n'
new_dict += f' # Cross 2\n'
new_dict += f' "c2_enable_preview": {props.c2_enable_preview},\n'
new_dict += f' "c2_center": ({props.c2_center[0]:.4f}, {props.c2_center[1]:.4f}, {props.c2_center[2]:.4f}),\n'
new_dict += f' "c2_length": {props.c2_length:.4f}, "c2_thickness": {props.c2_thickness:.4f}, "c2_plane": "{props.c2_plane}",\n'
new_dict += f' "c2_color1": ({cc2_1[0]:.4f}, {cc2_1[1]:.4f}, {cc2_1[2]:.4f}, {cc2_1[3]:.4f}),\n'
new_dict += f' "c2_color2": ({cc2_2[0]:.4f}, {cc2_2[1]:.4f}, {cc2_2[2]:.4f}, {cc2_2[3]:.4f}),\n'
new_dict += "}\n"
try:
tag_start = "# <BEGIN" + "_DICT>"
tag_end = "# <END" + "_DICT>"
pre_code, rest = code.split(tag_start, 1)
_, post_code = rest.split(tag_end, 1)
final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
context.window_manager.clipboard = final_code
self.report({'INFO'}, "Code copied!")
except: return {'CANCELLED'}
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 = "Close Addon"
def execute(self, context):
bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
self.report({'INFO'}, "アドオンを終了しました。")
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_MainPanel(Panel):
bl_label = PANEL_TITLE
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: return
row_prev = layout.row(); row_prev.scale_y = 1.5
if not props.enable_preview: row_prev.operator(OT_ShowLinePreview.bl_idname, icon='PLAY', text="Show Line Preview")
else: row_prev.prop(props, "enable_preview", text="Line Preview Active", toggle=True, icon='PAUSE')
box_info = layout.box()
box_info.label(text="【 Line Info 】", icon='INFO')
a_str = f"{props.val_a:.2f}" if abs(props.val_a) > 0.0001 else "0.00(Err)"
box_info.label(text=f"y = ({props.val_b:.2f} / {a_str}) x")
box_values = layout.box()
col_v = box_values.column(align=True)
col_v.prop(props, "val_a"); col_v.prop(props, "val_b"); col_v.prop(props, "val_d")
box_limits = layout.box()
box_limits.label(text="Limits (X, Y, Z)", icon='MOD_HULL')
for axis in ['x', 'y', 'z']:
r = box_limits.row(align=True)
r.prop(props, f"{axis}_min", text=f"{axis.upper()} Min")
r.prop(props, f"{axis}_max", text="Max")
class PT_VisibilityPanel(Panel):
bl_label = "Line Design & Detach"
bl_idname = f"{PREFIX}_PT_visibility"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_parent_id = f"{PREFIX}_PT_main"
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
layout.prop(props, "thickness"); layout.prop(props, "draw_plane"); layout.separator()
for i in range(1, 4):
r = layout.row(align=True)
r.prop(props, f"show_eq{i}", text=f"Eq {i}", toggle=True); r.prop(props, f"color{i}", text="")
layout.separator()
col_exec = layout.column(); col_exec.scale_y = 1.5
col_exec.operator(OT_DetachLines.bl_idname, icon='MESH_CYLINDER', text="Detach Lines (固定化)")
class PT_Torus1Panel(Panel):
bl_label = "Torus (Normal)"
bl_idname = f"{PREFIX}_PT_torus1"
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
row_prev = layout.row(); row_prev.scale_y = 1.5
if not props.t1_enable_preview: row_prev.operator(OT_ShowTorus1Preview.bl_idname, icon='PLAY', text="Show Normal Torus")
else: row_prev.prop(props, "t1_enable_preview", text="Normal Torus Active", toggle=True, icon='PAUSE')
box_eq = layout.box()
box_eq.label(text="Center Line: z = (b/a)x", icon='IPO_CONSTANT')
col = box_eq.column(align=True)
col.prop(props, "t1_val_a"); col.prop(props, "t1_val_b")
box_r = layout.box()
box_r.prop(props, "t1_mode", text="")
if props.t1_mode == 'INTERVAL':
c_int = box_r.column(align=True)
c_int.prop(props, "t1_z_center"); c_int.prop(props, "t1_z_interval"); c_int.prop(props, "t1_up_down_count")
else:
c_rng = box_r.column(align=True)
c_rng.prop(props, "t1_z_min"); c_rng.prop(props, "t1_z_max"); c_rng.prop(props, "t1_count")
box_s = layout.box()
box_s.prop(props, "t1_major_radius"); box_s.prop(props, "t1_minor_radius"); box_s.prop(props, "t1_color")
col_exec = layout.column(); col_exec.scale_y = 1.5
col_exec.operator(OT_DetachTorus1.bl_idname, icon='MESH_TORUS', text="Detach Normal Torus")
class PT_Torus2Panel(Panel):
bl_label = "Torus (Elliptic / Lorentz)"
bl_idname = f"{PREFIX}_PT_torus2"
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
row_prev = layout.row(); row_prev.scale_y = 1.5
if not props.t2_enable_preview: row_prev.operator(OT_ShowTorus2Preview.bl_idname, icon='PLAY', text="Show Elliptic Torus")
else: row_prev.prop(props, "t2_enable_preview", text="Elliptic Torus Active", toggle=True, icon='PAUSE')
box_eq = layout.box()
box_eq.label(text="Center Line: z = (b/a)x", icon='IPO_CONSTANT')
col = box_eq.column(align=True)
col.prop(props, "t2_val_a"); col.prop(props, "t2_val_b")
box_r = layout.box()
box_r.prop(props, "t2_mode", text="")
if props.t2_mode == 'INTERVAL':
c_int = box_r.column(align=True)
c_int.prop(props, "t2_z_center"); c_int.prop(props, "t2_z_interval"); c_int.prop(props, "t2_up_down_count")
else:
c_rng = box_r.column(align=True)
c_rng.prop(props, "t2_z_min"); c_rng.prop(props, "t2_z_max"); c_rng.prop(props, "t2_count")
box_s = layout.box()
box_s.prop(props, "t2_major_radius"); box_s.prop(props, "t2_minor_radius")
box_s.separator()
box_s.label(text="楕円トーラスの収縮割合 (f/g)", icon='CON_SIZELIKE')
col_fg = box_s.column(align=True)
col_fg.prop(props, "t2_f")
col_fg.prop(props, "t2_g")
g_val = props.t2_g if abs(props.t2_g) > 0.0001 else 0.0001
box_s.label(text=f"収縮率: {props.t2_f / g_val:.4f}")
box_s.prop(props, "t2_color")
col_exec = layout.column(); col_exec.scale_y = 1.5
col_exec.operator(OT_DetachTorus2.bl_idname, icon='MESH_TORUS', text="Detach Elliptic Torus")
class PT_Cross1Panel(Panel):
bl_label = "Cross Cylinders 1"
bl_idname = f"{PREFIX}_PT_cross1"
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
row_prev = layout.row(); row_prev.scale_y = 1.5
if not props.c1_enable_preview: row_prev.operator(OT_ShowCross1Preview.bl_idname, icon='PLAY', text="Show Cross 1")
else: row_prev.prop(props, "c1_enable_preview", text="Cross 1 Active", toggle=True, icon='PAUSE')
box_s = layout.box()
box_s.prop(props, "c1_center")
box_s.prop(props, "c1_length")
box_s.prop(props, "c1_thickness")
box_s.prop(props, "c1_plane")
box_s.separator()
r1 = box_s.row(align=True)
r1.label(text="y = x"); r1.prop(props, "c1_color1", text="")
r2 = box_s.row(align=True)
r2.label(text="y = -x"); r2.prop(props, "c1_color2", text="")
col_exec = layout.column(); col_exec.scale_y = 1.5
col_exec.operator(OT_DetachCross1.bl_idname, icon='MESH_DATA', text="Detach Cross 1")
class PT_Cross2Panel(Panel):
bl_label = "Cross Cylinders 2"
bl_idname = f"{PREFIX}_PT_cross2"
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
row_prev = layout.row(); row_prev.scale_y = 1.5
if not props.c2_enable_preview: row_prev.operator(OT_ShowCross2Preview.bl_idname, icon='PLAY', text="Show Cross 2")
else: row_prev.prop(props, "c2_enable_preview", text="Cross 2 Active", toggle=True, icon='PAUSE')
box_s = layout.box()
box_s.prop(props, "c2_center")
box_s.prop(props, "c2_length")
box_s.prop(props, "c2_thickness")
box_s.prop(props, "c2_plane")
box_s.separator()
r1 = box_s.row(align=True)
r1.label(text="y = x"); r1.prop(props, "c2_color1", text="")
r2 = box_s.row(align=True)
r2.label(text="y = -x"); r2.prop(props, "c2_color2", text="")
col_exec = layout.column(); col_exec.scale_y = 1.5
col_exec.operator(OT_DetachCross2.bl_idname, icon='MESH_DATA', text="Detach Cross 2")
class PT_SystemPanel(Panel):
bl_label = "System (Copy / Close)"
bl_idname = f"{PREFIX}_PT_system"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
r_c = layout.row(); r_c.scale_y = 1.2
r_c.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
r_r = layout.row(); r_r.scale_y = 1.2; r_r.alert = True
r_r.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Close Addon (完全終了)")
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
def draw(self, context):
for l in ADDON_LINKS:
self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"], icon='URL').url = l["url"]
# ==============================================================================
# REGISTER
# ==============================================================================
classes = (
PG_EquationProps,
OT_ShowLinePreview, OT_DetachLines,
OT_ShowTorus1Preview, OT_DetachTorus1,
OT_ShowTorus2Preview, OT_DetachTorus2,
OT_ShowCross1Preview, OT_DetachCross1,
OT_ShowCross2Preview, OT_DetachCross2,
OT_CopyFullScript, OT_OpenUrl, OT_RemoveAddon,
PT_MainPanel, PT_VisibilityPanel,
PT_Torus1Panel, PT_Torus2Panel,
PT_Cross1Panel, PT_Cross2Panel,
PT_SystemPanel, PT_LinksPanel
)
def auto_open_sidebar():
try:
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':
if not space.show_region_ui: space.show_region_ui = True
except: pass
return None
def register():
for c in classes:
try: bpy.utils.register_class(c)
except ValueError: pass
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_EquationProps))
bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)
def unregister():
global _timer
if _timer is not None:
try: bpy.app.timers.unregister(_timer)
except Exception: pass
_timer = None
cleanup_preview_data()
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except ValueError: pass
if __name__ == "__main__":
register()
# Copied: 2026-04-08 12:00:00
import bpy
import time
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime
# ==============================================================================
# 【 基本設定エリア 】
# ==============================================================================
PREFIX = "EqGen"
ADDON_NAME = "[ Equation Gen ]"
TAB_NAME = "[ Equation Gen ]"
PANEL_TITLE = "Equation Lines"
AUTHOR = "zionadchat"
# ★ このスクリプト自身のID(絶対に消さないこと)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: EQ_LINES_2026_04_08_V5_3 ###"
# ==============================================================================
# システム初期化 & ID管理
# ==============================================================================
bl_info = {
"name": f"{ADDON_NAME} {PREFIX}",
"author": AUTHOR,
"version": (5, 3, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": "Lines, Normal Torus, and Elliptic Torus Generators (Independent)",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
ADDON_LINKS = (
{"label": "Prefix 20260408", "url": "<https://www.notion.so/Prefix-20260408-33cf5dacaf43807e9e35ff8cdbbc39c6>"},
)
# ==============================================================================
# デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
# Line
"enable_preview": False,
"val_a": 0.6000,
"val_b": 1.0000,
"val_d": 10.0000,
"x_min": -50.0, "x_max": 50.0,
"y_min": -50.0, "y_max": 50.0,
"z_min": -50.0, "z_max": 50.0,
"thickness": 0.5000,
"draw_plane": "XZ",
"show_eq1": True, "show_eq2": True, "show_eq3": True,
"color1": (1.0000, 0.2000, 0.2000, 1.0000),
"color2": (0.2000, 1.0000, 0.2000, 1.0000),
"color3": (0.2000, 0.2000, 1.0000, 1.0000),
# Torus 1 (Normal)
"t1_enable_preview": False,
"t1_mode": "INTERVAL",
"t1_val_a": 0.6000, "t1_val_b": 1.0000,
"t1_z_min": -50.0, "t1_z_max": 50.0, "t1_count": 11,
"t1_z_center": 0.0, "t1_z_interval": 1.0, "t1_up_down_count": 5,
"t1_major_radius": 5.0, "t1_minor_radius": 1.0,
"t1_color": (0.2000, 0.8000, 0.8000, 1.0000), # Cyan
# Torus 2 (Elliptic / Lorentz)
"t2_enable_preview": False,
"t2_mode": "INTERVAL",
"t2_val_a": 0.6000, "t2_val_b": 1.0000,
"t2_z_min": -50.0, "t2_z_max": 50.0, "t2_count": 11,
"t2_z_center": 0.0, "t2_z_interval": 1.0, "t2_up_down_count": 5,
"t2_major_radius": 5.0, "t2_minor_radius": 1.0,
"t2_scale_x": 0.8000, # Lorentz contraction at v=0.6c
"t2_color": (0.8000, 0.2000, 0.8000, 1.0000), # Magenta
}
# <END_DICT>
PREVIEW_COL_LINE = f"{PREFIX}_Line_Preview"
PREVIEW_COL_TORUS1 = f"{PREFIX}_Torus1_Preview"
PREVIEW_COL_TORUS2 = f"{PREFIX}_Torus2_Preview"
# ==============================================================================
# 共通マテリアル・データ管理 ロジック
# ==============================================================================
def cleanup_preview_data():
for name in [PREVIEW_COL_LINE, PREVIEW_COL_TORUS1, PREVIEW_COL_TORUS2]:
col = bpy.data.collections.get(name)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0:
if isinstance(data, bpy.types.Curve):
bpy.data.curves.remove(data)
if len(col.objects) == 0:
bpy.data.collections.remove(col)
def apply_material_settings(mat, color):
mat.use_nodes = True
mat.blend_method = 'BLEND'
mat.diffuse_color = color
tree = mat.node_tree
bsdf = tree.nodes.get("Principled BSDF")
if not bsdf:
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.name = "Principled BSDF"
out = tree.nodes.new("ShaderNodeOutputMaterial")
tree.links.new(bsdf.outputs[0], out.inputs[0])
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
def get_preview_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name=name)
apply_material_settings(mat, color)
return mat
def build_curve_circle(curve, radius, segments=32):
if len(curve.splines) == 0:
spline = curve.splines.new('POLY')
spline.points.add(segments - 1)
spline.use_cyclic_u = True
else:
spline = curve.splines[0]
if len(spline.points) != segments:
curve.splines.clear()
spline = curve.splines.new('POLY')
spline.points.add(segments - 1)
spline.use_cyclic_u = True
for i in range(segments):
angle = 2 * math.pi * i / segments
spline.points[i].co = (radius * math.cos(angle), radius * math.sin(angle), 0.0, 1.0)
# ==============================================================================
# Line プレビューロジック
# ==============================================================================
def get_clipped_segment(m, c, x_min, x_max, v_min, v_max):
if m == 0:
if v_min <= c <= v_max:
return x_min, x_max
return None, None
else:
x_from_v1, x_from_v2 = (v_min - c) / m, (v_max - c) / m
valid_x_min, valid_x_max = min(x_from_v1, x_from_v2), max(x_from_v1, x_from_v2)
act_x_min, act_x_max = max(x_min, valid_x_min), min(x_max, valid_x_max)
if act_x_min > act_x_max:
return None, None
return act_x_min, act_x_max
def calc_points(props, m, c):
x_min, x_max = min(props.x_min, props.x_max), max(props.x_min, props.x_max)
y_min, y_max = min(props.y_min, props.y_max), max(props.y_min, props.y_max)
z_min, z_max = min(props.z_min, props.z_max), max(props.z_min, props.z_max)
if props.draw_plane == 'XZ':
act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, z_min, z_max)
if act_x_min is None: return None, None
return (act_x_min, 0.0, m * act_x_min + c), (act_x_max, 0.0, m * act_x_max + c)
else:
act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, y_min, y_max)
if act_x_min is None: return None, None
return (act_x_min, m * act_x_min + c, 0.0), (act_x_max, m * act_x_max + c, 0.0)
def update_line_preview(context, props):
if not props.enable_preview:
col = bpy.data.collections.get(PREVIEW_COL_LINE)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
bpy.data.collections.remove(col)
return
col = bpy.data.collections.get(PREVIEW_COL_LINE)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_LINE)
context.scene.collection.children.link(col)
a = props.val_a if abs(props.val_a) > 0.0001 else 0.0001
b, d = props.val_b, props.val_d
m = b / a
equations = [
{"id": 1, "show": props.show_eq1, "color": props.color1, "offset": 0.0},
{"id": 2, "show": props.show_eq2, "color": props.color2, "offset": -d},
{"id": 3, "show": props.show_eq3, "color": props.color3, "offset": d},
]
for eq in equations:
obj_name = f"[Preview] EqLine_{eq['id']}"
obj = bpy.data.objects.get(obj_name)
if not eq["show"]:
if obj: obj.hide_viewport = obj.hide_render = True
continue
p1, p2 = calc_points(props, m, eq["offset"])
if p1 is None:
if obj: obj.hide_viewport = obj.hide_render = True
continue
if not obj:
curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
curve.dimensions = '3D'
curve.fill_mode = 'FULL'
spline = curve.splines.new('POLY')
spline.points.add(1)
obj = bpy.data.objects.new(obj_name, curve)
col.objects.link(obj)
else:
curve = obj.data
spline = curve.splines[0]
curve.bevel_depth = props.thickness
curve.bevel_resolution = 6
spline.points[0].co = (*p1, 1.0)
spline.points[1].co = (*p2, 1.0)
obj.hide_viewport = obj.hide_render = False
mat = get_preview_material(f"Preview_Mat_EqLine{eq['id']}", eq["color"])
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
# ==============================================================================
# Torus 1 (Normal) プレビューロジック
# ==============================================================================
def update_torus1_preview(context, props):
if not props.t1_enable_preview:
col = bpy.data.collections.get(PREVIEW_COL_TORUS1)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
bpy.data.collections.remove(col)
return
col = bpy.data.collections.get(PREVIEW_COL_TORUS1)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_TORUS1)
context.scene.collection.children.link(col)
a = props.t1_val_a
b_val = props.t1_val_b if abs(props.t1_val_b) > 0.0001 else (0.0001 if props.t1_val_b >= 0 else -0.0001)
z_list = []
if props.t1_mode == 'RANGE':
count = props.t1_count
for i in range(count):
t = i / (count - 1) if count > 1 else 0.5
z = props.t1_z_min + t * (props.t1_z_max - props.t1_z_min)
z_list.append(z)
else:
center = props.t1_z_center
interval = props.t1_z_interval
ud_count = props.t1_up_down_count
for i in range(-ud_count, ud_count + 1):
z_list.append(center + i * interval)
existing_objs = list(col.objects)
mat = get_preview_material("Preview_Mat_Torus1", props.t1_color)
for i, z in enumerate(z_list):
obj_name = f"[Preview] Normal_Torus_{i+1}"
x = z * (a / b_val)
y = 0.0
if i < len(existing_objs):
obj = existing_objs[i]
curve = obj.data
else:
curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
curve.dimensions = '3D'
curve.fill_mode = 'FULL'
obj = bpy.data.objects.new(obj_name, curve)
col.objects.link(obj)
curve.bevel_depth = props.t1_minor_radius
curve.bevel_resolution = 8
build_curve_circle(curve, props.t1_major_radius)
obj.location = (x, y, z)
obj.scale = (1.0, 1.0, 1.0)
obj.hide_viewport = False
obj.hide_render = False
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
if len(existing_objs) > len(z_list):
for obj in existing_objs[len(z_list):]:
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
# ==============================================================================
# Torus 2 (Elliptic) プレビューロジック
# ==============================================================================
def update_torus2_preview(context, props):
if not props.t2_enable_preview:
col = bpy.data.collections.get(PREVIEW_COL_TORUS2)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
bpy.data.collections.remove(col)
return
col = bpy.data.collections.get(PREVIEW_COL_TORUS2)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_TORUS2)
context.scene.collection.children.link(col)
a = props.t2_val_a
b_val = props.t2_val_b if abs(props.t2_val_b) > 0.0001 else (0.0001 if props.t2_val_b >= 0 else -0.0001)
z_list = []
if props.t2_mode == 'RANGE':
count = props.t2_count
for i in range(count):
t = i / (count - 1) if count > 1 else 0.5
z = props.t2_z_min + t * (props.t2_z_max - props.t2_z_min)
z_list.append(z)
else:
center = props.t2_z_center
interval = props.t2_z_interval
ud_count = props.t2_up_down_count
for i in range(-ud_count, ud_count + 1):
z_list.append(center + i * interval)
existing_objs = list(col.objects)
mat = get_preview_material("Preview_Mat_Torus2", props.t2_color)
for i, z in enumerate(z_list):
obj_name = f"[Preview] Elliptic_Torus_{i+1}"
x = z * (a / b_val)
y = 0.0
if i < len(existing_objs):
obj = existing_objs[i]
curve = obj.data
else:
curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
curve.dimensions = '3D'
curve.fill_mode = 'FULL'
obj = bpy.data.objects.new(obj_name, curve)
col.objects.link(obj)
curve.bevel_depth = props.t2_minor_radius
curve.bevel_resolution = 8
build_curve_circle(curve, props.t2_major_radius)
obj.location = (x, y, z)
obj.scale = (props.t2_scale_x, 1.0, 1.0) # Lorentz Contraction
obj.hide_viewport = False
obj.hide_render = False
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
if len(existing_objs) > len(z_list):
for obj in existing_objs[len(z_list):]:
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
# ==============================================================================
# タイマー管理
# ==============================================================================
_timer = None
_last_update_time = 0
def delayed_update():
global _timer, _last_update_time
_timer = None
now = time.time()
if now - _last_update_time < 0.05:
if _timer is None:
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
return None
_last_update_time = now
ctx = bpy.context
if not ctx or not ctx.scene: return None
if ctx.object and ctx.object.mode != 'OBJECT': return None
props = getattr(ctx.scene, PROPS_NAME, None)
if props:
update_line_preview(ctx, props)
update_torus1_preview(ctx, props)
update_torus2_preview(ctx, props)
return None
def on_update(self, context):
global _timer
if _timer is None:
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_EquationProps(PropertyGroup):
# Line Properties
enable_preview: BoolProperty(name="Enable Line Preview", default=CURRENT_DEFAULTS['enable_preview'], update=on_update)
val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['val_a'], update=on_update)
val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['val_b'], update=on_update)
val_d: FloatProperty(name="Value d", default=CURRENT_DEFAULTS['val_d'], update=on_update)
x_min: FloatProperty(name="X Min", default=CURRENT_DEFAULTS['x_min'], update=on_update)
x_max: FloatProperty(name="X Max", default=CURRENT_DEFAULTS['x_max'], update=on_update)
y_min: FloatProperty(name="Y Min", default=CURRENT_DEFAULTS['y_min'], update=on_update)
y_max: FloatProperty(name="Y Max", default=CURRENT_DEFAULTS['y_max'], update=on_update)
z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['z_min'], update=on_update)
z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['z_max'], update=on_update)
thickness: FloatProperty(name="Line Thickness", default=CURRENT_DEFAULTS['thickness'], min=0.01, max=10.0, update=on_update)
draw_plane: EnumProperty(name="Draw Plane", items=[('XZ', "Front (XZ)", "XZ Plane"), ('XY', "Top (XY)", "XY Plane")], default=CURRENT_DEFAULTS['draw_plane'], update=on_update)
show_eq1: BoolProperty(name="Show Eq 1", default=CURRENT_DEFAULTS['show_eq1'], update=on_update)
show_eq2: BoolProperty(name="Show Eq 2", default=CURRENT_DEFAULTS['show_eq2'], update=on_update)
show_eq3: BoolProperty(name="Show Eq 3", default=CURRENT_DEFAULTS['show_eq3'], update=on_update)
color1: FloatVectorProperty(name="Color 1", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color1'], update=on_update)
color2: FloatVectorProperty(name="Color 2", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color2'], update=on_update)
color3: FloatVectorProperty(name="Color 3", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color3'], update=on_update)
# Torus 1 (Normal)
t1_enable_preview: BoolProperty(name="Enable Torus 1 Preview", default=CURRENT_DEFAULTS['t1_enable_preview'], update=on_update)
t1_mode: EnumProperty(name="Mode", items=[('INTERVAL', "Interval Mode", ""), ('RANGE', "Range Mode", "")], default=CURRENT_DEFAULTS['t1_mode'], update=on_update)
t1_val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['t1_val_a'], update=on_update)
t1_val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['t1_val_b'], update=on_update)
t1_z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['t1_z_min'], update=on_update)
t1_z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['t1_z_max'], update=on_update)
t1_count: IntProperty(name="Total Count", default=CURRENT_DEFAULTS['t1_count'], min=1, max=500, update=on_update)
t1_z_center: FloatProperty(name="Z Center", default=CURRENT_DEFAULTS['t1_z_center'], update=on_update)
t1_z_interval: FloatProperty(name="Z Interval", default=CURRENT_DEFAULTS['t1_z_interval'], update=on_update)
t1_up_down_count: IntProperty(name="Up/Down Count", default=CURRENT_DEFAULTS['t1_up_down_count'], min=0, max=100, update=on_update)
t1_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['t1_major_radius'], min=0.1, max=100.0, update=on_update)
t1_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['t1_minor_radius'], min=0.01, max=50.0, update=on_update)
t1_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['t1_color'], update=on_update)
# Torus 2 (Elliptic)
t2_enable_preview: BoolProperty(name="Enable Torus 2 Preview", default=CURRENT_DEFAULTS['t2_enable_preview'], update=on_update)
t2_mode: EnumProperty(name="Mode", items=[('INTERVAL', "Interval Mode", ""), ('RANGE', "Range Mode", "")], default=CURRENT_DEFAULTS['t2_mode'], update=on_update)
t2_val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['t2_val_a'], update=on_update)
t2_val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['t2_val_b'], update=on_update)
t2_z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['t2_z_min'], update=on_update)
t2_z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['t2_z_max'], update=on_update)
t2_count: IntProperty(name="Total Count", default=CURRENT_DEFAULTS['t2_count'], min=1, max=500, update=on_update)
t2_z_center: FloatProperty(name="Z Center", default=CURRENT_DEFAULTS['t2_z_center'], update=on_update)
t2_z_interval: FloatProperty(name="Z Interval", default=CURRENT_DEFAULTS['t2_z_interval'], update=on_update)
t2_up_down_count: IntProperty(name="Up/Down Count", default=CURRENT_DEFAULTS['t2_up_down_count'], min=0, max=100, update=on_update)
t2_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['t2_major_radius'], min=0.1, max=100.0, update=on_update)
t2_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['t2_minor_radius'], min=0.01, max=50.0, update=on_update)
t2_scale_x: FloatProperty(name="X Scale", default=CURRENT_DEFAULTS['t2_scale_x'], min=0.01, max=5.0, update=on_update)
t2_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['t2_color'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_ShowLinePreview(Operator):
bl_idname = f"{OP_PREFIX}.show_line_preview"; bl_label = "Show Line Preview"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props: props.enable_preview = True; update_line_preview(context, props)
return {'FINISHED'}
class OT_DetachLines(Operator):
bl_idname = f"{OP_PREFIX}.detach_lines"; bl_label = "Detach Lines"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col_preview = bpy.data.collections.get(PREVIEW_COL_LINE)
if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
target_col = context.collection
timestamp = datetime.now().strftime('%H%M%S')
bpy.ops.object.select_all(action='DESELECT')
for obj in list(col_preview.objects):
if obj.hide_viewport: continue
if obj.name not in target_col.objects: target_col.objects.link(obj)
col_preview.objects.unlink(obj)
obj.name = obj.name.replace("[Preview]", "Solid_EqLine") + f"_{timestamp}"
if obj.data.materials:
mat = obj.data.materials[0]
new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
obj.data.materials[0] = new_mat
obj.select_set(True)
props = getattr(context.scene, PROPS_NAME, None)
if props: update_line_preview(context, props)
return {'FINISHED'}
class OT_ShowTorus1Preview(Operator):
bl_idname = f"{OP_PREFIX}.show_torus1_preview"; bl_label = "Show Normal Torus Preview"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props: props.t1_enable_preview = True; update_torus1_preview(context, props)
return {'FINISHED'}
class OT_DetachTorus1(Operator):
bl_idname = f"{OP_PREFIX}.detach_torus1"; bl_label = "Detach Normal Torus"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col_preview = bpy.data.collections.get(PREVIEW_COL_TORUS1)
if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
target_col = context.collection
timestamp = datetime.now().strftime('%H%M%S')
bpy.ops.object.select_all(action='DESELECT')
for obj in list(col_preview.objects):
if obj.name not in target_col.objects: target_col.objects.link(obj)
col_preview.objects.unlink(obj)
obj.name = obj.name.replace("[Preview]", "Solid_Normal_Torus") + f"_{timestamp}"
if obj.data.materials:
mat = obj.data.materials[0]
new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
obj.data.materials[0] = new_mat
bpy.context.view_layer.objects.active = obj; obj.select_set(True)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
obj.select_set(False)
props = getattr(context.scene, PROPS_NAME, None)
if props: update_torus1_preview(context, props)
return {'FINISHED'}
class OT_ShowTorus2Preview(Operator):
bl_idname = f"{OP_PREFIX}.show_torus2_preview"; bl_label = "Show Elliptic Torus Preview"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props: props.t2_enable_preview = True; update_torus2_preview(context, props)
return {'FINISHED'}
class OT_DetachTorus2(Operator):
bl_idname = f"{OP_PREFIX}.detach_torus2"; bl_label = "Detach Elliptic Torus"; bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col_preview = bpy.data.collections.get(PREVIEW_COL_TORUS2)
if not col_preview or len(col_preview.objects) == 0: return {'CANCELLED'}
target_col = context.collection
timestamp = datetime.now().strftime('%H%M%S')
bpy.ops.object.select_all(action='DESELECT')
for obj in list(col_preview.objects):
if obj.name not in target_col.objects: target_col.objects.link(obj)
col_preview.objects.unlink(obj)
obj.name = obj.name.replace("[Preview]", "Solid_Elliptic_Torus") + f"_{timestamp}"
if obj.data.materials:
mat = obj.data.materials[0]
new_mat = mat.copy(); new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
obj.data.materials[0] = new_mat
bpy.context.view_layer.objects.active = obj; obj.select_set(True)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
obj.select_set(False)
props = getattr(context.scene, PROPS_NAME, None)
if props: update_torus2_preview(context, props)
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string(): target_text = t; break
if not target_text: return {'CANCELLED'}
code = target_text.as_string()
c1, c2, c3, t1c, t2c = props.color1, props.color2, props.color3, props.t1_color, props.t2_color
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "enable_preview": {props.enable_preview},\n'
new_dict += f' "val_a": {props.val_a:.4f}, "val_b": {props.val_b:.4f}, "val_d": {props.val_d:.4f},\n'
new_dict += f' "x_min": {props.x_min:.4f}, "x_max": {props.x_max:.4f},\n'
new_dict += f' "y_min": {props.y_min:.4f}, "y_max": {props.y_max:.4f},\n'
new_dict += f' "z_min": {props.z_min:.4f}, "z_max": {props.z_max:.4f},\n'
new_dict += f' "thickness": {props.thickness:.4f}, "draw_plane": "{props.draw_plane}",\n'
new_dict += f' "show_eq1": {props.show_eq1}, "show_eq2": {props.show_eq2}, "show_eq3": {props.show_eq3},\n'
new_dict += f' "color1": ({c1[0]:.4f}, {c1[1]:.4f}, {c1[2]:.4f}, {c1[3]:.4f}),\n'
new_dict += f' "color2": ({c2[0]:.4f}, {c2[1]:.4f}, {c2[2]:.4f}, {c2[3]:.4f}),\n'
new_dict += f' "color3": ({c3[0]:.4f}, {c3[1]:.4f}, {c3[2]:.4f}, {c3[3]:.4f}),\n\n'
new_dict += f' "t1_enable_preview": {props.t1_enable_preview}, "t1_mode": "{props.t1_mode}",\n'
new_dict += f' "t1_val_a": {props.t1_val_a:.4f}, "t1_val_b": {props.t1_val_b:.4f},\n'
new_dict += f' "t1_z_min": {props.t1_z_min:.4f}, "t1_z_max": {props.t1_z_max:.4f}, "t1_count": {props.t1_count},\n'
new_dict += f' "t1_z_center": {props.t1_z_center:.4f}, "t1_z_interval": {props.t1_z_interval:.4f}, "t1_up_down_count": {props.t1_up_down_count},\n'
new_dict += f' "t1_major_radius": {props.t1_major_radius:.4f}, "t1_minor_radius": {props.t1_minor_radius:.4f},\n'
new_dict += f' "t1_color": ({t1c[0]:.4f}, {t1c[1]:.4f}, {t1c[2]:.4f}, {t1c[3]:.4f}),\n\n'
new_dict += f' "t2_enable_preview": {props.t2_enable_preview}, "t2_mode": "{props.t2_mode}",\n'
new_dict += f' "t2_val_a": {props.t2_val_a:.4f}, "t2_val_b": {props.t2_val_b:.4f},\n'
new_dict += f' "t2_z_min": {props.t2_z_min:.4f}, "t2_z_max": {props.t2_z_max:.4f}, "t2_count": {props.t2_count},\n'
new_dict += f' "t2_z_center": {props.t2_z_center:.4f}, "t2_z_interval": {props.t2_z_interval:.4f}, "t2_up_down_count": {props.t2_up_down_count},\n'
new_dict += f' "t2_major_radius": {props.t2_major_radius:.4f}, "t2_minor_radius": {props.t2_minor_radius:.4f},\n'
new_dict += f' "t2_scale_x": {props.t2_scale_x:.4f},\n'
new_dict += f' "t2_color": ({t2c[0]:.4f}, {t2c[1]:.4f}, {t2c[2]:.4f}, {t2c[3]:.4f}),\n'
new_dict += "}\n"
try:
tag_start = "# <BEGIN" + "_DICT>"
tag_end = "# <END" + "_DICT>"
pre_code, rest = code.split(tag_start, 1)
_, post_code = rest.split(tag_end, 1)
final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
context.window_manager.clipboard = final_code
self.report({'INFO'}, "Code copied!")
except: return {'CANCELLED'}
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 = "Close Addon"
def execute(self, context):
bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
self.report({'INFO'}, "アドオンを終了しました。")
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_MainPanel(Panel):
bl_label = PANEL_TITLE
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: return
row_prev = layout.row()
row_prev.scale_y = 1.5
if not props.enable_preview: row_prev.operator(OT_ShowLinePreview.bl_idname, icon='PLAY', text="Show Line Preview (表示開始)")
else: row_prev.prop(props, "enable_preview", text="Line Preview Active", toggle=True, icon='PAUSE')
box_info = layout.box()
box_info.label(text="【 Line Info 】", icon='INFO')
a_str = f"{props.val_a:.2f}" if abs(props.val_a) > 0.0001 else "0.00(Err)"
box_info.label(text=f"y = ({props.val_b:.2f} / {a_str}) x")
box_values = layout.box()
col_v = box_values.column(align=True)
col_v.prop(props, "val_a"); col_v.prop(props, "val_b"); col_v.prop(props, "val_d")
box_limits = layout.box()
box_limits.label(text="Limits (X, Y, Z)", icon='MOD_HULL')
for axis in ['x', 'y', 'z']:
r = box_limits.row(align=True)
r.prop(props, f"{axis}_min", text=f"{axis.upper()} Min")
r.prop(props, f"{axis}_max", text="Max")
class PT_VisibilityPanel(Panel):
bl_label = "Line Design & Visibility"
bl_idname = f"{PREFIX}_PT_visibility"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_parent_id = f"{PREFIX}_PT_main"
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
layout.prop(props, "thickness"); layout.prop(props, "draw_plane"); layout.separator()
for i in range(1, 4):
r = layout.row(align=True)
r.prop(props, f"show_eq{i}", text=f"Eq {i}", toggle=True); r.prop(props, f"color{i}", text="")
class PT_CreatePanel(Panel):
bl_label = "Line Detach"
bl_idname = f"{PREFIX}_PT_create"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_parent_id = f"{PREFIX}_PT_main"
def draw(self, context):
col_exec = self.layout.column(); col_exec.scale_y = 2.0
col_exec.operator(OT_DetachLines.bl_idname, icon='MESH_CYLINDER', text="Detach Lines (位置を固定して切り離し)")
class PT_Torus1Panel(Panel):
bl_label = "Torus Generator (Normal)"
bl_idname = f"{PREFIX}_PT_torus1"
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
row_prev = layout.row(); row_prev.scale_y = 1.5
if not props.t1_enable_preview: row_prev.operator(OT_ShowTorus1Preview.bl_idname, icon='PLAY', text="Show Normal Torus Preview")
else: row_prev.prop(props, "t1_enable_preview", text="Normal Torus Active", toggle=True, icon='PAUSE')
box_eq = layout.box()
box_eq.label(text="Center Line: z = (b/a)x", icon='IPO_CONSTANT')
col = box_eq.column(align=True)
col.prop(props, "t1_val_a"); col.prop(props, "t1_val_b")
box_r = layout.box()
box_r.prop(props, "t1_mode", text="")
if props.t1_mode == 'INTERVAL':
c_int = box_r.column(align=True)
c_int.prop(props, "t1_z_center"); c_int.prop(props, "t1_z_interval"); c_int.prop(props, "t1_up_down_count")
else:
c_rng = box_r.column(align=True)
c_rng.prop(props, "t1_z_min"); c_rng.prop(props, "t1_z_max"); c_rng.prop(props, "t1_count")
box_s = layout.box()
box_s.prop(props, "t1_major_radius"); box_s.prop(props, "t1_minor_radius"); box_s.prop(props, "t1_color")
col_exec = layout.column(); col_exec.scale_y = 1.5
col_exec.operator(OT_DetachTorus1.bl_idname, icon='MESH_TORUS', text="Detach Normal Torus")
class PT_Torus2Panel(Panel):
bl_label = "Torus Generator (Elliptic / Lorentz)"
bl_idname = f"{PREFIX}_PT_torus2"
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
row_prev = layout.row(); row_prev.scale_y = 1.5
if not props.t2_enable_preview: row_prev.operator(OT_ShowTorus2Preview.bl_idname, icon='PLAY', text="Show Elliptic Torus Preview")
else: row_prev.prop(props, "t2_enable_preview", text="Elliptic Torus Active", toggle=True, icon='PAUSE')
box_eq = layout.box()
box_eq.label(text="Center Line: z = (b/a)x", icon='IPO_CONSTANT')
col = box_eq.column(align=True)
col.prop(props, "t2_val_a"); col.prop(props, "t2_val_b")
box_r = layout.box()
box_r.prop(props, "t2_mode", text="")
if props.t2_mode == 'INTERVAL':
c_int = box_r.column(align=True)
c_int.prop(props, "t2_z_center"); c_int.prop(props, "t2_z_interval"); c_int.prop(props, "t2_up_down_count")
else:
c_rng = box_r.column(align=True)
c_rng.prop(props, "t2_z_min"); c_rng.prop(props, "t2_z_max"); c_rng.prop(props, "t2_count")
box_s = layout.box()
box_s.prop(props, "t2_major_radius"); box_s.prop(props, "t2_minor_radius")
box_s.separator()
box_s.label(text="Lorentz Contraction (Scale)", icon='CON_SIZELIKE')
box_s.prop(props, "t2_scale_x", text="X Scale (v=0.6c -> 0.8)")
box_s.prop(props, "t2_color")
col_exec = layout.column(); col_exec.scale_y = 1.5
col_exec.operator(OT_DetachTorus2.bl_idname, icon='MESH_TORUS', text="Detach Elliptic Torus")
class PT_SystemPanel(Panel):
bl_label = "System (Copy / Close)"
bl_idname = f"{PREFIX}_PT_system"
bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
r_c = layout.row(); r_c.scale_y = 1.2
r_c.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
r_r = layout.row(); r_r.scale_y = 1.2; r_r.alert = True
r_r.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Close Addon (完全終了)")
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
def draw(self, context):
for l in ADDON_LINKS:
self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"], icon='URL').url = l["url"]
# ==============================================================================
# REGISTER
# ==============================================================================
classes = (
PG_EquationProps,
OT_ShowLinePreview, OT_DetachLines,
OT_ShowTorus1Preview, OT_DetachTorus1,
OT_ShowTorus2Preview, OT_DetachTorus2,
OT_CopyFullScript, OT_OpenUrl, OT_RemoveAddon,
PT_MainPanel, PT_VisibilityPanel, PT_CreatePanel,
PT_Torus1Panel, PT_Torus2Panel, PT_SystemPanel, PT_LinksPanel
)
def auto_open_sidebar():
try:
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':
if not space.show_region_ui: space.show_region_ui = True
except: pass
return None
def register():
for c in classes:
try: bpy.utils.register_class(c)
except ValueError: pass
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_EquationProps))
bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)
def unregister():
global _timer
if _timer is not None:
try: bpy.app.timers.unregister(_timer)
except Exception: pass
_timer = None
cleanup_preview_data()
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except ValueError: pass
if __name__ == "__main__":
register()
# Copied: 2026-04-08 12:00:00
import bpy
import time
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime
# ==============================================================================
# 【 基本設定エリア 】
# ==============================================================================
PREFIX = "EqGen"
ADDON_NAME = "[ Equation Gen ]"
TAB_NAME = "[ Equation Gen ]"
PANEL_TITLE = "Equation Lines"
AUTHOR = "zionadchat"
# ★ このスクリプト自身のID(絶対に消さないこと)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: EQ_LINES_2026_04_08_V5_1 ###"
# ==============================================================================
# システム初期化 & ID管理
# ==============================================================================
bl_info = {
"name": f"{ADDON_NAME} {PREFIX}",
"author": AUTHOR,
"version": (5, 1, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": "Equation Lines & Torus Generator with independent calculations",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
ADDON_LINKS = (
{"label": "Prefix 20260408", "url": "<https://www.notion.so/Prefix-20260408-33cf5dacaf43807e9e35ff8cdbbc39c6>"},
)
# ==============================================================================
# デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"enable_preview": False,
"val_a": 0.6000,
"val_b": 1.0000,
"val_d": 10.0000,
"x_min": -50.0,
"x_max": 50.0,
"y_min": -50.0,
"y_max": 50.0,
"z_min": -50.0,
"z_max": 50.0,
"thickness": 0.5000,
"draw_plane": "XZ",
"show_eq1": True,
"show_eq2": True,
"show_eq3": True,
"color1": (1.0000, 0.2000, 0.2000, 1.0000),
"color2": (0.2000, 1.0000, 0.2000, 1.0000),
"color3": (0.2000, 0.2000, 1.0000, 1.0000),
# Torus Properties
"t_enable_preview": False,
"t_mode": "INTERVAL", # 'RANGE' または 'INTERVAL'
"t_val_a": 0.6000,
"t_val_b": 1.0000,
# RANGE Mode defaults
"t_z_min": -50.0,
"t_z_max": 50.0,
"t_count": 11,
# INTERVAL Mode defaults
"t_z_center": 0.0,
"t_z_interval": 1.0,
"t_up_down_count": 5,
"t_major_radius": 5.0,
"t_minor_radius": 1.0,
"t_color": (0.2000, 0.8000, 0.8000, 1.0000),
}
# <END_DICT>
PREVIEW_COL_LINE = f"{PREFIX}_Line_Preview"
PREVIEW_COL_TORUS = f"{PREFIX}_Torus_Preview"
# ==============================================================================
# 共通マテリアル・データ管理 ロジック
# ==============================================================================
def cleanup_preview_data():
for name in [PREVIEW_COL_LINE, PREVIEW_COL_TORUS]:
col = bpy.data.collections.get(name)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0:
if isinstance(data, bpy.types.Curve):
bpy.data.curves.remove(data)
if len(col.objects) == 0:
bpy.data.collections.remove(col)
def apply_material_settings(mat, color):
mat.use_nodes = True
mat.blend_method = 'BLEND'
mat.diffuse_color = color
tree = mat.node_tree
bsdf = tree.nodes.get("Principled BSDF")
if not bsdf:
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.name = "Principled BSDF"
out = tree.nodes.new("ShaderNodeOutputMaterial")
tree.links.new(bsdf.outputs[0], out.inputs[0])
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
def get_preview_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name=name)
apply_material_settings(mat, color)
return mat
# ==============================================================================
# Line プレビューロジック
# ==============================================================================
def get_clipped_segment(m, c, x_min, x_max, v_min, v_max):
if m == 0:
if v_min <= c <= v_max:
return x_min, x_max
return None, None
else:
x_from_v1, x_from_v2 = (v_min - c) / m, (v_max - c) / m
valid_x_min, valid_x_max = min(x_from_v1, x_from_v2), max(x_from_v1, x_from_v2)
act_x_min, act_x_max = max(x_min, valid_x_min), min(x_max, valid_x_max)
if act_x_min > act_x_max:
return None, None
return act_x_min, act_x_max
def calc_points(props, m, c):
x_min, x_max = min(props.x_min, props.x_max), max(props.x_min, props.x_max)
y_min, y_max = min(props.y_min, props.y_max), max(props.y_min, props.y_max)
z_min, z_max = min(props.z_min, props.z_max), max(props.z_min, props.z_max)
if props.draw_plane == 'XZ':
act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, z_min, z_max)
if act_x_min is None: return None, None
return (act_x_min, 0.0, m * act_x_min + c), (act_x_max, 0.0, m * act_x_max + c)
else:
act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, y_min, y_max)
if act_x_min is None: return None, None
return (act_x_min, m * act_x_min + c, 0.0), (act_x_max, m * act_x_max + c, 0.0)
def update_line_preview(context, props):
if not props.enable_preview:
col = bpy.data.collections.get(PREVIEW_COL_LINE)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
bpy.data.collections.remove(col)
return
col = bpy.data.collections.get(PREVIEW_COL_LINE)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_LINE)
context.scene.collection.children.link(col)
a = props.val_a if abs(props.val_a) > 0.0001 else 0.0001
b, d = props.val_b, props.val_d
m = b / a
equations = [
{"id": 1, "show": props.show_eq1, "color": props.color1, "offset": 0.0},
{"id": 2, "show": props.show_eq2, "color": props.color2, "offset": -d},
{"id": 3, "show": props.show_eq3, "color": props.color3, "offset": d},
]
for eq in equations:
obj_name = f"[Preview] EqLine_{eq['id']}"
obj = bpy.data.objects.get(obj_name)
if not eq["show"]:
if obj: obj.hide_viewport = obj.hide_render = True
continue
p1, p2 = calc_points(props, m, eq["offset"])
if p1 is None:
if obj: obj.hide_viewport = obj.hide_render = True
continue
if not obj:
curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
curve.dimensions = '3D'
curve.fill_mode = 'FULL'
spline = curve.splines.new('POLY')
spline.points.add(1)
obj = bpy.data.objects.new(obj_name, curve)
col.objects.link(obj)
else:
curve = obj.data
spline = curve.splines[0]
curve.bevel_depth = props.thickness
curve.bevel_resolution = 6
spline.points[0].co = (*p1, 1.0)
spline.points[1].co = (*p2, 1.0)
obj.hide_viewport = obj.hide_render = False
mat = get_preview_material(f"Preview_Mat_EqLine{eq['id']}", eq["color"])
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
# ==============================================================================
# Torus プレビューロジック (Z=0平面に平行)
# ==============================================================================
def build_curve_circle(curve, radius, segments=32):
if len(curve.splines) == 0:
spline = curve.splines.new('POLY')
spline.points.add(segments - 1)
spline.use_cyclic_u = True
else:
spline = curve.splines[0]
if len(spline.points) != segments:
curve.splines.clear()
spline = curve.splines.new('POLY')
spline.points.add(segments - 1)
spline.use_cyclic_u = True
for i in range(segments):
angle = 2 * math.pi * i / segments
# XY平面上の円を作成 (Z=0に平行)
spline.points[i].co = (radius * math.cos(angle), radius * math.sin(angle), 0.0, 1.0)
def update_torus_preview(context, props):
if not props.t_enable_preview:
col = bpy.data.collections.get(PREVIEW_COL_TORUS)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
bpy.data.collections.remove(col)
return
col = bpy.data.collections.get(PREVIEW_COL_TORUS)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_TORUS)
context.scene.collection.children.link(col)
a = props.t_val_a
# Z から X を求めるため、bが分母になる (z = (b/a)x -> x = z * a / b)
b_val = props.t_val_b if abs(props.t_val_b) > 0.0001 else (0.0001 if props.t_val_b >= 0 else -0.0001)
# 選択モードに応じてZ座標のリストを生成
z_list = []
if props.t_mode == 'RANGE':
count = props.t_count
for i in range(count):
t = i / (count - 1) if count > 1 else 0.5
z = props.t_z_min + t * (props.t_z_max - props.t_z_min)
z_list.append(z)
else: # INTERVAL
center = props.t_z_center
interval = props.t_z_interval
ud_count = props.t_up_down_count
# -ud_count から +ud_count まで (例: 5なら -5から+5の計11個)
for i in range(-ud_count, ud_count + 1):
z = center + i * interval
z_list.append(z)
existing_objs = list(col.objects)
mat = get_preview_material("Preview_Mat_Torus", props.t_color)
for i, z in enumerate(z_list):
obj_name = f"[Preview] Torus_{i+1}"
# 座標計算 z = (b/a)x => x = z * a / b
x = z * (a / b_val)
y = 0.0 # XZ平面上に配置
if i < len(existing_objs):
obj = existing_objs[i]
curve = obj.data
else:
curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
curve.dimensions = '3D'
curve.fill_mode = 'FULL'
obj = bpy.data.objects.new(obj_name, curve)
col.objects.link(obj)
curve.bevel_depth = props.t_minor_radius
curve.bevel_resolution = 8
build_curve_circle(curve, props.t_major_radius)
obj.location = (x, y, z)
obj.hide_viewport = False
obj.hide_render = False
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
# 個数が減った場合、余分なオブジェクトを削除
if len(existing_objs) > len(z_list):
for obj in existing_objs[len(z_list):]:
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0:
bpy.data.curves.remove(data)
# ==============================================================================
# タイマー管理
# ==============================================================================
_timer = None
_last_update_time = 0
def delayed_update():
global _timer, _last_update_time
_timer = None
now = time.time()
if now - _last_update_time < 0.05:
if _timer is None:
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
return None
_last_update_time = now
ctx = bpy.context
if not ctx or not ctx.scene: return None
if ctx.object and ctx.object.mode != 'OBJECT': return None
props = getattr(ctx.scene, PROPS_NAME, None)
if props:
update_line_preview(ctx, props)
update_torus_preview(ctx, props)
return None
def on_update(self, context):
global _timer
if _timer is None:
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_EquationProps(PropertyGroup):
# Line Properties
enable_preview: BoolProperty(name="Enable Line Preview", default=CURRENT_DEFAULTS['enable_preview'], update=on_update)
val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['val_a'], update=on_update)
val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['val_b'], update=on_update)
val_d: FloatProperty(name="Value d", default=CURRENT_DEFAULTS['val_d'], update=on_update)
x_min: FloatProperty(name="X Min", default=CURRENT_DEFAULTS['x_min'], update=on_update)
x_max: FloatProperty(name="X Max", default=CURRENT_DEFAULTS['x_max'], update=on_update)
y_min: FloatProperty(name="Y Min", default=CURRENT_DEFAULTS['y_min'], update=on_update)
y_max: FloatProperty(name="Y Max", default=CURRENT_DEFAULTS['y_max'], update=on_update)
z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['z_min'], update=on_update)
z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['z_max'], update=on_update)
thickness: FloatProperty(name="Line Thickness", default=CURRENT_DEFAULTS['thickness'], min=0.01, max=10.0, update=on_update)
draw_plane: EnumProperty(name="Draw Plane", items=[('XZ', "Front (XZ)", "XZ Plane"), ('XY', "Top (XY)", "XY Plane")], default=CURRENT_DEFAULTS['draw_plane'], update=on_update)
show_eq1: BoolProperty(name="Show Eq 1", default=CURRENT_DEFAULTS['show_eq1'], update=on_update)
show_eq2: BoolProperty(name="Show Eq 2", default=CURRENT_DEFAULTS['show_eq2'], update=on_update)
show_eq3: BoolProperty(name="Show Eq 3", default=CURRENT_DEFAULTS['show_eq3'], update=on_update)
color1: FloatVectorProperty(name="Color 1", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color1'], update=on_update)
color2: FloatVectorProperty(name="Color 2", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color2'], update=on_update)
color3: FloatVectorProperty(name="Color 3", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color3'], update=on_update)
# Torus Properties
t_enable_preview: BoolProperty(name="Enable Torus Preview", default=CURRENT_DEFAULTS['t_enable_preview'], update=on_update)
t_mode: EnumProperty(
name="Placement Mode",
items=[
('INTERVAL', "Interval Mode", "Set Center Z, Interval, and Up/Down counts"),
('RANGE', "Range Mode", "Set Min Z, Max Z, and Total Count")
],
default=CURRENT_DEFAULTS['t_mode'], update=on_update
)
t_val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['t_val_a'], update=on_update)
t_val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['t_val_b'], update=on_update)
# Range mode
t_z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['t_z_min'], update=on_update)
t_z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['t_z_max'], update=on_update)
t_count: IntProperty(name="Total Count", default=CURRENT_DEFAULTS['t_count'], min=1, max=500, update=on_update)
# Interval mode
t_z_center: FloatProperty(name="Z Center", default=CURRENT_DEFAULTS['t_z_center'], update=on_update)
t_z_interval: FloatProperty(name="Z Interval", default=CURRENT_DEFAULTS['t_z_interval'], update=on_update)
t_up_down_count: IntProperty(name="Up/Down Count", default=CURRENT_DEFAULTS['t_up_down_count'], min=0, max=100, update=on_update)
t_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['t_major_radius'], min=0.1, max=100.0, update=on_update)
t_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['t_minor_radius'], min=0.01, max=50.0, update=on_update)
t_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['t_color'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_ShowLinePreview(Operator):
bl_idname = f"{OP_PREFIX}.show_line_preview"
bl_label = "Show Line Preview"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props:
props.enable_preview = True
update_line_preview(context, props)
return {'FINISHED'}
class OT_DetachLines(Operator):
bl_idname = f"{OP_PREFIX}.detach_lines"
bl_label = "Detach Lines"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col_preview = bpy.data.collections.get(PREVIEW_COL_LINE)
if not col_preview or len(col_preview.objects) == 0:
self.report({'WARNING'}, "切り離すプレビュー線が見つかりません。")
return {'CANCELLED'}
target_col = context.collection
timestamp = datetime.now().strftime('%H%M%S')
bpy.ops.object.select_all(action='DESELECT')
for obj in list(col_preview.objects):
if obj.hide_viewport: continue
if obj.name not in target_col.objects: target_col.objects.link(obj)
col_preview.objects.unlink(obj)
obj.name = obj.name.replace("[Preview]", "Solid_EqLine") + f"_{timestamp}"
if obj.data.materials:
mat = obj.data.materials[0]
new_mat = mat.copy()
new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
obj.data.materials[0] = new_mat
obj.select_set(True)
self.report({'INFO'}, "Lines Detached!")
props = getattr(context.scene, PROPS_NAME, None)
if props: update_line_preview(context, props)
return {'FINISHED'}
class OT_ShowTorusPreview(Operator):
bl_idname = f"{OP_PREFIX}.show_torus_preview"
bl_label = "Show Torus Preview"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props:
props.t_enable_preview = True
update_torus_preview(context, props)
return {'FINISHED'}
class OT_DetachTorus(Operator):
bl_idname = f"{OP_PREFIX}.detach_torus"
bl_label = "Detach Torus"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col_preview = bpy.data.collections.get(PREVIEW_COL_TORUS)
if not col_preview or len(col_preview.objects) == 0:
self.report({'WARNING'}, "切り離すトーラスが見つかりません。")
return {'CANCELLED'}
target_col = context.collection
timestamp = datetime.now().strftime('%H%M%S')
bpy.ops.object.select_all(action='DESELECT')
for obj in list(col_preview.objects):
if obj.name not in target_col.objects: target_col.objects.link(obj)
col_preview.objects.unlink(obj)
obj.name = obj.name.replace("[Preview]", "Solid_Torus") + f"_{timestamp}"
if obj.data.materials:
mat = obj.data.materials[0]
new_mat = mat.copy()
new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
obj.data.materials[0] = new_mat
obj.select_set(True)
self.report({'INFO'}, "Torus Detached!")
props = getattr(context.scene, PROPS_NAME, None)
if props: update_torus_preview(context, props)
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string(): target_text = t; break
if not target_text: return {'CANCELLED'}
code = target_text.as_string()
c1, c2, c3, tc = props.color1, props.color2, props.color3, props.t_color
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "enable_preview": {props.enable_preview},\n'
new_dict += f' "val_a": {props.val_a:.4f},\n'
new_dict += f' "val_b": {props.val_b:.4f},\n'
new_dict += f' "val_d": {props.val_d:.4f},\n'
new_dict += f' "x_min": {props.x_min:.4f}, "x_max": {props.x_max:.4f},\n'
new_dict += f' "y_min": {props.y_min:.4f}, "y_max": {props.y_max:.4f},\n'
new_dict += f' "z_min": {props.z_min:.4f}, "z_max": {props.z_max:.4f},\n'
new_dict += f' "thickness": {props.thickness:.4f},\n'
new_dict += f' "draw_plane": "{props.draw_plane}",\n'
new_dict += f' "show_eq1": {props.show_eq1}, "show_eq2": {props.show_eq2}, "show_eq3": {props.show_eq3},\n'
new_dict += f' "color1": ({c1[0]:.4f}, {c1[1]:.4f}, {c1[2]:.4f}, {c1[3]:.4f}),\n'
new_dict += f' "color2": ({c2[0]:.4f}, {c2[1]:.4f}, {c2[2]:.4f}, {c2[3]:.4f}),\n'
new_dict += f' "color3": ({c3[0]:.4f}, {c3[1]:.4f}, {c3[2]:.4f}, {c3[3]:.4f}),\n'
new_dict += f' "t_enable_preview": {props.t_enable_preview},\n'
new_dict += f' "t_mode": "{props.t_mode}",\n'
new_dict += f' "t_val_a": {props.t_val_a:.4f}, "t_val_b": {props.t_val_b:.4f},\n'
new_dict += f' "t_z_min": {props.t_z_min:.4f}, "t_z_max": {props.t_z_max:.4f}, "t_count": {props.t_count},\n'
new_dict += f' "t_z_center": {props.t_z_center:.4f}, "t_z_interval": {props.t_z_interval:.4f}, "t_up_down_count": {props.t_up_down_count},\n'
new_dict += f' "t_major_radius": {props.t_major_radius:.4f}, "t_minor_radius": {props.t_minor_radius:.4f},\n'
new_dict += f' "t_color": ({tc[0]:.4f}, {tc[1]:.4f}, {tc[2]:.4f}, {tc[3]:.4f}),\n'
new_dict += "}\n"
try:
tag_start = "# <BEGIN" + "_DICT>"
tag_end = "# <END" + "_DICT>"
pre_code, rest = code.split(tag_start, 1)
_, post_code = rest.split(tag_end, 1)
final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
context.window_manager.clipboard = final_code
self.report({'INFO'}, "Code copied!")
except: return {'CANCELLED'}
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 = "Close Addon"
def execute(self, context):
bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
self.report({'INFO'}, "アドオンを終了しました。")
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_MainPanel(Panel):
bl_label = PANEL_TITLE
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: return
# --- Line Preview Button ---
row_prev = layout.row()
row_prev.scale_y = 1.5
if not props.enable_preview:
row_prev.operator(OT_ShowLinePreview.bl_idname, icon='PLAY', text="Show Line Preview (表示開始)")
else:
row_prev.prop(props, "enable_preview", text="Line Preview Active (ON/OFF)", toggle=True, icon='PAUSE')
box_info = layout.box()
box_info.label(text="【 Line Info 】", icon='INFO')
a_str = f"{props.val_a:.2f}" if abs(props.val_a) > 0.0001 else "0.00(Err)"
box_info.label(text=f"y = ({props.val_b:.2f} / {a_str}) x")
box_values = layout.box()
col_v = box_values.column(align=True)
col_v.prop(props, "val_a")
col_v.prop(props, "val_b")
col_v.prop(props, "val_d")
box_limits = layout.box()
box_limits.label(text="Limits (X, Y, Z)", icon='MOD_HULL')
for axis in ['x', 'y', 'z']:
r = box_limits.row(align=True)
r.prop(props, f"{axis}_min", text=f"{axis.upper()} Min")
r.prop(props, f"{axis}_max", text="Max")
class PT_VisibilityPanel(Panel):
bl_label = "Line Design & Visibility"
bl_idname = f"{PREFIX}_PT_visibility"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_parent_id = f"{PREFIX}_PT_main"
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
layout.prop(props, "thickness")
layout.prop(props, "draw_plane")
layout.separator()
for i in range(1, 4):
r = layout.row(align=True)
r.prop(props, f"show_eq{i}", text=f"Eq {i}", toggle=True)
r.prop(props, f"color{i}", text="")
class PT_CreatePanel(Panel):
bl_label = "Line Detach"
bl_idname = f"{PREFIX}_PT_create"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_parent_id = f"{PREFIX}_PT_main"
def draw(self, context):
col_exec = self.layout.column()
col_exec.scale_y = 2.0
col_exec.operator(OT_DetachLines.bl_idname, icon='MESH_CYLINDER', text="Detach Lines (位置を固定して切り離し)")
class PT_TorusPanel(Panel):
bl_label = "Torus Generator"
bl_idname = f"{PREFIX}_PT_torus"
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
row_prev = layout.row()
row_prev.scale_y = 1.5
if not props.t_enable_preview:
row_prev.operator(OT_ShowTorusPreview.bl_idname, icon='PLAY', text="Show Torus Preview (表示開始)")
else:
row_prev.prop(props, "t_enable_preview", text="Torus Preview Active (ON/OFF)", toggle=True, icon='PAUSE')
box_eq = layout.box()
box_eq.label(text="Center Line: z = (b/a)x", icon='IPO_CONSTANT')
col = box_eq.column(align=True)
col.prop(props, "t_val_a")
col.prop(props, "t_val_b")
box_r = layout.box()
box_r.label(text="Placement Mode", icon='UV_SYNC_SELECT')
box_r.prop(props, "t_mode", text="")
box_r.separator()
if props.t_mode == 'INTERVAL':
c_int = box_r.column(align=True)
c_int.prop(props, "t_z_center")
c_int.prop(props, "t_z_interval")
c_int.prop(props, "t_up_down_count")
else:
c_rng = box_r.column(align=True)
c_rng.prop(props, "t_z_min")
c_rng.prop(props, "t_z_max")
c_rng.prop(props, "t_count")
box_s = layout.box()
box_s.label(text="Torus Shape", icon='MESH_TORUS')
box_s.prop(props, "t_major_radius")
box_s.prop(props, "t_minor_radius")
box_s.prop(props, "t_color")
col_exec = layout.column()
col_exec.scale_y = 1.5
col_exec.operator(OT_DetachTorus.bl_idname, icon='MESH_TORUS', text="Detach Torus (固定化)")
class PT_SystemPanel(Panel):
bl_label = "System (Copy / Close)"
bl_idname = f"{PREFIX}_PT_system"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
r_c = layout.row()
r_c.scale_y = 1.2
r_c.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
r_r = layout.row()
r_r.scale_y = 1.2
r_r.alert = True
r_r.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Close Addon (完全終了)")
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
def draw(self, context):
for l in ADDON_LINKS:
self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"], icon='URL').url = l["url"]
# ==============================================================================
# REGISTER
# ==============================================================================
classes = (
PG_EquationProps,
OT_ShowLinePreview, OT_DetachLines,
OT_ShowTorusPreview, OT_DetachTorus,
OT_CopyFullScript, OT_OpenUrl, OT_RemoveAddon,
PT_MainPanel, PT_VisibilityPanel, PT_CreatePanel,
PT_TorusPanel, PT_SystemPanel, PT_LinksPanel
)
def auto_open_sidebar():
try:
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':
if not space.show_region_ui:
space.show_region_ui = True
except: pass
return None
def register():
for c in classes:
try: bpy.utils.register_class(c)
except ValueError: pass
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_EquationProps))
bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)
def unregister():
global _timer
if _timer is not None:
try: bpy.app.timers.unregister(_timer)
except Exception: pass
_timer = None
cleanup_preview_data()
if hasattr(bpy.types.Scene, PROPS_NAME):
delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except ValueError: pass
if __name__ == "__main__":
register()
# Copied: 2026-04-08 12:00:00
import bpy
import time
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty, IntProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime
# ==============================================================================
# 【 基本設定エリア 】
# ==============================================================================
PREFIX = "EqGen"
ADDON_NAME = "[ Equation Gen ]"
TAB_NAME = "[ Equation Gen ]"
PANEL_TITLE = "Equation Lines"
AUTHOR = "zionadchat"
# ★ このスクリプト自身のID(絶対に消さないこと)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: EQ_LINES_2026_04_08_V5 ###"
# ==============================================================================
# システム初期化 & ID管理
# ==============================================================================
bl_info = {
"name": f"{ADDON_NAME} {PREFIX}",
"author": AUTHOR,
"version": (5, 0, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": "Equation Lines & Torus Generator with independent calculations",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
ADDON_LINKS = (
{"label": "Prefix 20260408", "url": "<https://www.notion.so/Prefix-20260408-33cf5dacaf43807e9e35ff8cdbbc39c6>"},
)
# ==============================================================================
# デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"enable_preview": False,
"val_a": 0.6000,
"val_b": 1.0000,
"val_d": 10.0000,
"x_min": -50.0,
"x_max": 50.0,
"y_min": -50.0,
"y_max": 50.0,
"z_min": -50.0,
"z_max": 50.0,
"thickness": 0.5000,
"draw_plane": "XZ",
"show_eq1": True,
"show_eq2": True,
"show_eq3": True,
"color1": (1.0000, 0.2000, 0.2000, 1.0000),
"color2": (0.2000, 1.0000, 0.2000, 1.0000),
"color3": (0.2000, 0.2000, 1.0000, 1.0000),
# Torus Properties
"t_enable_preview": False,
"t_val_a": 0.6000,
"t_val_b": 1.0000,
"t_x_min": -50.0,
"t_x_max": 50.0,
"t_z_min": -50.0,
"t_z_max": 50.0,
"t_count": 11,
"t_major_radius": 5.0,
"t_minor_radius": 1.0,
"t_color": (0.2000, 0.8000, 0.8000, 1.0000),
}
# <END_DICT>
PREVIEW_COL_LINE = f"{PREFIX}_Line_Preview"
PREVIEW_COL_TORUS = f"{PREFIX}_Torus_Preview"
# ==============================================================================
# 共通マテリアル・データ管理 ロジック
# ==============================================================================
def cleanup_preview_data():
for name in [PREVIEW_COL_LINE, PREVIEW_COL_TORUS]:
col = bpy.data.collections.get(name)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0:
if isinstance(data, bpy.types.Curve):
bpy.data.curves.remove(data)
if len(col.objects) == 0:
bpy.data.collections.remove(col)
def apply_material_settings(mat, color):
mat.use_nodes = True
mat.blend_method = 'BLEND'
mat.diffuse_color = color
tree = mat.node_tree
bsdf = tree.nodes.get("Principled BSDF")
if not bsdf:
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.name = "Principled BSDF"
out = tree.nodes.new("ShaderNodeOutputMaterial")
tree.links.new(bsdf.outputs[0], out.inputs[0])
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
def get_preview_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name=name)
apply_material_settings(mat, color)
return mat
# ==============================================================================
# Line プレビューロジック
# ==============================================================================
def get_clipped_segment(m, c, x_min, x_max, v_min, v_max):
if m == 0:
if v_min <= c <= v_max:
return x_min, x_max
return None, None
else:
x_from_v1, x_from_v2 = (v_min - c) / m, (v_max - c) / m
valid_x_min, valid_x_max = min(x_from_v1, x_from_v2), max(x_from_v1, x_from_v2)
act_x_min, act_x_max = max(x_min, valid_x_min), min(x_max, valid_x_max)
if act_x_min > act_x_max:
return None, None
return act_x_min, act_x_max
def calc_points(props, m, c):
x_min, x_max = min(props.x_min, props.x_max), max(props.x_min, props.x_max)
y_min, y_max = min(props.y_min, props.y_max), max(props.y_min, props.y_max)
z_min, z_max = min(props.z_min, props.z_max), max(props.z_min, props.z_max)
if props.draw_plane == 'XZ':
act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, z_min, z_max)
if act_x_min is None: return None, None
return (act_x_min, 0.0, m * act_x_min + c), (act_x_max, 0.0, m * act_x_max + c)
else:
act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, y_min, y_max)
if act_x_min is None: return None, None
return (act_x_min, m * act_x_min + c, 0.0), (act_x_max, m * act_x_max + c, 0.0)
def update_line_preview(context, props):
if not props.enable_preview:
col = bpy.data.collections.get(PREVIEW_COL_LINE)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
bpy.data.collections.remove(col)
return
col = bpy.data.collections.get(PREVIEW_COL_LINE)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_LINE)
context.scene.collection.children.link(col)
a = props.val_a if abs(props.val_a) > 0.0001 else 0.0001
b, d = props.val_b, props.val_d
m = b / a
equations = [
{"id": 1, "show": props.show_eq1, "color": props.color1, "offset": 0.0},
{"id": 2, "show": props.show_eq2, "color": props.color2, "offset": -d},
{"id": 3, "show": props.show_eq3, "color": props.color3, "offset": d},
]
for eq in equations:
obj_name = f"[Preview] EqLine_{eq['id']}"
obj = bpy.data.objects.get(obj_name)
if not eq["show"]:
if obj: obj.hide_viewport = obj.hide_render = True
continue
p1, p2 = calc_points(props, m, eq["offset"])
if p1 is None:
if obj: obj.hide_viewport = obj.hide_render = True
continue
if not obj:
curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
curve.dimensions = '3D'
curve.fill_mode = 'FULL'
spline = curve.splines.new('POLY')
spline.points.add(1)
obj = bpy.data.objects.new(obj_name, curve)
col.objects.link(obj)
else:
curve = obj.data
spline = curve.splines[0]
curve.bevel_depth = props.thickness
curve.bevel_resolution = 6
spline.points[0].co = (*p1, 1.0)
spline.points[1].co = (*p2, 1.0)
obj.hide_viewport = obj.hide_render = False
mat = get_preview_material(f"Preview_Mat_EqLine{eq['id']}", eq["color"])
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
# ==============================================================================
# Torus プレビューロジック (Z=0平面に平行)
# ==============================================================================
def build_curve_circle(curve, radius, segments=32):
if len(curve.splines) == 0:
spline = curve.splines.new('POLY')
spline.points.add(segments - 1)
spline.use_cyclic_u = True
else:
spline = curve.splines[0]
if len(spline.points) != segments:
curve.splines.clear()
spline = curve.splines.new('POLY')
spline.points.add(segments - 1)
spline.use_cyclic_u = True
for i in range(segments):
angle = 2 * math.pi * i / segments
# XY平面上の円を作成 (Z=0に平行)
spline.points[i].co = (radius * math.cos(angle), radius * math.sin(angle), 0.0, 1.0)
def update_torus_preview(context, props):
if not props.t_enable_preview:
col = bpy.data.collections.get(PREVIEW_COL_TORUS)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0: bpy.data.curves.remove(data)
bpy.data.collections.remove(col)
return
col = bpy.data.collections.get(PREVIEW_COL_TORUS)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_TORUS)
context.scene.collection.children.link(col)
a = props.t_val_a if abs(props.t_val_a) > 0.0001 else 0.0001
b = props.t_val_b
count = props.t_count
existing_objs = list(col.objects)
mat = get_preview_material("Preview_Mat_Torus", props.t_color)
for i in range(count):
obj_name = f"[Preview] Torus_{i+1}"
# 補間比率
t = i / (count - 1) if count > 1 else 0.5
x = props.t_x_min + t * (props.t_x_max - props.t_x_min)
z = props.t_z_min + t * (props.t_z_max - props.t_z_min)
y = (b / a) * x
if i < len(existing_objs):
obj = existing_objs[i]
curve = obj.data
else:
curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
curve.dimensions = '3D'
curve.fill_mode = 'FULL'
obj = bpy.data.objects.new(obj_name, curve)
col.objects.link(obj)
curve.bevel_depth = props.t_minor_radius
curve.bevel_resolution = 8
build_curve_circle(curve, props.t_major_radius)
# 中心位置に移動
obj.location = (x, y, z)
obj.hide_viewport = False
obj.hide_render = False
if not obj.data.materials: obj.data.materials.append(mat)
else: obj.data.materials[0] = mat
# 余分なオブジェクトを削除
if len(existing_objs) > count:
for obj in existing_objs[count:]:
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0:
bpy.data.curves.remove(data)
# ==============================================================================
# タイマー管理
# ==============================================================================
_timer = None
_last_update_time = 0
def delayed_update():
global _timer, _last_update_time
_timer = None
now = time.time()
if now - _last_update_time < 0.05:
if _timer is None:
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
return None
_last_update_time = now
ctx = bpy.context
if not ctx or not ctx.scene: return None
if ctx.object and ctx.object.mode != 'OBJECT': return None
props = getattr(ctx.scene, PROPS_NAME, None)
if props:
update_line_preview(ctx, props)
update_torus_preview(ctx, props)
return None
def on_update(self, context):
global _timer
if _timer is None:
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_EquationProps(PropertyGroup):
# Line Properties
enable_preview: BoolProperty(name="Enable Line Preview", default=CURRENT_DEFAULTS['enable_preview'], update=on_update)
val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['val_a'], update=on_update)
val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['val_b'], update=on_update)
val_d: FloatProperty(name="Value d", default=CURRENT_DEFAULTS['val_d'], update=on_update)
x_min: FloatProperty(name="X Min", default=CURRENT_DEFAULTS['x_min'], update=on_update)
x_max: FloatProperty(name="X Max", default=CURRENT_DEFAULTS['x_max'], update=on_update)
y_min: FloatProperty(name="Y Min", default=CURRENT_DEFAULTS['y_min'], update=on_update)
y_max: FloatProperty(name="Y Max", default=CURRENT_DEFAULTS['y_max'], update=on_update)
z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['z_min'], update=on_update)
z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['z_max'], update=on_update)
thickness: FloatProperty(name="Line Thickness", default=CURRENT_DEFAULTS['thickness'], min=0.01, max=10.0, update=on_update)
draw_plane: EnumProperty(name="Draw Plane", items=[('XZ', "Front (XZ)", "XZ Plane"), ('XY', "Top (XY)", "XY Plane")], default=CURRENT_DEFAULTS['draw_plane'], update=on_update)
show_eq1: BoolProperty(name="Show Eq 1", default=CURRENT_DEFAULTS['show_eq1'], update=on_update)
show_eq2: BoolProperty(name="Show Eq 2", default=CURRENT_DEFAULTS['show_eq2'], update=on_update)
show_eq3: BoolProperty(name="Show Eq 3", default=CURRENT_DEFAULTS['show_eq3'], update=on_update)
color1: FloatVectorProperty(name="Color 1", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color1'], update=on_update)
color2: FloatVectorProperty(name="Color 2", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color2'], update=on_update)
color3: FloatVectorProperty(name="Color 3", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color3'], update=on_update)
# Torus Properties
t_enable_preview: BoolProperty(name="Enable Torus Preview", default=CURRENT_DEFAULTS['t_enable_preview'], update=on_update)
t_val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['t_val_a'], update=on_update)
t_val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['t_val_b'], update=on_update)
t_x_min: FloatProperty(name="X Min", default=CURRENT_DEFAULTS['t_x_min'], update=on_update)
t_x_max: FloatProperty(name="X Max", default=CURRENT_DEFAULTS['t_x_max'], update=on_update)
t_z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['t_z_min'], update=on_update)
t_z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['t_z_max'], update=on_update)
t_count: IntProperty(name="Torus Count", default=CURRENT_DEFAULTS['t_count'], min=1, max=100, update=on_update)
t_major_radius: FloatProperty(name="Major Radius", default=CURRENT_DEFAULTS['t_major_radius'], min=0.1, max=100.0, update=on_update)
t_minor_radius: FloatProperty(name="Minor Radius", default=CURRENT_DEFAULTS['t_minor_radius'], min=0.01, max=50.0, update=on_update)
t_color: FloatVectorProperty(name="Torus Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['t_color'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_ShowLinePreview(Operator):
bl_idname = f"{OP_PREFIX}.show_line_preview"
bl_label = "Show Line Preview"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props:
props.enable_preview = True
update_line_preview(context, props)
return {'FINISHED'}
class OT_DetachLines(Operator):
bl_idname = f"{OP_PREFIX}.detach_lines"
bl_label = "Detach Lines"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col_preview = bpy.data.collections.get(PREVIEW_COL_LINE)
if not col_preview or len(col_preview.objects) == 0:
self.report({'WARNING'}, "切り離すプレビュー線が見つかりません。")
return {'CANCELLED'}
target_col = context.collection
timestamp = datetime.now().strftime('%H%M%S')
bpy.ops.object.select_all(action='DESELECT')
for obj in list(col_preview.objects):
if obj.hide_viewport: continue
if obj.name not in target_col.objects: target_col.objects.link(obj)
col_preview.objects.unlink(obj)
obj.name = obj.name.replace("[Preview]", "Solid_EqLine") + f"_{timestamp}"
if obj.data.materials:
mat = obj.data.materials[0]
new_mat = mat.copy()
new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
obj.data.materials[0] = new_mat
obj.select_set(True)
self.report({'INFO'}, "Lines Detached!")
props = getattr(context.scene, PROPS_NAME, None)
if props: update_line_preview(context, props)
return {'FINISHED'}
class OT_ShowTorusPreview(Operator):
bl_idname = f"{OP_PREFIX}.show_torus_preview"
bl_label = "Show Torus Preview"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props:
props.t_enable_preview = True
update_torus_preview(context, props)
return {'FINISHED'}
class OT_DetachTorus(Operator):
bl_idname = f"{OP_PREFIX}.detach_torus"
bl_label = "Detach Torus"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col_preview = bpy.data.collections.get(PREVIEW_COL_TORUS)
if not col_preview or len(col_preview.objects) == 0:
self.report({'WARNING'}, "切り離すトーラスが見つかりません。")
return {'CANCELLED'}
target_col = context.collection
timestamp = datetime.now().strftime('%H%M%S')
bpy.ops.object.select_all(action='DESELECT')
for obj in list(col_preview.objects):
if obj.name not in target_col.objects: target_col.objects.link(obj)
col_preview.objects.unlink(obj)
obj.name = obj.name.replace("[Preview]", "Solid_Torus") + f"_{timestamp}"
if obj.data.materials:
mat = obj.data.materials[0]
new_mat = mat.copy()
new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
obj.data.materials[0] = new_mat
obj.select_set(True)
self.report({'INFO'}, "Torus Detached!")
props = getattr(context.scene, PROPS_NAME, None)
if props: update_torus_preview(context, props)
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string(): target_text = t; break
if not target_text: return {'CANCELLED'}
code = target_text.as_string()
c1, c2, c3, tc = props.color1, props.color2, props.color3, props.t_color
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "enable_preview": {props.enable_preview},\n'
new_dict += f' "val_a": {props.val_a:.4f},\n'
new_dict += f' "val_b": {props.val_b:.4f},\n'
new_dict += f' "val_d": {props.val_d:.4f},\n'
new_dict += f' "x_min": {props.x_min:.4f}, "x_max": {props.x_max:.4f},\n'
new_dict += f' "y_min": {props.y_min:.4f}, "y_max": {props.y_max:.4f},\n'
new_dict += f' "z_min": {props.z_min:.4f}, "z_max": {props.z_max:.4f},\n'
new_dict += f' "thickness": {props.thickness:.4f},\n'
new_dict += f' "draw_plane": "{props.draw_plane}",\n'
new_dict += f' "show_eq1": {props.show_eq1}, "show_eq2": {props.show_eq2}, "show_eq3": {props.show_eq3},\n'
new_dict += f' "color1": ({c1[0]:.4f}, {c1[1]:.4f}, {c1[2]:.4f}, {c1[3]:.4f}),\n'
new_dict += f' "color2": ({c2[0]:.4f}, {c2[1]:.4f}, {c2[2]:.4f}, {c2[3]:.4f}),\n'
new_dict += f' "color3": ({c3[0]:.4f}, {c3[1]:.4f}, {c3[2]:.4f}, {c3[3]:.4f}),\n'
new_dict += f' "t_enable_preview": {props.t_enable_preview},\n'
new_dict += f' "t_val_a": {props.t_val_a:.4f}, "t_val_b": {props.t_val_b:.4f},\n'
new_dict += f' "t_x_min": {props.t_x_min:.4f}, "t_x_max": {props.t_x_max:.4f},\n'
new_dict += f' "t_z_min": {props.t_z_min:.4f}, "t_z_max": {props.t_z_max:.4f},\n'
new_dict += f' "t_count": {props.t_count},\n'
new_dict += f' "t_major_radius": {props.t_major_radius:.4f}, "t_minor_radius": {props.t_minor_radius:.4f},\n'
new_dict += f' "t_color": ({tc[0]:.4f}, {tc[1]:.4f}, {tc[2]:.4f}, {tc[3]:.4f}),\n'
new_dict += "}\n"
try:
tag_start = "# <BEGIN" + "_DICT>"
tag_end = "# <END" + "_DICT>"
pre_code, rest = code.split(tag_start, 1)
_, post_code = rest.split(tag_end, 1)
final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
context.window_manager.clipboard = final_code
self.report({'INFO'}, "Code copied!")
except: return {'CANCELLED'}
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 = "Close Addon"
def execute(self, context):
bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
self.report({'INFO'}, "アドオンを終了しました。")
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_MainPanel(Panel):
bl_label = PANEL_TITLE
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: return
# --- Line Preview Button ---
row_prev = layout.row()
row_prev.scale_y = 1.5
if not props.enable_preview:
row_prev.operator(OT_ShowLinePreview.bl_idname, icon='PLAY', text="Show Line Preview (表示開始)")
else:
row_prev.prop(props, "enable_preview", text="Line Preview Active (ON/OFF)", toggle=True, icon='PAUSE')
box_info = layout.box()
box_info.label(text="【 Line Info 】", icon='INFO')
a_str = f"{props.val_a:.2f}" if abs(props.val_a) > 0.0001 else "0.00(Err)"
box_info.label(text=f"y = ({props.val_b:.2f} / {a_str}) x")
box_values = layout.box()
col_v = box_values.column(align=True)
col_v.prop(props, "val_a")
col_v.prop(props, "val_b")
col_v.prop(props, "val_d")
box_limits = layout.box()
box_limits.label(text="Limits (X, Y, Z)", icon='MOD_HULL')
for axis in ['x', 'y', 'z']:
r = box_limits.row(align=True)
r.prop(props, f"{axis}_min", text=f"{axis.upper()} Min")
r.prop(props, f"{axis}_max", text="Max")
class PT_VisibilityPanel(Panel):
bl_label = "Line Design & Visibility"
bl_idname = f"{PREFIX}_PT_visibility"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_parent_id = f"{PREFIX}_PT_main"
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
layout.prop(props, "thickness")
layout.prop(props, "draw_plane")
layout.separator()
for i in range(1, 4):
r = layout.row(align=True)
r.prop(props, f"show_eq{i}", text=f"Eq {i}", toggle=True)
r.prop(props, f"color{i}", text="")
class PT_CreatePanel(Panel):
bl_label = "Line Detach"
bl_idname = f"{PREFIX}_PT_create"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_parent_id = f"{PREFIX}_PT_main"
def draw(self, context):
col_exec = self.layout.column()
col_exec.scale_y = 2.0
col_exec.operator(OT_DetachLines.bl_idname, icon='MESH_CYLINDER', text="Detach Lines (位置を固定して切り離し)")
class PT_TorusPanel(Panel):
bl_label = "Torus Generator (独立計算)"
bl_idname = f"{PREFIX}_PT_torus"
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
row_prev = layout.row()
row_prev.scale_y = 1.5
if not props.t_enable_preview:
row_prev.operator(OT_ShowTorusPreview.bl_idname, icon='PLAY', text="Show Torus Preview (表示開始)")
else:
row_prev.prop(props, "t_enable_preview", text="Torus Preview Active (ON/OFF)", toggle=True, icon='PAUSE')
box_eq = layout.box()
box_eq.label(text="Center Line: y = (b/a)x", icon='NORMALS_FACE')
col = box_eq.column(align=True)
col.prop(props, "t_val_a")
col.prop(props, "t_val_b")
box_r = layout.box()
box_r.label(text="Placement Range", icon='ARROW_LEFTRIGHT')
r_z = box_r.row(align=True)
r_z.prop(props, "t_z_min", text="Z Min")
r_z.prop(props, "t_z_max", text="Z Max")
r_x = box_r.row(align=True)
r_x.prop(props, "t_x_min", text="X Min")
r_x.prop(props, "t_x_max", text="X Max")
box_r.prop(props, "t_count")
box_s = layout.box()
box_s.label(text="Torus Shape", icon='MESH_TORUS')
box_s.prop(props, "t_major_radius")
box_s.prop(props, "t_minor_radius")
box_s.prop(props, "t_color")
col_exec = layout.column()
col_exec.scale_y = 1.5
col_exec.operator(OT_DetachTorus.bl_idname, icon='MESH_TORUS', text="Detach Torus (固定化)")
class PT_SystemPanel(Panel):
bl_label = "System (Copy / Close)"
bl_idname = f"{PREFIX}_PT_system"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
def draw(self, context):
layout = self.layout
r_c = layout.row()
r_c.scale_y = 1.2
r_c.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
r_r = layout.row()
r_r.scale_y = 1.2
r_r.alert = True
r_r.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Close Addon (完全終了)")
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
def draw(self, context):
for l in ADDON_LINKS:
self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"], icon='URL').url = l["url"]
# ==============================================================================
# REGISTER
# ==============================================================================
classes = (
PG_EquationProps,
OT_ShowLinePreview, OT_DetachLines,
OT_ShowTorusPreview, OT_DetachTorus,
OT_CopyFullScript, OT_OpenUrl, OT_RemoveAddon,
PT_MainPanel, PT_VisibilityPanel, PT_CreatePanel,
PT_TorusPanel, PT_SystemPanel, PT_LinksPanel
)
def auto_open_sidebar():
try:
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':
if not space.show_region_ui:
space.show_region_ui = True
except: pass
return None
def register():
for c in classes:
try: bpy.utils.register_class(c)
except ValueError: pass
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_EquationProps))
bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)
def unregister():
global _timer
if _timer is not None:
try: bpy.app.timers.unregister(_timer)
except Exception: pass
_timer = None
cleanup_preview_data()
if hasattr(bpy.types.Scene, PROPS_NAME):
delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes):
try: bpy.utils.unregister_class(c)
except ValueError: pass
if __name__ == "__main__":
register()
# Copied: 2026-04-08 12:00:00
import bpy
import time
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty
from bpy.types import Operator, Panel, PropertyGroup
from datetime import datetime
# ==============================================================================
# 【 基本設定エリア 】
# ==============================================================================
PREFIX = "EqLines"
ADDON_NAME = "[ Equation Lines ]"
TAB_NAME = "[ Equation Gen ]"
PANEL_TITLE = "Equation Lines"
AUTHOR = "zionadchat"
# ★ このスクリプト自身のID(絶対に消さないこと)
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: EQ_LINES_2026_04_08_V4 ###"
# ==============================================================================
# システム初期化 & ID管理
# ==============================================================================
bl_info = {
"name": f"{ADDON_NAME} {PREFIX}",
"author": AUTHOR,
"version": (4, 3, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": "Equation Lines Generator - Preview button, detach, split panels",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
ADDON_LINKS = (
{"label": "Prefix 20260408", "url": "<https://www.notion.so/Prefix-20260408-33cf5dacaf43807e9e35ff8cdbbc39c6>"},
)
# ==============================================================================
# デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"enable_preview": False,
"val_a": 0.6000,
"val_b": 1.0000,
"val_d": 10.0000,
"x_min": -50.0,
"x_max": 50.0,
"y_min": -50.0,
"y_max": 50.0,
"z_min": -50.0,
"z_max": 50.0,
"thickness": 0.5000,
"draw_plane": "XZ",
"show_eq1": True,
"show_eq2": True,
"show_eq3": True,
"color1": (1.0000, 0.2000, 0.2000, 1.0000),
"color2": (0.2000, 1.0000, 0.2000, 1.0000),
"color3": (0.2000, 0.2000, 1.0000, 1.0000),
}
# <END_DICT>
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
# ==============================================================================
# マテリアル・データ管理 ロジック
# ==============================================================================
def cleanup_preview_data():
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if col:
for obj in list(col.objects):
data = obj.data
bpy.data.objects.remove(obj, do_unlink=True)
if data and data.users == 0:
if isinstance(data, bpy.types.Curve):
bpy.data.curves.remove(data)
if len(col.objects) == 0:
bpy.data.collections.remove(col)
def apply_material_settings(mat, color):
mat.use_nodes = True
mat.blend_method = 'BLEND'
mat.diffuse_color = color
tree = mat.node_tree
bsdf = tree.nodes.get("Principled BSDF")
if not bsdf:
tree.nodes.clear()
bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
bsdf.name = "Principled BSDF"
out = tree.nodes.new("ShaderNodeOutputMaterial")
tree.links.new(bsdf.outputs[0], out.inputs[0])
if "Base Color" in bsdf.inputs:
bsdf.inputs["Base Color"].default_value = color
if "Alpha" in bsdf.inputs:
bsdf.inputs["Alpha"].default_value = color[3]
def get_preview_material(name, color):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name=name)
apply_material_settings(mat, color)
return mat
def get_clipped_segment(m, c, x_min, x_max, v_min, v_max):
if m == 0:
if v_min <= c <= v_max:
return x_min, x_max
else:
return None, None
else:
x_from_v1 = (v_min - c) / m
x_from_v2 = (v_max - c) / m
valid_x_min = min(x_from_v1, x_from_v2)
valid_x_max = max(x_from_v1, x_from_v2)
act_x_min = max(x_min, valid_x_min)
act_x_max = min(x_max, valid_x_max)
if act_x_min > act_x_max:
return None, None
return act_x_min, act_x_max
def calc_points(props, m, c):
x_min, x_max = min(props.x_min, props.x_max), max(props.x_min, props.x_max)
y_min, y_max = min(props.y_min, props.y_max), max(props.y_min, props.y_max)
z_min, z_max = min(props.z_min, props.z_max), max(props.z_min, props.z_max)
if props.draw_plane == 'XZ':
act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, z_min, z_max)
if act_x_min is None: return None, None
v1 = m * act_x_min + c
v2 = m * act_x_max + c
return (act_x_min, 0.0, v1), (act_x_max, 0.0, v2)
else:
act_x_min, act_x_max = get_clipped_segment(m, c, x_min, x_max, y_min, y_max)
if act_x_min is None: return None, None
v1 = m * act_x_min + c
v2 = m * act_x_max + c
return (act_x_min, v1, 0.0), (act_x_max, v2, 0.0)
def update_preview_geometry(context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
# ★ プレビューが無効な場合はデータを削除して終了
if not props.enable_preview:
cleanup_preview_data()
return
col = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col:
col = bpy.data.collections.new(PREVIEW_COL_NAME)
if col.name not in context.scene.collection.children:
context.scene.collection.children.link(col)
a = props.val_a if abs(props.val_a) > 0.0001 else (0.0001 if props.val_a >= 0 else -0.0001)
b = props.val_b
d = props.val_d
m = b / a
equations = [
{"id": 1, "show": props.show_eq1, "color": props.color1, "offset": 0.0},
{"id": 2, "show": props.show_eq2, "color": props.color2, "offset": -d},
{"id": 3, "show": props.show_eq3, "color": props.color3, "offset": d},
]
for eq in equations:
obj_name = f"[Preview] EqLine_{eq['id']}"
obj = bpy.data.objects.get(obj_name)
if not eq["show"]:
if obj:
obj.hide_viewport = True
obj.hide_render = True
continue
p1, p2 = calc_points(props, m, eq["offset"])
if p1 is None:
if obj:
obj.hide_viewport = True
obj.hide_render = True
continue
if not obj:
curve = bpy.data.curves.new(name=f"{obj_name}_curve", type='CURVE')
curve.dimensions = '3D'
curve.fill_mode = 'FULL'
spline = curve.splines.new('POLY')
spline.points.add(1)
obj = bpy.data.objects.new(obj_name, curve)
col.objects.link(obj)
else:
curve = obj.data
spline = curve.splines[0]
curve.bevel_depth = props.thickness
curve.bevel_resolution = 6
spline.points[0].co = (*p1, 1.0)
spline.points[1].co = (*p2, 1.0)
obj.hide_viewport = False
obj.hide_render = False
mat = get_preview_material(f"Preview_Mat_EqLine{eq['id']}", eq["color"])
if not obj.data.materials:
obj.data.materials.append(mat)
else:
obj.data.materials[0] = mat
_timer = None
_last_update_time = 0
def delayed_update():
global _timer, _last_update_time
_timer = None
now = time.time()
if now - _last_update_time < 0.05:
if _timer is None:
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
return None
_last_update_time = now
ctx = bpy.context
if not ctx or not ctx.scene: return None
if ctx.object and ctx.object.mode != 'OBJECT': return None
update_preview_geometry(ctx)
return None
def on_update(self, context):
global _timer
if _timer is None:
_timer = bpy.app.timers.register(delayed_update, first_interval=0.05)
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_EquationProps(PropertyGroup):
enable_preview: BoolProperty(name="Enable Preview", default=CURRENT_DEFAULTS['enable_preview'], update=on_update)
val_a: FloatProperty(name="Value a", default=CURRENT_DEFAULTS['val_a'], update=on_update)
val_b: FloatProperty(name="Value b", default=CURRENT_DEFAULTS['val_b'], update=on_update)
val_d: FloatProperty(name="Value d", default=CURRENT_DEFAULTS['val_d'], update=on_update)
x_min: FloatProperty(name="X Min", default=CURRENT_DEFAULTS['x_min'], update=on_update)
x_max: FloatProperty(name="X Max", default=CURRENT_DEFAULTS['x_max'], update=on_update)
y_min: FloatProperty(name="Y Min", default=CURRENT_DEFAULTS['y_min'], update=on_update)
y_max: FloatProperty(name="Y Max", default=CURRENT_DEFAULTS['y_max'], update=on_update)
z_min: FloatProperty(name="Z Min", default=CURRENT_DEFAULTS['z_min'], update=on_update)
z_max: FloatProperty(name="Z Max", default=CURRENT_DEFAULTS['z_max'], update=on_update)
thickness: FloatProperty(name="Cylinder Thickness", default=CURRENT_DEFAULTS['thickness'], min=0.01, max=10.0, update=on_update)
draw_plane: EnumProperty(
name="Draw Plane",
items=[('XZ', "Front (XZ)", "Draw on XZ Plane"), ('XY', "Top (XY)", "Draw on XY Plane")],
default=CURRENT_DEFAULTS['draw_plane'], update=on_update
)
show_eq1: BoolProperty(name="Show Eq 1", default=CURRENT_DEFAULTS['show_eq1'], update=on_update)
show_eq2: BoolProperty(name="Show Eq 2", default=CURRENT_DEFAULTS['show_eq2'], update=on_update)
show_eq3: BoolProperty(name="Show Eq 3", default=CURRENT_DEFAULTS['show_eq3'], update=on_update)
color1: FloatVectorProperty(name="Color 1", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color1'], update=on_update)
color2: FloatVectorProperty(name="Color 2", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color2'], update=on_update)
color3: FloatVectorProperty(name="Color 3", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color3'], update=on_update)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_ShowPreview(Operator):
bl_idname = f"{OP_PREFIX}.show_preview"
bl_label = "Show Preview Lines (最初の一括表示)"
bl_description = "プレビューを有効化し、画面にラインを一括表示します"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props:
props.enable_preview = True
update_preview_geometry(context)
return {'FINISHED'}
class OT_DetachLines(Operator):
bl_idname = f"{OP_PREFIX}.detach_lines"
bl_label = "Detach Lines"
bl_description = "現在のプレビュー線を通常オブジェクトに変換し、パラメータ追従から切り離します"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
col_preview = bpy.data.collections.get(PREVIEW_COL_NAME)
if not col_preview or len(col_preview.objects) == 0:
self.report({'WARNING'}, "切り離すプレビュー線が見つかりません。")
return {'CANCELLED'}
target_col = context.collection
timestamp = datetime.now().strftime('%H%M%S')
bpy.ops.object.select_all(action='DESELECT')
created_count = 0
for obj in list(col_preview.objects):
if obj.hide_viewport:
continue
if obj.name not in target_col.objects:
target_col.objects.link(obj)
col_preview.objects.unlink(obj)
obj.name = obj.name.replace("[Preview]", "Solid_EqLine") + f"_{timestamp}"
if obj.data.materials:
mat = obj.data.materials[0]
new_mat = mat.copy()
new_mat.name = mat.name.replace("Preview_", "Solid_") + f"_{timestamp}"
obj.data.materials[0] = new_mat
obj.select_set(True)
context.view_layer.objects.active = obj
created_count += 1
if created_count > 0:
self.report({'INFO'}, f"{created_count}個のラインを切り離しました!(位置が固定されます)")
else:
self.report({'WARNING'}, "切り離せるラインがありませんでした。")
update_preview_geometry(context)
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string(): target_text = t; break
if not target_text:
self.report({'WARNING'}, "Source script not found in Text Editor.")
return {'CANCELLED'}
code = target_text.as_string()
c1, c2, c3 = props.color1, props.color2, props.color3
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "enable_preview": {props.enable_preview},\n'
new_dict += f' "val_a": {props.val_a:.4f},\n'
new_dict += f' "val_b": {props.val_b:.4f},\n'
new_dict += f' "val_d": {props.val_d:.4f},\n'
new_dict += f' "x_min": {props.x_min:.4f},\n'
new_dict += f' "x_max": {props.x_max:.4f},\n'
new_dict += f' "y_min": {props.y_min:.4f},\n'
new_dict += f' "y_max": {props.y_max:.4f},\n'
new_dict += f' "z_min": {props.z_min:.4f},\n'
new_dict += f' "z_max": {props.z_max:.4f},\n'
new_dict += f' "thickness": {props.thickness:.4f},\n'
new_dict += f' "draw_plane": "{props.draw_plane}",\n'
new_dict += f' "show_eq1": {props.show_eq1},\n'
new_dict += f' "show_eq2": {props.show_eq2},\n'
new_dict += f' "show_eq3": {props.show_eq3},\n'
new_dict += f' "color1": ({c1[0]:.4f}, {c1[1]:.4f}, {c1[2]:.4f}, {c1[3]:.4f}),\n'
new_dict += f' "color2": ({c2[0]:.4f}, {c2[1]:.4f}, {c2[2]:.4f}, {c2[3]:.4f}),\n'
new_dict += f' "color3": ({c3[0]:.4f}, {c3[1]:.4f}, {c3[2]:.4f}, {c3[3]:.4f}),\n'
new_dict += "}\n"
try:
tag_start = "# <BEGIN" + "_DICT>"
tag_end = "# <END" + "_DICT>"
if tag_start not in code or tag_end not in code: return {'CANCELLED'}
pre_code, rest = code.split(tag_start, 1)
_, post_code = rest.split(tag_end, 1)
final_code = pre_code + tag_start + "\n" + new_dict + tag_end + post_code
lines = final_code.split("\n")
if len(lines) > 0 and lines[0].startswith("# Copied:"):
lines[0] = f"# Copied: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
context.window_manager.clipboard = "\n".join(lines)
self.report({'INFO'}, "Code copied!")
except Exception:
return {'CANCELLED'}
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 = "Close Addon"
def execute(self, context):
bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
self.report({'INFO'}, "アドオンを終了し、プレビューを削除しました。")
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_MainPanel(Panel):
bl_label = PANEL_TITLE
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: return
# --- Show/Hide Preview Button ---
row_prev = layout.row()
row_prev.scale_y = 1.5
if not props.enable_preview:
row_prev.operator(OT_ShowPreview.bl_idname, icon='PLAY', text="Show Preview Lines (表示開始)")
else:
row_prev.prop(props, "enable_preview", text="Preview Active (ON - クリックで消去)", toggle=True, icon='PAUSE')
layout.separator()
# --- Equations Info ---
box_info = layout.box()
box_info.label(text="【 Equations Info 】", icon='INFO')
a_str = f"{props.val_a:.2f}" if abs(props.val_a) > 0.0001 else "0.00(Err)"
b_str = f"{props.val_b:.2f}"
d_str = f"{props.val_d:.2f}"
box_info.label(text=f"y = ({b_str} / {a_str}) x")
box_info.label(text=f"y = ({b_str} / {a_str}) x - {d_str}")
box_info.label(text=f"y = ({b_str} / {a_str}) x + {d_str}")
layout.separator()
# --- Parameters ---
box_values = layout.box()
box_values.label(text="Parameters", icon='DRIVER')
col_v = box_values.column(align=True)
col_v.prop(props, "val_a")
col_v.prop(props, "val_b")
col_v.prop(props, "val_d")
layout.separator()
# --- Limits ---
box_limits = layout.box()
box_limits.label(text="Limits (X, Y, Z)", icon='MOD_HULL')
row_x = box_limits.row(align=True)
row_x.prop(props, "x_min", text="X Min")
row_x.prop(props, "x_max", text="Max")
row_y = box_limits.row(align=True)
row_y.prop(props, "y_min", text="Y Min")
row_y.prop(props, "y_max", text="Max")
row_z = box_limits.row(align=True)
row_z.prop(props, "z_min", text="Z Min")
row_z.prop(props, "z_max", text="Max")
class PT_VisibilityPanel(Panel):
bl_label = "Design & Visibility"
bl_idname = f"{PREFIX}_PT_visibility"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_parent_id = f"{PREFIX}_PT_main"
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
layout.prop(props, "thickness")
layout.prop(props, "draw_plane")
layout.separator()
# --- 個別表示非表示・カラー ---
r1 = layout.row(align=True)
r1.prop(props, "show_eq1", text="Eq 1", toggle=True)
r1.prop(props, "color1", text="")
r2 = layout.row(align=True)
r2.prop(props, "show_eq2", text="Eq 2", toggle=True)
r2.prop(props, "color2", text="")
r3 = layout.row(align=True)
r3.prop(props, "show_eq3", text="Eq 3", toggle=True)
r3.prop(props, "color3", text="")
class PT_CreatePanel(Panel):
bl_label = "Create Objects"
bl_idname = f"{PREFIX}_PT_create"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_parent_id = f"{PREFIX}_PT_main"
def draw(self, context):
layout = self.layout
# --- オブジェクト切り離しボタン ---
col_exec = layout.column()
col_exec.scale_y = 2.0
col_exec.operator(OT_DetachLines.bl_idname, icon='MESH_CYLINDER', text="Detach Lines (位置を固定して切り離し)")
class PT_SystemPanel(Panel):
bl_label = "System (Copy / Close)"
bl_idname = f"{PREFIX}_PT_system"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_parent_id = f"{PREFIX}_PT_main"
def draw(self, context):
layout = self.layout
# --- コピー機能 ---
row_copy = layout.row()
row_copy.scale_y = 1.2
row_copy.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
# --- アドオン終了 ---
row_rem = layout.row()
row_rem.scale_y = 1.2
row_rem.alert = True
row_rem.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Close Addon (アドオン完全終了)")
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_parent_id = f"{PREFIX}_PT_main"
def draw(self, context):
layout = self.layout
# --- リンク パネル ---
for l in ADDON_LINKS:
layout.operator(OT_OpenUrl.bl_idname, text=l["label"], icon='URL').url = l["url"]
# ==============================================================================
# REGISTER
# ==============================================================================
classes = (
PG_EquationProps,
OT_ShowPreview,
OT_DetachLines,
OT_CopyFullScript,
OT_OpenUrl,
OT_RemoveAddon,
PT_MainPanel,
PT_VisibilityPanel,
PT_CreatePanel,
PT_SystemPanel,
PT_LinksPanel
)
def auto_open_sidebar():
try:
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':
if not space.show_region_ui:
space.show_region_ui = True
except: pass
return None
def register():
for c in classes:
try:
bpy.utils.register_class(c)
except ValueError:
pass
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_EquationProps))
bpy.app.timers.register(auto_open_sidebar, first_interval=0.1)
def unregister():
global _timer
if _timer is not None:
try:
bpy.app.timers.unregister(_timer)
except Exception:
pass
_timer = None
cleanup_preview_data()
if hasattr(bpy.types.Scene, PROPS_NAME):
delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes):
try:
bpy.utils.unregister_class(c)
except ValueError:
pass
if __name__ == "__main__":
register()