オシロスコープのプローブホルダーをFreeCADのPythonスクリプトで作ってみた

# プローブホルダー
以前、[オシロスコープのプローブホルダーをOpenSCADで作ってみた](https://qiita.com/nachtele/items/33875916ed3719016d06)のとほぼ同じものを、FreeCADのPythonスクリプトで作ってみました。
OpenSCADは2021.01でリリースが止まっていますが、FreeCADは2024年11月18日にFreeCAD 1.0がリリースされ、今後にも期待が持てます。

![FreeCADのPythonスクリプトで生成したプローブホルダー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1923193/cbf7d43f-f595-4a4e-922f-dc2cd1df759c.png)

FreeCADはもちろんGUIだけでも様々なモデリングができますが、パラメーター化したPythonスクリプトを記述すれば、プローブを引っ掛ける穴の大きさ・数・位置や、壁の肉厚などを自在に変更できて便利です。

OpenSCADでプローブホルダーを作ったときは、壁とリブの接続部の内Rを形成するためにR形状の抜型を作ってそれを引き算してくり抜いていましたが。FreeCADにはフィレット機能があるので、下記のように、より単純な手順で作ることができました。
- 直方体から一回り小さい直方体をくり抜いて箱型を形成
- リブを立てる
- プローブ穴+スリットの壁部分を形成
- プローブ穴+スリットをくり抜く
- 外形の壁を楕円形にくり抜く
- 対象のエッジを選んでフィレットを形成

GUIでフィレット対象のエッジを選択するのはなかなか骨の折れる作業ですが、スクリプトなら(ルールが明確化できれば)簡単です。

コードを以下に示します。
繰り返し記述を簡略化するために、関数 box() と cylinder() を定義しています。

```python
import FreeCAD as App
import Part

# パラメーター
wt = 1.35   #肉厚
filletR = 1 #フィレット半径
sizeX = 90  #外形(x)
sizeY = 25  #外形(y)
sizeZ = 25  #外形(z)
hPosX = [12, 34, 56, 78]    #プローブ穴位置(x)
hPosY = 16  #プローブ穴位置(y)
hDia = 13   #プローブ穴直径
hSlit_w = 5     #プローブスリット幅
hSlitb_w = 10   #プローブスリットの壁部分の幅
hSlit_t = 5     #プローブスリット開口部の厚み
hDepth = 7      #プローブ穴の深さ

def box(
    xsize, ysize, zsize,    #サイズ
    pos = (0, 0, 0)         #位置
):
    bx = Part.makeBox(xsize, ysize, zsize, App.Vector(pos), App.Vector(0, 0, 1))
    return bx

def cylinder(
        r,      #半径
        len,    #長さ
        pos = (0, 0, 0),    #位置
        dir = (0, 0, 1),    #方向
        mat = App.Matrix()  #変形matrix
):
    cy = Part.makeCylinder(r, len, App.Vector(0, 0, 0),App.Vector(dir))
    if mat != App.Matrix():
        cy = cy.transformGeometry(mat)
    cy = cy.translate(App.Vector(pos))
    return cy


#外形の箱
ph = box(sizeX, sizeY, sizeZ)
bx = box((sizeX - wt*2), (sizeY - wt*2), sizeZ, (wt, wt, wt))
ph = ph.cut(bx)

#リブ(横)
bx = box((sizeX - (wt*2)), wt, (hDepth-wt), (wt, (hPosY -(wt/2)), wt))
ph = ph.fuse(bx)

for hx in hPosX:
    #リブ(縦)
    bx = box(wt, (hPosY - wt), (hDepth-wt), ((hx - (wt/2)), wt, wt))
    ph = ph.fuse(bx)
    #プローブ穴とスリットの壁
    cy = cylinder((hDia / 2) + wt, (hDepth-wt), (hx, hPosY, wt))
    ph = ph.fuse(cy)
    bx = box(hSlitb_w , (sizeY - hPosY - wt), (hDepth-wt), ((hx - (hSlitb_w/2)), hPosY, wt))
    ph = ph.fuse(bx)
    #プローブ穴
    cy = cylinder(hDia / 2, sizeZ, (hx, hPosY, 0))
    ph = ph.cut(cy)
    #スリット
    bx = box(hSlit_w, (sizeY - hPosY), sizeZ, ((hx - (hSlit_w/2)), hPosY, 0))
    ph = ph.cut(bx)

#外形に対して楕円柱を引き算する
r = sizeY-wt
mat = App.Matrix(); mat.scale(1, 1, (sizeY - hSlit_t) / r)
cy1 = cylinder(r, sizeX, (0, sizeY, sizeZ), (1, 0, 0), mat)
ph = ph.cut(cy1)

#フィレット
edges = ph.Edges
##外形のエッジのうちフィレット対象を抽出
fil_edges = []
for ed in edges:
    sel = False
    for vx in ed.Vertexes:
        if (
         #リブの接続部
         ((wt-0.1) < vx.Z < (wt+0.1))
         #スリットの外側
         or (((sizeY-0.1) < vx.Y < (sizeY+0.1)) and ((hSlit_t-0.1) < vx.Z < (hSlit_t+0.1)))
        ):
            sel = True
            break
    if sel:
        fil_edges.append(ed)
ph = ph.makeFillet(filletR, fil_edges)

Part.show(ph)
```

# おまけ:ケーブルトレイ
ついでに、プローブのケーブルを受けるトレイも作ってみました。
これは箱型をくり抜いてから短辺の壁をさらに切り下げて、すべてのエッジにフィレットを形成しただけなので簡単です。

![FreeCADのPythonスクリプトで生成したケーブルトレイ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1923193/c61c7c97-8946-4351-a326-05cf9ca8cee9.png)

```python
import FreeCAD as App
import Part

# パラメーター
wt = 1.35   #肉厚
filletR = wt/2.01  #フィレット半径
sizeX = 90  #外形(x)
sizeY = 25  #外形(y)
sizeZ = 25  #外形(z)
wallH = 10  #短辺壁高さ

def box(
    xsize, ysize, zsize,    #サイズ
    pos = (0, 0, 0)         #位置
):
    bx = Part.makeBox(xsize, ysize, zsize, App.Vector(pos), App.Vector(0, 0, 1))
    return bx


#外形の箱
ct = box(sizeX, sizeY, sizeZ)
bx = box((sizeX - wt*2), (sizeY - wt*2), sizeZ, (wt, wt, wt))
ct = ct.cut(bx)

#短辺壁
bx = box(sizeX, (sizeY - wt*2), sizeZ, (0, wt, wallH))
ct = ct.cut(bx)

#フィレット
edges = ct.Edges
ct = ct.makeFillet(filletR, edges)

Part.show(ct)
```

# 参考文献
https://wiki.freecad.org/Python_scripting_tutorial/ja

https://wiki.freecad.org/Topological_data_scripting