興味があろうがなかろうが。

なるべく役に立つ、とがった内容を記していきたいと思います。

【FreeCADプログラミング】ベクトル確認用の矢印ソリッドを作ろう

本日のお題は『ベクトル確認用の矢印ソリッドを作ろう』です。

ベクトルの計算をする時、ベクトルの数値自体は分かるものの、実際の方向が目視できず、分かりにくかったと思います。

今回は確認しやすいよう、押出しなどを駆使して、オブジェクトを作っていきます。

1. 環境

  • FreeCAD 0.18

2. 例題の内容

選択したフェースのピックポイント(選択位置)からソリッドを使って、法線ベクトルの方向を表示する。

3. 矢印ソリッドを作る手順

とりあえずパッと頭に浮かぶ手順は、下記の通りです。

  1. 円柱形状を作る
  2. 円錐形状を作る
  3. 円柱と円錐の和を取る
  4. ピックポイントの取得
  5. 法線ベクトルの取得
  6. 矢印ソリッドをピックポイント位置まで移動
  7. 法線ベクトル方向へ矢印ソリッドを向ける

ちなみに、矢印の方向が自在に変えられないと、プログラムとして使いにくいです。

今回は用意しませんが、GUIツールキットの『PyQt』を使って、パラメトリックなアプリを作るともっと良いプログラムになります。

4. プログラムを書く前に

4.1. マクロを使う

プログラムをゴリゴリ書いていくのも良いですが、こういう時は時間短縮のためにマクロを活用していきましょう。

とりあえず、上で書いた1~3までの手順をマクロに出したものが以下となります。
(一部文字化けしたり、余計な操作のコードが出力されていますが、気にしないでください)

import FreeCAD
import Part
import JoinFeatures
import BOPTools.JoinFeatures

App.ActiveDocument.addObject("Part::Cone","Cone")
App.ActiveDocument.ActiveObject.Label = "a??e??"
App.ActiveDocument.recompute()
#Gui.SendMsgToActiveView("ViewFit")
FreeCAD.getDocument("Unnamed").getObject("Cone").Radius1 = '0 mm'
FreeCAD.getDocument("Unnamed").getObject("Cone").Radius1 = '1 mm'
FreeCAD.getDocument("Unnamed").getObject("Cone").Radius1 = '0 mm'
FreeCAD.getDocument("Unnamed").getObject("Cone").Placement = App.Placement(App.Vector(0,0,0),App.Rotation(App.Vector(0,1,1),0))
FreeCAD.getDocument("Unnamed").getObject("Cone").Placement = App.Placement(App.Vector(0,0,0),App.Rotation(App.Vector(0,0,1),0))
FreeCAD.getDocument("Unnamed").getObject("Cone").Placement = App.Placement(App.Vector(0,0,0),App.Rotation(App.Vector(0,1,1),0))
FreeCAD.getDocument("Unnamed").getObject("Cone").Placement = App.Placement(App.Vector(0,0,0),App.Rotation(App.Vector(0,1,0),0))
FreeCAD.getDocument("Unnamed").getObject("Cone").Placement = App.Placement(App.Vector(0,0,0),App.Rotation(App.Vector(0,1,0),1))
FreeCAD.getDocument("Unnamed").getObject("Cone").Placement = App.Placement(App.Vector(0,0,0),App.Rotation(App.Vector(0,1,0),18))
FreeCAD.getDocument("Unnamed").getObject("Cone").Placement = App.Placement(App.Vector(0,0,0),App.Rotation(App.Vector(0,1,0),180))
App.ActiveDocument.addObject("Part::Cylinder","Cylinder")
App.ActiveDocument.ActiveObject.Label = "a??a?�}"
App.ActiveDocument.recompute()
#Gui.SendMsgToActiveView("ViewFit")

FreeCAD.getDocument("Unnamed").getObject("Cylinder").Placement = App.Placement(App.Vector(0,0,2),App.Rotation(App.Vector(0,0,1),0))
FreeCAD.getDocument("Unnamed").getObject("Cylinder").Placement = App.Placement(App.Vector(0,0,0),App.Rotation(App.Vector(0,0,1),0))
FreeCAD.getDocument("Unnamed").getObject("Cone").Placement = App.Placement(App.Vector(0,0,20),App.Rotation(App.Vector(0,1,0),180))
FreeCAD.getDocument("Unnamed").getObject("Cone").Placement = App.Placement(App.Vector(0,0,1),App.Rotation(App.Vector(0,1,0),180))
FreeCAD.getDocument("Unnamed").getObject("Cone").Placement = App.Placement(App.Vector(0,0,19),App.Rotation(App.Vector(0,1,0),180))
FreeCAD.getDocument("Unnamed").getObject("Cone").Placement = App.Placement(App.Vector(0,0,19.9),App.Rotation(App.Vector(0,1,0),180))
FreeCAD.getDocument("Unnamed").getObject("Cone").Placement = App.Placement(App.Vector(0,0,19.99),App.Rotation(App.Vector(0,1,0),180))
j = BOPTools.JoinFeatures.makeConnect(name = 'Connect')
j.Objects = [App.ActiveDocument.Cone]
j.Proxy.execute(j)
j.purgeTouched()

for obj in j.ViewObject.Proxy.claimChildren():
    obj.ViewObject.hide()

App.activeDocument().addObject("Part::MultiFuse","Fusion")
App.activeDocument().Fusion.Shapes = [App.activeDocument().Cylinder,App.activeDocument().Cone,]
#Gui.activeDocument().Cylinder.Visibility=False
#Gui.activeDocument().Cone.Visibility=False
#Gui.ActiveDocument.Fusion.ShapeColor=Gui.ActiveDocument.Cylinder.ShapeColor
#Gui.ActiveDocument.Fusion.DisplayMode=Gui.ActiveDocument.Cylinder.DisplayMode
App.ActiveDocument.recompute()

4.2. 吐き出したマクロの加工

これでは使い物にならないため、下記のようにちょっと加工します。

名前が『Unnamed』になっているドキュメントを拾うと、名前が変わった場合に使えないため、ActiveDocumentを拾うように加工しています。

また、不要なコードを除去しました。

今回はやっていませんが、メソッド化した方が後々使いやすいですね。

import FreeCAD
import Part
import JoinFeatures
import BOPTools.JoinFeatures

App.ActiveDocument.addObject("Part::Cone","Cone")
App.ActiveDocument.ActiveObject.Label = "Cone"
App.ActiveDocument.recompute()

cone = App.ActiveDocument.getObject("Cone")
App.ActiveDocument.getObject("Cone").Radius1 = '0 mm'
App.ActiveDocument.getObject("Cone").Placement = App.Placement(App.Vector(0,0,0),App.Rotation(App.Vector(0,1,0),180))

App.ActiveDocument.addObject("Part::Cylinder","Cylinder")
App.ActiveDocument.ActiveObject.Label = "Cylinder"
App.ActiveDocument.recompute()

cylinder = App.ActiveDocument.getObject("Cylinder")
App.ActiveDocument.getObject("Cone").Placement = App.Placement(App.Vector(0,0,19.99),App.Rotation(App.Vector(0,1,0),180))

App.activeDocument().addObject("Part::MultiFuse","Fusion")
App.activeDocument().Fusion.Shapes = [App.activeDocument().Cylinder,App.activeDocument().Cone,]
App.ActiveDocument.recompute()

5. 詳細なロジック検討

上で説明した手順1~3の操作は上記のプログラムでできることがわかりました。今度は4~7ですね。

どちらかと言えば、こちらの方が問題です。

少々調べた感じでは、ソリッドと位置と方向を入れれば勝手に移動してくれるスーパーメソッドは見当たらなかったので、地道に行きます。

5.1. FreeCADのクセを探る

まず前提として、FreeCADには以下のクセがあります。

  • 円錐・円柱形状のデフォルト作成方向がZ方向
  • デフォルトの高さが10mm
  • 特定の軸で回転させる際は、Placementの軸に回転軸にしたいベクトルを入力

5.2. 回転させるための軸方向取得

現状、回転軸になる方向は不明ですので、それを求めていきましょう。

私の場合、こういう時に一番最初に考えるのは『外積で求められる方向』です。

www.interested-or-not.com

もうお気づきかもしれませんが、外積を求めるための2つの方向成分は、既に登場しています。

  • Z方向 (円錐・円柱形状のデフォルト作成方向)
  • フェースの法線ベクトル

この2つのベクトルを外積のAPIに渡すことで、直交する方向(回転軸の方向)が求められます。

f:id:appli-get:20191021093653j:plain

回転方向と回転角度を求めるイメージ

5.3. 回転角度の取得

回転角度については、2ベクトルの内積で求めます。これも以前学習しましたね。

www.interested-or-not.com

5.4. ピックポイントの位置取得

こちらに関しては、以前紹介した『フェースの法線ベクトル取得』の記事で記載しましたので、割愛させて頂きます。

www.interested-or-not.com

6. プログラムを書いてみよう

以下に回答のプログラムを記載しています。

まずは見ないでチャレンジしてみてください。

# -*- coding: utf-8 -*-

#import ptvsd
#print("Waiting for debugger attach")
# 5678 is the default attach port in the VS Code debug configurations
#ptvsd.enable_attach(address=('localhost', 5678), redirect_output=True)
#ptvsd.wait_for_attach()

import FreeCAD
import Part
import JoinFeatures
import BOPTools.JoinFeatures
import math
import sys

# ビュー上の選択されたオブジェクトを取得
# (フェースを前提にしている。他のタイプが選択されたときの例外処理は入れていない)
sel = FreeCADGui.Selection.getSelectionEx()

if(len(sel) != 0):
    # ピック位置と、ピック位置の法線ベクトルを取得
    pickCoord = sel[0].PickedPoints[0]
    selectedFace = sel[0].SubObjects[0]
    faceSurface = selectedFace.Surface
    uvCoord = faceSurface.parameter(pickCoord)
    normalVec = selectedFace.normalAt(uvCoord[0], uvCoord[1])
    print(str(normalVec))
	
    # 円錐の作成(デフォルトはheightが10mmになる)
    App.ActiveDocument.addObject("Part::Cone","Cone")
    App.ActiveDocument.ActiveObject.Label = "Cone"
    App.ActiveDocument.recompute()
	cone = App.ActiveDocument.getObject("Cone")
    App.ActiveDocument.getObject("Cone").Radius1 = '0 mm'
    App.ActiveDocument.getObject("Cone").Placement = App.Placement(App.Vector(0,0,0),App.Rotation(App.Vector(0,1,0),180))

    # 円柱の作成(デフォルトはheightが10mmになる)
    App.ActiveDocument.addObject("Part::Cylinder","Cylinder")
    App.ActiveDocument.ActiveObject.Label = "Cylinder"
    App.ActiveDocument.recompute()
    
    # 円錐と円柱がスレスレの位置にあると、和が取れない可能性があるので、0.01mm重なるように設定
    cylinder = App.ActiveDocument.getObject("Cylinder")
    App.ActiveDocument.getObject("Cone").Placement = App.Placement(App.Vector(0,0,19.99),App.Rotation(App.Vector(0,1,0),180))
    
    # 円錐と円柱の和を取る
    App.activeDocument().addObject("Part::MultiFuse","Fusion")
    App.activeDocument().Fusion.Shapes = [App.activeDocument().Cylinder,App.activeDocument().Cone,]
    App.ActiveDocument.recompute()
    
    # 絶対座標系
    xAxis = FreeCAD.Vector(1,0,0)
    yAxis = FreeCAD.Vector(0,1,0)
    zAxis = FreeCAD.Vector(0,0,1)
    
    # 法線ベクトルを単位化(おそらくデフォルトで単位化されているはずだが念のため)
    normalVec.normalize()
    
    # Z軸との外積を取得する(Z軸を軸に外積を取るのがミソ)
    rotationAxis = zAxis.cross(normalVec)
    
    # 回転のAPIの入力が度数を欲しているため変換
    angle = zAxis.getAngle(normalVec) * 180 / math.pi
    
    # ソリッドの移動と回転
    App.activeDocument().getObject("Fusion").Placement = App.Placement(App.Vector(pickCoord.x,pickCoord.y,pickCoord.z),App.Rotation(rotationAxis, angle))

5. まとめ

  • 根性でAPIを見つける必要はない。時間短縮のためにマクロを最大限活用する。
  • CADのクセを認識するのは重要。(デフォルトの作成位置、作成方向など)

6. 最後に

ここに来て、今まで学習した内積・外積計算が役に立ち始めましたね。

基礎はやはり大事です。

更なる情報をお探しの方は!

下記のリンクから、FreeCADプログラミングのトップページに飛びます。

よろしければご参照くださいませ。

www.interested-or-not.com