C#ATIA

↑タイトル詐欺 主にFusion360API 偶にCATIA V5 VBA(絶賛ネタ切れ中)

自身のフォルダパス

pythonで実行中のファイルのフォルダパスが欲しい時が結構あります。
設定ファイルや一時的なファイルを保管したり等です。

"os.getcwd()"で取得する方法もあるのですが、個人的には
”標準モジュールでpathlibあるんだから、そっち使えよ!”派です。
(使いこなせていません・・・)

pathlibを使用した場合は、こんな感じで目的のフォルダパスの
取得が出来ます。

import pathlib

print(pathlib.Path(__file__).parent)

"parent"が直感的で良いですね。

ずっとこれでOKだと思っていました。と言いますか、通常は
これでOKです。
そしてFusion360APIの場合も、これでOKです。

素のpython(Fusion360APIでは無い)での”通常"のサンプルは
こんな感じで作りました。

import pathlib
from tkinter import messagebox

def main():
    thisDir = pathlib.Path(__file__).parent
    messagebox.showinfo("ThisDir", thisDir)

if __name__ == "__main__":
    main()

これをVSCodeで実行するとこんな感じです。

これ(c:/temp/this_dir_test)は正解です。

実は困ったのがExeファイル化した際です。
こちらでも使用した"auto-py-to-exe"です。
auto-py-to-exe - C#ATIA

あぁ、ちょっとこれでも手間取ったので書き残しておきます。
こちらの"コンソール画面"ですが、自分の場合は"ウィンドウベース"に
すると、Windows Defenderに引っ掛かりファイルが作成されずに
削除されました。
念の為、対策方法を調べこちらを見つけて実行したのですが、
効果がありませんでした。
(auto-py-to-exeの実態はPyinstaller)
【Pyinstaller】Windows Defenderに引っかからないようにする #Python - Qiita
お陰様で、pythonを再インストールしクリーンな環境を手に
入れました・・・。

で、Exe化して実行した結果がこちらです。

おいおいおいおい、全然違うパスじゃないのさ。
何となく先日試した"tempfile"のパスっぽくも感じます。
Pyinstallerは一時的にファイルを作成し、そんな感じの処理で
実行させているんじゃないのかな?
一時的なフォルダ - C#ATIA

で、困った末解決案がこれです。

import pathlib
from tkinter import messagebox
import os

def main():
    # thisDir = pathlib.Path(__file__).parent
    thisDir = pathlib.Path(os.getcwd())
    messagebox.showinfo("ThisDir", thisDir)

if __name__ == "__main__":
    main()


結局、os.getcwd()使ってんじゃん
えぇPyinstallerを利用すると、これしかなさそうです・・・。

pathlib使わずに、これでもOKです。

    thisDir = os.getcwd()

でもpathlib好きです。(osによるパスのセパレーターを吸収してくれるので)

外周エッジを取得する

少し前ですが、こんな感じで複数の面から構成されているボディ(サーフェス
の外周の境界の線を取得したかった際に、ちょっと迷ったのでご紹介を。

CATIA V5の場合であれば、境界コマンドで簡単に作れます。
・・・厳密に言えば、外周の境界線から線を作るだけで、境界線そのもの
じゃないのですが。
但し、Fusion360ではそれすら ズバッと行う機能が無いと思います。

Fusion360ではソリッドであろうがサーフェスだろうがボディは"BRepBody"に
なります。ドキュメントのこちらのページにガッチリ?ゴッツリ?
説明があります。
Fusion Help

"BRepLump"についての表現を簡単にすれば、"2個のソリッドが1個のボディ"に
なっている状態です。CATIA V5ではこの状態を作る事が出来るのですが、
Fusion360の場合はGUIでこれを作ろうとしても強制的に1個のソリッドで1個のボディ
の状態になります。
(スケッチで離れた2個の円を描いて、1回の押し出して2個の円を選択すると、2個の
ボディが出来上がります)
結論からしてこれは無視です。

"BRepShell"はサーフェス的なイメージです。説明書きにあるように、中空のボディの
場合は、外側の面と内側の面が離れているにも関わらず1個のボディとして成立
しますね。ものづくりな視点で考えると "作れない形状じゃん" と思ったのですが、
3Dプリンタであれば作れそうな気もします。

"BRepFace"は面そのものです。そう1枚の面です。

"BRepLoop"は面の境界です。これは1枚の面(BRepFace)の境界であって、"BRepShell"
(複数の面がくっ付いた状態)の境界では無いです。
"BRepShell"にloopsプロパティがあれば目的が簡単に達成出来そうだと思ったの
ですが、残念な事にありませんでした。
Fusion Help

そこで"どうする?"となるのですが、結論から書くと"BRepCoEdge"を利用します。
あのリンク先の"BRepXXX"は"BRepCoEdge"以外はジオメトリ(幾何学)的な
要素なのですが"BRepCoEdge"のみトポロジー(位相幾何学)的な要素です。
上手く説明できていないかも知れませんが、

ジオメトリー:座標値や線の長さ等の形状的な情報
トポロジー:隣の面はどれ?このエッジを利用している面はどれ?等の関係性の情報

です・・・多分。

今回の場合はエッジについて注目するのですが、"BRepCoEdge"は
ハーフエッジデータ構造(個人的にはハーフエッジ構造と覚えていました)
を採用しています。

ハーフエッジデータ構造の説明書きを探した所こちらを見つけました。
ハーフエッジデータ構造を理解する #データ構造 - Qiita
難しいので理解できないですね・・・。

但し、分かっている事は赤のエッジの場合は片方のハーフエッジの反対側にも
ハーフエッジが存在しているのですが、緑のエッジは片方のハーフエッジ
反対側にはハーフエッジが無いと言う事です。

これさえ理解していれば、外周のエッジは"BRepEdge"のcoEdgesプロパティ
が1個だと言う事が分かります。
Fusion Help

そこでこんな感じにスクリプトを作りました。

# Fusion360API Python script

import traceback
import adsk.core as core
import adsk.fusion as fusion

def run(context):
    ui: core.UserInterface = None
    try:
        app: core.Application = core.Application.get()
        ui = app.userInterface
        des: fusion.Design = app.activeProduct
        root: fusion.Component = des.rootComponent

        body: fusion.BRepBody = root.bRepBodies[0]
        print(f"{body.name}:{body.faces.count}")

        outerEdges = [edge for edge in body.edges if edge.coEdges.count < 2]
        select_entities(outerEdges)

    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


def select_entities(entities: list):
    app: core.Application = core.Application.get()
    sels: core.Selections = app.userInterface.activeSelections

    sels.clear()
    [sels.add(x) for x in entities]

実際に実行した結果はこちらです。(左実行前 右実行後)

本当は数日前にもうちょっと詳しく書こうと思っていたのですが、
睡魔に負けてしまい・・・。

一時的なフォルダ

先日、OpenCVを試していた際に気が付いたのですが、OpenCVで処理を
行う画像ファイルのファイルパスは2バイト文字(全角)はNGっぽいです。
これにナカナカ気が付かずに戸惑いました。

過去にも2バイト文字NGとなる経験がありました。最初の頃は諦めていた
のですが、慣れてくると"ファイルを読み込む直前にリネームし、読み込み後
元の名前に戻せば良いんじゃない?"とか思い付き対応出来ていました。

しかし今回2バイト文字が含まれていたのは、ファイル名では無くフォルダ名
でした。

ちょっと話はそれますが、2バイト文字を利用しているのはアジア圏の
言語が多いです。(Copilotで調べました・・・)
世界の人口の60%はアジア人です。(Copilotで調べる前は3/4だと思ってました)
2バイト文字未対応のソフトウェア/ライブラリ/モジュールは60%もの
ユーザーを対象外としているので損だと思うのですけどね・・・。
(アジア圏でも1バイト文字で足りる言語もあると思うので、極論です)

で、フォルダ名のリネームは抵抗がある為、2バイト文字未使用の
ファイルパスとなる所にファイルをコピーし、コピーしたファイルを
読ませる事でエラーを回避する事にしました。

pythonの場合、この"一時的なフォルダの作成"に便利なモジュール
"tempfile"があります。標準モジュールなので本当に助かります。

検索で調べつつこんな感じで試しました。

# python
import tempfile
import pathlib

def main():
    with tempfile.TemporaryDirectory() as td:
        print(f"{td} : {pathlib.Path(td).exists()}")

    print(f"{td} : {pathlib.Path(td).exists()}")

if __name__ == '__main__':
    main()

実行結果はこんな感じです。

C:\Users\<ユーザー名>\AppData\Local\Temp\tmp4tnxeewh : True
C:\Users\<ユーザー名>\AppData\Local\Temp\tmp4tnxeewh : False

with文を抜けると一時的なフォルダが削除されます。便利です!!

ん?ちょっと待て。2バイト文字が含まれているのは<ユーザー名>
なんです。これじゃ意味ないじゃない・・・。

色々と検索しましたが、TemporaryDirectoryメソッドで指定した
フォルダ内に一時的なフォルダを作るようなサンプルが見当たりません。
結局、分かりにくいなと思いつつ公式のドキュメントへ
tempfile --- Generate temporary files and directories — Python 3.12.3 ドキュメント

パラメータ"dir"で指定するとそのフォルダ内に一時フォルダを
作ってくれそうなので、この様に修正しました。

・・・
def main():
    with tempfile.TemporaryDirectory(dir = "c:/") as td:
        print(f"{td} : {pathlib.Path(td).exists()}")
・・・

結果はこんな感じです。

c:/tmpeyrdezt7 : True
c:/tmpeyrdezt7 : False

処理中に例外が発生しても、勝手に削除してくれるようです。

tempfileモジュールの便利さを例えるのであれば、ハイテク便座で
立ち上がると勝手に流れるようなイメージでしょうか?
我が家は機能を止めているのですが、あれって健康診断等の検○の際に
機能を止め忘れ立ち上がってしまうと、途中で止められないし、
手でつかみ取るわけにもいかないし・・・ただ見守るしかないので
しょう。 あぁあれをWの悲劇と呼ぶのか。(トイレ=WC)

OpenCVで1

全く更新していなかった・・・実際何もしていなかったです。
(家を建ててから10年経つと、色々な契約等が切れて忙しすぎる。
もっとズレてやって来てくれないかな?)

最近、ちょっとOpenCVに触れたところ少し興味が出てきました。
イヤ深くでは無いです・・・。

Fusion360の図面をエクスポートする場合はPDFになりますよね?
但し、個人ユーザーはNGだとの記憶ですが。

で、図面の違いをOpenCVで・・・と思ったのですがPDFダメっぽいです。
その為、こんな作戦です。

PDF -> 画像の何か -> OpenCV

一応、Fusion360APIでは無く、素のPython(3.11)で行ってみました。
仮想環境下で開発するので、venvで環境を作成した後に以下を
インストール。(ひょっとしたらnumpyは不要かも・・・)

PDFを画像にする為のモジュールを探すと"pdf2image"が
見つかるのですが、他のソフト(?)をインストーラーでインストール
する必要があるっぽいので却下。探した所"PyMuPDF"が
見つかりました。

図面はこちら・・・以前、何かのテストで作ったもので根拠の
無いものです。(根拠のあるものを持っていない・・・)

以下を他サイトを参考にながら作成しました。

import traceback
from pathlib import Path
import fitz
import cv2

def pdf2png(file_path: str) -> str:
    """
    PDFファイルの1ページを画像ファイルに変換する
    """
    path_file = Path(file_path)
    folder_name = path_file.parent
    base_file_name = path_file.stem

    # PDFファイルを開く
    pdf = fitz.open(file_path)

    # 1ページのみ変換
    page = pdf[0]
    pix = page.get_pixmap()
    image_f_name = f'{base_file_name}.png'
    image_f_path = Path(folder_name) / image_f_name
    pix.save(image_f_path)

    return str(image_f_path)


def exec_diff_pic(
        picPath1: str,
        picPath2: str,
):
    """
    二つの画像を比較して、異なる部分に印を付けた画像を作成
    """
    try:
        img1 = cv2.imread(picPath1)
        img2 = cv2.imread(picPath2)

        # グレースケールに変換
        img1_gray = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
        img2_gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)

        # 絶対差を計算
        diff = cv2.absdiff(img1_gray, img2_gray)

        # しきい値を適用して重大な違いを識別する
        thresh = 30
        diff[diff < thresh] = 0
        diff[diff >= thresh] = 255

        # 差の輪郭を見つける
        contours, _ = cv2.findContours(
            diff, 
            cv2.RETR_EXTERNAL, 
            cv2.CHAIN_APPROX_SIMPLE
        )

        # 長方形を描画
        for contour in contours:
            (x, y, w, h) = cv2.boundingRect(contour)
            cv2.rectangle(img1, (x, y), (x + w, y + h), (0, 0, 255), 2)

        cv2.imwrite("diff.png", img1)

        # 表示
        cv2.imshow('Output', img1)
        cv2.waitKey(0)
        cv2.destroyAllWindows()

    except:
        print('Failed:\n{}'.format(traceback.format_exc()))


def main():
    """
    メイン処理
    """
    exec_diff_pic(
        pdf2png("box draw v18.pdf"),
        pdf2png("box draw v19.pdf"),
    )


if __name__ == '__main__':
    """
    エントリーポイント
    """
    main()

結果はこちら

本来なら途中の画像はtempfileで作るべきだろうけど・・・。

地球は丸かった

数年前に知ったのですが、世の中には"地球は平らだ"と思っている
方々が一定数いらっしゃるようです。・・・結構な人数だそうです。

地球が平らな可能性が無くも無いとは思いますし、完全な球体では
ない(確か、遠心力で赤道周回は地軸周回より長かったはず)とは
思いますが、まぁパッと見て"地球は丸い"と言う表現で多くの方々は
納得してもらえると思います。

もちろん直接丸い事を見たことはありませんが、陸から離岸した船が
水平線で消えていく様とか、衛星写真やら、宇宙に行かれた方の発言
やらを考慮した上で、"丸い"と認識しているんです。
・・・そう言われると、幽霊の存在、UFOの存在と変わらないレベルか?

"地球が平らだ"と思っている方々の場合、当人に問題があるわけでも無く
単純に知識・・・は言い過ぎだ、教育課程でそれらに触れる機会が
無かった為の結果なだけのような気がします。

幼少期であれば身近な大人の意見の影響力は絶大で、"地球は平ら!"と
言われ続け育てば、"丸い"と認識する事はかなり困難なように思えます。

大人になり"丸い"と認識出来るようになるためには、恐らく本人自身が
学ぶための気持ち(向上心)を強く持つ必要があるはずで、大勢の
周囲の人が"丸いんだよ"と教えても、平らなままの可能性が高いでしょう。


今は21世紀。20世紀のスタイルは難しいが極力そうする。
世間のスタイルとは逸脱している事に気が付かないのは、
外を知らないからさ。
でも、地球は丸いんだよ。

VBA-JSONのエスケープシーエンス

VBAjsonファイルを扱う際、調べるとVBA-JSON一択のような気がしてます。
GitHub - VBA-tools/VBA-JSON: JSON conversion and parsing for VBA
こんなの自力では作れないです・・・。

ちょっと困っているのが、エスケープシーエンスです。
実はファイルパスを含んだものをjsonで扱いたいのですが、上手く行かず
悩んでます。Issuesで検索すると、同じことを指摘しています。
A backslash is removed from double backslash · Issue #129 · VBA-tools/VBA-JSON · GitHub

この様なjsonがあるとします。

これを読み込み、保存すると以下の状態になります。

リンク先に記載されている通り、2重のバックスラッシュを
value側は問題無いのですが、key側の場合は1個消えてしまいます。

これを引き起こしているのは、保存時では無く読み込み時だとは
確認しているのですが、結構な再帰になっており読み取り切れず・・・。


結果的に、jsonのフォーマットを変えて対処しようかな・・。
(または¥を@等の他の文字にするとか)

三次元ベクトルの内積と長さ

三次元ベクトルを扱っていると外積やら内積やら単位化やら必要に
なってきますよね?

自分だったらベクトルクラス作って、メソッドにそれらを実装します。
外積内積や単位化は、ベクトルの為の関数なので、ベクトルクラスに
責任持ってもらうのが筋だと思いませんか?

ですが、訳あって関数で必要になりそうなので、切り出したのですが
今まで気が付きませんでした・・・。
内積と長さを求める関数です。

'三次元ベクトルの内積
'param: vec1_array(double)-ベクトル
'param: vec2_array(double)-ベクトル
'return: スカラー
Private Function dot_3d( _
        ByVal vec1 As Variant, _
        ByVal vec2 As Valiant) As Double

    dot_3d = _
        vec1(0) * vec2(0) + _
        vec1(1) * vec2(1) + _
        vec1(2) * vec2(2)

End Function


'三次元ベクトルの長さ
'param: vec_array(double)-ベクトル
'return: 長さ
Private Function get_length_3d( _
        ByVal vec As Variant) As Double

    get_length_3d = Sqr( _
        vec(0) * vec(0) + _
        vec(1) * vec(1) + _
        vec(2) * vec(2) _
    )

End Function

長さの計算の際に二乗で計算させると気が付きにくいのですが
長さの計算式って、自身同士の内積平方根なんですね。
だからこんな風に関数を作っても良さそう。

'三次元ベクトルの長さ
'param: vec_array(double)-ベクトル
'return: 長さ
Private Function get_length_3d( _
        ByVal vec As Variant) As Double

    get_length_3d = Sqr( _
        dot_3d(vec, vec) _
    )

End Function

無関係だけど、外積はこちら

'三次元ベクトルの外積
'param: vec1_array(double)-ベクトル
'param: vec2_array(double)-ベクトル
'return: array(double)-ベクトル
Private Function cross_3d( _
        ByVal vec1 As Variant, _
        ByVal vec2 As Valiant) As Variant

    cross_3d = Array( _
        vec1(1) * vec2(2) - vec1(2) * vec2(1), _
        vec1(2) * vec2(0) - vec1(0) * vec2(2), _
        vec1(0) * vec2(1) - vec1(1) * vec2(0) _
    )

End Function

Xの成分を求める計算はお互いのYZの利用して・・・
って感じで不思議な事は印象に残ってます。
(計算式はいつまでたっても覚えない)