tmp


import os
import re

import numpy as np
import openvsp as vsp
import math

XSEC_CRV_TYPE = {
    vsp.XS_UNDEFINED : 'XS_UNDEFINED' ,
    vsp.XS_POINT : 'XS_POINT' ,
    vsp.XS_CIRCLE : 'XS_CIRCLE' ,
    vsp.XS_ELLIPSE : 'XS_ELLIPSE' ,
    vsp.XS_SUPER_ELLIPSE : 'XS_SUPER_ELLIPSE' ,
    vsp.XS_ROUNDED_RECTANGLE : 'XS_ROUNDED_RECTANGLE' ,
    vsp.XS_GENERAL_FUSE : 'XS_GENERAL_FUSE' ,
    vsp.XS_FILE_FUSE : 'XS_FILE_FUSE' ,
    vsp.XS_BICONVEX : 'XS_BICONVEX' ,
    vsp.XS_WEDGE : 'XS_WEDGE' ,
    vsp.XS_EDIT_CURVE : 'XS_EDIT_CURVE' ,
    vsp.XS_FOUR_SERIES : 'XS_FOUR_SERIES' ,
    vsp.XS_SIX_SERIES : 'XS_SIX_SERIES' ,
    vsp.XS_FILE_AIRFOIL : 'XS_FILE_AIRFOIL' ,
    vsp.XS_CST_AIRFOIL : 'XS_CST_AIRFOIL' ,
    vsp.XS_VKT_AIRFOIL : 'XS_VKT_AIRFOIL' ,
    vsp.XS_FOUR_DIGIT_MOD : 'XS_FOUR_DIGIT_MOD' ,
    vsp.XS_FIVE_DIGIT : 'XS_FIVE_DIGIT' ,
    vsp.XS_FIVE_DIGIT_MOD : 'XS_FIVE_DIGIT_MOD' ,
    vsp.XS_ONE_SIX_SERIES : 'XS_ONE_SIX_SERIES' ,
    vsp.XS_NUM_TYPES : 'XS_NUM_TYPES' ,
}

PARM_TYPE = { 
    vsp.PARM_DOUBLE_TYPE : 'double',
    vsp.PARM_INT_TYPE : 'int',
    vsp.PARM_BOOL_TYPE : 'boolean',
    vsp.PARM_FRACTION_TYPE : 'fraction',
    vsp.PARM_LIMITED_INT_TYPE : 'limited int',
    vsp.PARM_NOTEQ_TYPE : 'noteq',
    vsp.PARM_POWER_INT_TYPE : 'power int',
}

class BuildePlane:
    """
    OpenVSP を利用して航空機 (主翼・水平尾翼・垂直尾翼) を構築するクラス。
    vsp (openvsp Python API) をラップし、XWIMP 形式の定義ファイルから各翼を生成して
    ひとつの .vsp3 モデルとして保存する。

    Parameters
    ----------
    wing_file : str, default "wing.xwimp"
        主翼 (Main Wing) の定義ファイル (XWIMP 形式) へのパス。
    tail_file : str, default "tail.xwimp"
        水平尾翼 (Horizontal Tail) の定義ファイルパス。
    fin_file : str, default "fin.xwimp"
        垂直尾翼 (Vertical Tail / Fin) の定義ファイルパス。
    """

    def __init__(self, wing_file="wing.xwimp", tail_file="tail.xwimp", fin_file="fin.xwimp"):
        # [重要] 既存の VSP モデルを完全にクリアする。
        # - これ以降は新規モデルとして構築される。
        # - 未保存の変更は失われるため、外部からの呼び出し順序・保存ロジックを設計する際は注意。
        vsp.ClearVSPModel()

        # 主翼 (wing) 、水平尾翼 (tail) 、垂直尾翼 (fin) をそれぞれ XWIMP から生成。
        # - make_wing_from_xwimp(...) 内で XWIMP の各セクションが追加され、
        #   スパン/コード/後退角/捩り/上反角/翼型などが設定される。
        # - is_vertical=False : 平面 (対称) 翼として初期化。
        self.wing_id = self.make_wing_from_xwimp(wing_file, is_vertical=False)
        # - 水平尾翼も平面翼として初期化。
        self.tail_id = self.make_wing_from_xwimp(tail_file, is_vertical=False)
        # - 垂直尾翼は is_vertical=True : 対称面をオフ、X 軸回転で 90 度立てる等の処理を行う。
        self.fin_id  = self.make_wing_from_xwimp(fin_file, is_vertical=True)

        # パラメータ変更を評価して内部ジオメトリを確定。
        # - InsertXSec/SetParmValUpdate で設定した値がすべて反映される。
        # - 大量のジオメトリを扱う場合は、性能観点で Update の呼び出し回数を最小化すると良い。
        vsp.Update()

        # 現在のモデル (主翼・尾翼を含む全体) を .vsp3 としてディスクへ保存。
        # - 既存ファイルがある場合は上書きされる (ファイルロック・書き込み権限に注意) 。
        vsp.WriteVSPFile("aircraft.vsp3")

    # ===== パラメータ設定補助 =====
    def set_airfoil_xsec(self, xsec_surf_id, xsec_index, crv_type, args):
        """
        XSec (翼断面) の形状タイプとパラメータを設定する補助関数。

        説明 :
        - OpenVSP の XSecSurface (xsec_surf_id) 内の指定インデックス (xsec_index) に対して
          断面タイプ (crv_type) を設定し、crv_type に応じたパラメータを args から読み取って反映する。
        - この関数自体は vsp.Update() を呼ばないため、呼び出し後に呼び出し元で vsp.Update() を実行して
          形状に反映する必要があり。

        引数 :
        - xsec_surf_id (int): vsp.GetXSecSurf(...) で取得した XSecSurface の ID (整数) 。
        - xsec_index (int):   XSecSurface 内の断面インデックス (通常 0 始まり) 。
        - crv_type (int):     vsp の XSEC_CRV_TYPE 定数のいずれか (例 : vsp.XS_FILE_AIRFOIL) 。
        - args (dict):        crv_type に応じた設定値を持つ辞書。
        """

        # 断面タイプを変更する。
        # vsp.ChangeXSecShape は xsec_surf_id と断面インデックスに対して、新しい断面タイプを割り当てる。
        # 注意 : 既存の断面タイプから変更するとき、一部の内部データ (翼型の座標など) がリセットされる可能性がある。
        vsp.ChangeXSecShape(xsec_surf_id, xsec_index, crv_type)

        # 変更した断面の XSec ID を取得する。
        # 以降、個々のパラメータはこの xsec_id に対して設定する。
        xsec_id = vsp.GetXSec(xsec_surf_id, xsec_index)

        # 内部ヘルパー : パラメータ名とデフォルト値を受け取り、args から値を取り出して設定する。
        # - vsp.GetXSecParm(xsec_id, parm_name) はそのパラメータの ID を返す。
        # - vsp.SetParmValUpdate(parm_id, value) を使うことで、値をセットすると同時に内部の依存関係を更新する。
        # 可搬性向上のための注意 : parm_id が無効 (None、0、負数など) である可能性があるため、
        # 実運用では parm_id の検査を行い、無効な場合は警告を出す実装を追加することを推奨。
        def set_parm(xsec_id, parm_name, default):
            parm_id = vsp.GetXSecParm(xsec_id, parm_name)
            if parm_id is None or parm_id == '':
                print(f"Parameter '{parm_name}' not found for xsec {xsec_id}")
                return
            else:
                vsp.SetParmVal(parm_id, args.get(parm_name, default))

        # 各断面タイプごとの処理
        # ファイルから読み込む場合 : 翼型座標ファイルを読み込む (例: .dat 形式) 。
        if crv_type == vsp.XS_FILE_AIRFOIL:
            # 'file' キーは必須。パスは相対ではなく絶対パス推奨。
            if "file" not in args:
                raise ValueError("XS_FILE_AIRFOIL requires 'file' argument")
            # ReadFileAirfoil は xsec_id とファイルパスを受け取り、翼型座標をロードする。
            vsp.ReadFileAirfoil(xsec_id, args.get("file", ''))

        # CST (Class/Shape Transformation) 曲線で定義する場合
        elif crv_type == vsp.XS_CST_AIRFOIL:
            # nupper/nlower が指定されていなければデフォルトを使用
            nupper = args.get("nupper", 6)
            nlower = args.get("nlower", 6)
            # 上下の CST 係数配列 (長さは nupper/nlower と一致することが望ましい)
            ule = args.get("ule", [0.0] * nupper)
            lle = args.get("lle", [0.0] * nlower)
            # OpenVSP の CST API に配列を渡す
            vsp.SetUpperCST(xsec_id, ule)
            vsp.SetLowerCST(xsec_id, lle)

        # ===== VKT翼型 (擬似実装: GetVKTAirfoilPnts + SetAirfoilPnts)  =====
        elif crv_type == vsp.XS_VKT_AIRFOIL:
            npts     = args.get("npts", 121)
            alpha    = args.get("alpha", 0.0) * math.pi / 180.0  # degree→rad
            epsilon  = args.get("epsilon", 0.1)
            kappa    = args.get("kappa", 0.1)
            tau      = args.get("tau", 10.0) * math.pi / 180.0   # degree→rad

            # VKT 点列を取得
            xyz_airfoil = vsp.GetVKTAirfoilPnts(npts, alpha, epsilon, kappa, tau)
            # --- numpy 配列に変換 ---
            xyz_np = np.array([[p.x(), p.y(), p.z()] for p in xyz_airfoil])

            # 上下面に分割
            half = npts // 2
            up_array  = xyz_airfoil[:half][::-1]
            low_array = xyz_airfoil[half:]

            # XS_FILE_AIRFOIL のときしか SetAirfoilPnts は使えないので一度変換
            vsp.ChangeXSecShape(xsec_surf_id, xsec_index, vsp.XS_FILE_AIRFOIL)
            xsec_id = vsp.GetXSec(xsec_surf_id, xsec_index)
            vsp.SetAirfoilPnts(xsec_id, up_array, low_array)

        # NACA 4 桁系列相当 : Camber (ピーク凸度) , CamberLoc (位置) , ThickChord (厚み) など
        elif crv_type == vsp.XS_FOUR_SERIES:
            set_parm(xsec_id, "Camber", 0.02)
            set_parm(xsec_id, "CamberLoc", 0.4)
            set_parm(xsec_id, "ThickChord", 0.12)
            # SharpTEFlag は後縁を鋭くするフラグ (boolean) 。True/False を渡す。
            set_parm(xsec_id, "SharpTEFlag", True)

        # NACA 5 桁系列相当 : IdealCl (設計揚力係数) 等を使用
        elif crv_type == vsp.XS_FIVE_DIGIT:
            set_parm(xsec_id, "IdealCl", 0.3)
            set_parm(xsec_id, "CamberLoc", 0.15)
            set_parm(xsec_id, "ThickChord", 0.12)
            set_parm(xsec_id, "SharpTEFlag", True)

        # 6 シリーズ相当
        elif crv_type == vsp.XS_SIX_SERIES:
            set_parm(xsec_id, "IdealCl", 0.3)
            set_parm(xsec_id, "ThickChord", 0.12)
            set_parm(xsec_id, "SharpTEFlag", True)

        # 4 桁修正版
        elif crv_type == vsp.XS_FOUR_DIGIT_MOD:
            # CamberInputFlag はcambar lineの入力方法を切り替えるフラグ (仕様確認推奨)
            set_parm(xsec_id, "CamberInputFlag", 0)
            set_parm(xsec_id, "Camber", 0.02)
            set_parm(xsec_id, "CamberLoc", 0.4)
            set_parm(xsec_id, "ThickChord", 0.12)
            set_parm(xsec_id, "SharpTEFlag", True)

        # 5 桁修正版
        elif crv_type == vsp.XS_FIVE_DIGIT_MOD:
            set_parm(xsec_id, "CamberInputFlag", 0)
            set_parm(xsec_id, "IdealCl", 0.3)
            set_parm(xsec_id, "CamberLoc", 0.15)
            set_parm(xsec_id, "ThickChord", 0.12)
            set_parm(xsec_id, "SharpTEFlag", True)

        # NACA 16 系列
        elif crv_type == vsp.XS_ONE_SIX_SERIES:
            set_parm(xsec_id, "IdealCl", 0.3)
            set_parm(xsec_id, "ThickChord", 0.12)
            set_parm(xsec_id, "SharpTEFlag", True)

        # 未サポートの断面タイプを受け取った場合は例外を投げる
        else:
            raise ValueError(f"Unsupported XSEC_CRV_TYPE: {crv_type}")

    # ===== NACAコード解析 =====
    def parse_naca_code(self, naca_str):
        '''

        '''

        m = re.match(r'NACA(\d{4,6})$', naca_str.replace('-', '').upper())
        if not m:
            raise ValueError("入力がNACA4〜6字形式になっていません。例: NACA2412, NACA23012, NACA63415")
        code = m.group(1)
        length = len(code)

        if length == 4:
            Camber = int(code[0]) / 100.0
            CamberLoc = int(code[1]) / 10.0
            ThickChord = int(code[2:]) / 100.0
            return {'series': 4, 'Camber': Camber, 'CamberLoc': CamberLoc, 'ThickChord': ThickChord}

        elif length == 5 and code[0] != '6': # 5-digit
            L = int(code[0])
            CamberLoc = int(code[1]) / 100 / 2
            ThickChord = int(code[3:]) / 100.0
            IdealCl = 0.15 * L
            return {'series': 5, 'IdealCl': IdealCl, 'CamberLoc': CamberLoc, 'ThickChord': ThickChord}

        elif length == 5 or length == 6: # 6 series
            series = int(code[0])
            IdealCl = int(code[-3]) / 10.0
            ThickChord = int(code[-2:]) / 100.0
            return {'series': series, 'IdealCl': IdealCl, 'ThickChord': ThickChord}

        else:
            raise ValueError("NACAコードの桁数が不正です。")

    # ===== XWIMPパーサ =====
    def parse_xwimp(self, path):
        with open(path, "r") as f:
            lines = [l.strip() for l in f.readlines() if l.strip()]
        name = lines[0]
        sec_data = []
        for l in lines[1:]:
            toks = l.split()
            sec_data.append({
                "y": float(toks[0]),
                "chord": float(toks[1]),
                "x_offset": float(toks[2]),
                "dihed": float(toks[3]),
                "twist": float(toks[4]),
                "foil_in": toks[9],
                "foil_out": toks[10],
            })
        return name, sec_data

    # ===== 翼生成 =====
    def make_wing_from_xwimp(self, path, is_vertical=False):
        name, sec_data = self.parse_xwimp(path)

        geom_id = vsp.AddGeom("WING")
        vsp.SetGeomName(geom_id, name)

        xsec_surf_id = vsp.GetXSecSurf(geom_id, 0)
        if is_vertical:
            vsp.SetParmVal(vsp.FindParm(geom_id, "Sym_Planar_Flag", "Sym"), 0.0)
            pid_rot = vsp.FindParm(geom_id, "X_Rel_Rotation", "XForm")
            if pid_rot: vsp.SetParmValUpdate(pid_rot, 90.0)

        pid_x = vsp.FindParm(geom_id, "X_Rel_Location", "XForm")
        vsp.SetParmValUpdate(pid_x, vsp.GetParmVal(pid_x)+sec_data[0]['x_offset'])

        num_to_add = max(0, len(sec_data)) - 1
        for i in range(1, num_to_add):
            vsp.InsertXSec(geom_id, i, vsp.XS_FOUR_SERIES)

        for i, (root_sec, tip_sec) in enumerate(zip(sec_data[:-1], sec_data[1:])):
            span = tip_sec['y'] - root_sec['y']
            sweep_le = np.rad2deg(np.arctan((tip_sec['x_offset'] - root_sec['x_offset']) / (tip_sec['y'] - root_sec['y'])))

            xsec_index = i+1
            xsec_id = vsp.GetXSec(xsec_surf_id, xsec_index)
            vsp.SetParmValUpdate(vsp.GetXSecParm(xsec_id, 'Span'),       span)
            vsp.SetParmValUpdate(vsp.GetXSecParm(xsec_id, 'Sweep'),      sweep_le)
            vsp.SetParmValUpdate(vsp.GetXSecParm(xsec_id, 'Twist'),      tip_sec['twist'])
            vsp.SetParmValUpdate(vsp.GetXSecParm(xsec_id, 'Dihedral'),   root_sec['dihed'])
            vsp.SetParmValUpdate(vsp.GetXSecParm(xsec_id, 'Tip_Chord'),  tip_sec["chord"])
            vsp.SetParmValUpdate(vsp.GetXSecParm(xsec_id, 'Root_Chord'), root_sec["chord"])
            vsp.SetParmValUpdate(vsp.GetXSecParm(xsec_id, 'Sec_Sweep_Location'), 0)

            foil_name = root_sec["foil_in"] if i == 0 else root_sec["foil_out"]
            if foil_name.upper().startswith("NACA"):
                args = self.parse_naca_code(foil_name)
                if args['series'] == 4:
                    crv_type = vsp.XS_FOUR_SERIES
                elif args['series'] == 5:
                    crv_type = vsp.XS_FIVE_DIGIT
                elif args['series'] == 6:
                    crv_type = vsp.XS_SIX_SERIES
                else:
                    raise ValueError("対応外の NACA 系列です")
            else:
                args = {'file': f'{foil_name}.dat'}
                crv_type = vsp.XS_FILE_AIRFOIL

            self.set_airfoil_xsec(xsec_surf_id, xsec_index-1, crv_type, args)
            self.set_airfoil_xsec(xsec_surf_id, xsec_index, crv_type, args)

        vsp.Update()
        return geom_id


# ===== 実行例 =====
if __name__ == "__main__":
    buildplane = BuildePlane()

def print_parm_discription(geom_id):
    parm_array = vsp.GetGeomParmIDs( geom_id )
    print(f'| {"parm_name":25s}', f'| {"type":<12s}', '| discription |')
    print('| --- | --- | --- |')
    for parm_id in parm_array:
        parm_name = vsp.GetParmName(parm_id)
        parm_type = vsp.GetParmType(parm_id)
        parm_discription = vsp.GetParmDescript(parm_id)

        print(f'| {parm_name:25s}', f'| {PARM_TYPE.get(parm_type, "Unknown"):12s}', '\t|', parm_discription.replace('Default Description', '-'), '|')

geom_id = vsp.AddGeom("WING")
vsp.SetGeomName(geom_id, "WingGeom")
print_parm_discription(geom_id)

def print_parm_discription(geom_id):
    parm_array = vsp.GetXSecParmIDs( geom_id )
    print(f'| {"parm_name":25s}', f'| {"type":<12s}', '| discription |')
    print('| --- | --- | --- |')
    for parm_id in parm_array:
        parm_name = vsp.GetParmName(parm_id)
        parm_type = vsp.GetParmType(parm_id)
        parm_discription = vsp.GetParmDescript(parm_id)

        print(f'| {parm_name:25s}', f'| {PARM_TYPE.get(parm_type, "Unknown"):12s}', '\t|', parm_discription.replace('Default Description', '-'), '|')

geom_id = vsp.AddGeom("WING")
vsp.SetGeomName(geom_id, "WingGeom")
xsec_surf_id = vsp.GetXSecSurf(geom_id, 0)
xsec_id = vsp.GetXSec(xsec_surf_id, 1)
print_parm_discription(xsec_id)

import os
import stat
import datetime
from pathlib import Path


# COLOR_RESET = "\033[0m"
# COLOR_DIR = "\033[34m"
# COLOR_LINK = "\033[36m"
# COLOR_FILE = "\033[0m"
COLOR_RESET = ""
COLOR_DIR = ""
COLOR_LINK = ""
COLOR_FILE = ""


def enable_windows_ansi():
    pass


def format_permissions(mode):
    """ls -l 形式のパーミッション"""
    return stat.filemode(mode)


def format_size_bytes(size):
    """バイト表示"""
    return str(size)


def format_size_human(size):
    """単位付きサイズ表示"""
    for unit in ["B", "K", "M", "G", "T", "P"]:
        if size < 1024:
            return f"{size}{unit}"
        size //= 1024
    return f"{size}E"


def format_time(ts):
    """ls -l 互換の時刻表示"""
    dt = datetime.datetime.fromtimestamp(ts)
    return dt.strftime("%Y-%m-%d %H:%M")


def classify_suffix(entry: Path):
    """ls -F 互換の種別表示"""
    if entry.is_symlink():
        return "@"
    if entry.is_dir():
        return "/"
    if entry.is_file() and os.access(entry, os.X_OK):
        return "*"
    if stat.S_ISFIFO(entry.stat().st_mode):
        return "|"
    if stat.S_ISSOCK(entry.stat().st_mode):
        return "="
    return ""


class Stats:
    def __init__(self):
        self.files = 0
        self.dirs = 0


def should_show(entry, all_files, dirs_only):
    if not all_files and entry.name.startswith("."):
        return False
    if dirs_only and not entry.is_dir():
        return False
    return True


def sorted_entries(path, dirsfirst):
    try:
        entries = list(path.iterdir())
    except PermissionError:
        return []

    if dirsfirst:
        return sorted(entries, key=lambda e: (not e.is_dir(), e.name.lower()))
    return sorted(entries, key=lambda e: e.name.lower())


def build_columns(entry, options):
    """表示列生成"""
    cols = []

    try:
        st = entry.lstat()
    except PermissionError:
        return ""

    if options["p"]:
        cols.append(format_permissions(st.st_mode))

    if options["s"]:
        cols.append(format_size_bytes(st.st_size))

    if options["h"]:
        cols.append(format_size_human(st.st_size))

    if options["D"]:
        cols.append(format_time(st.st_mtime))

    return " ".join(cols)


def colorize(name, entry, color):
    if not color:
        return name

    if entry.is_symlink():
        return f"{COLOR_LINK}{name}{COLOR_RESET}"
    if entry.is_dir():
        return f"{COLOR_DIR}{name}{COLOR_RESET}"
    return f"{COLOR_FILE}{name}{COLOR_RESET}"


def print_entry(prefix, entry, is_last, options):
    connector = "└── " if is_last else "├── "

    try:
        st = entry.lstat()
    except PermissionError:
        return

    columns = build_columns(entry, options)

    name = entry.name

    if options["F"]:
        name += classify_suffix(entry)

    if entry.is_symlink():
        try:
            target = os.readlink(entry)
            name += f" -> {target}"
        except OSError:
            pass

    name = colorize(name, entry, options["color"])

    if columns:
        print(f"{prefix}{connector}{columns} {name}")
    else:
        print(f"{prefix}{connector}{name}")


def walk(path, prefix, depth, options, stats):
    if options["max_depth"] is not None and depth > options["max_depth"]:
        return

    entries = [
        e for e in sorted_entries(path, options["dirsfirst"])
        if should_show(e, options["all_files"], options["dirs_only"])
    ]

    for i, entry in enumerate(entries):
        is_last = i == len(entries) - 1

        try:
            if entry.is_dir():
                stats.dirs += 1
            else:
                stats.files += 1
        except PermissionError:
            continue

        print_entry(prefix, entry, is_last, options)

        if entry.is_dir():
            extension = "    " if is_last else "│   "
            walk(entry, prefix + extension, depth + 1, options, stats)


def tree(
    path=".",
    all_files=False,
    max_depth=None,
    dirs_only=False,
    color=True,
    dirsfirst=False,
    p=False,
    s=False,
    h=False,
    D=False,
    F=False
):
    """
    Linux tree 互換表示関数
    """

    enable_windows_ansi()

    root = Path(path)

    if not root.exists():
        print("指定パスが存在しません")
        return

    options = {
        "all_files": all_files,
        "max_depth": max_depth,
        "dirs_only": dirs_only,
        "color": color,
        "dirsfirst": dirsfirst,
        "p": p,
        "s": s,
        "h": h,
        "D": D,
        "F": F,
    }

    stats = Stats()

    print(root)

    walk(root, "", 1, options, stats)

    print()
    print(f"{stats.dirs} directories, {stats.files} files")


if __name__ == "__main__":
    tree(".", dirsfirst=True, p=False, h=False, D=False, F=False)







質問・感想・意見などあれば気軽にTwitterのDMかコメントお願いします!
スポンサーリンク