FreeCADのPythonスクリプトで作った着脱ベースを改良してみた

# ベース板つき着脱プラグ
先日の[着脱式プローブホルダー](https://qiita.com/nachtele/items/28a9a551702dbceddff8)と[着脱式ケーブルトレイ](https://qiita.com/nachtele/items/4de51e1c2a30fdde5ee1)で一緒に作った着脱ベース部はプラグ部分のみしかなく、両面テープで貼り付ける面積が不足気味でした。そこで、ベース板にプラグが付いた形にしてみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1923193/861caed0-7942-476a-a741-48dd74bd7ed1.png)

## 加算/減算ロフト
今回は、加算ロフト、減算ロフトとフィレットだけで作りました。
前回同様、繰り返し使用する処理は関数化していますが、今回は、一つのloft関数で加算ロフトと減算ロフトのどちらも生成できるようにしてみました。
また、3つ以上のプロファイルでロフトを作ったときもスプライン補完されないようにRuledプロパティをTrueに設定しています。

## フェイス・エッジの分断解消
複数のシェイプを加算などでくっつけた場合、それらのフェイスやエッジが連続するような配置になっていても一体化されず継ぎ目が残ってしまうことがあり、フィレットの生成などで問題が出る場合があります。これを避けるためには下記の方法が考えられます。
- 大きめに作ってから、他のシェイプでカットする
- 加算を使わず減算のみで形成する

今回は、大きめに作ってから、減算ロフトで作った直方体でカットしています。
減算ロフトは好きな座標に生成できるので、減算直方体よりも便利です。


## スクリプトコード
```python
import FreeCAD as App
import Draft

# パラメーター
wt = 1.25   #肉厚
bsX = [[-15, +15]]  #ベースプレートx座標
bsX += [[0, n*30] for n in range(1,3)]
pgPosX = [[i*30 for i in range(n+1)] for n in range(3)]   #プラグx座標
pgSizeX = 10    #プラグ底部幅/2
pgSizeY = 3     #プラグ厚み
pgSizeY2 = 0.2  #ベース面とプラグとの間隔
pgSizeZ = 20    #プラグ高さ
pgGrdXZ = 1/10  #プラグ斜面の傾き(x/z)
pgGrdXY = 1/1   #プラグ斜面の傾き(x/y)
pgfilletR = 0.5 #プラグ部フィレット半径
pgfilletR2 = 0.199  #プラグ部フィレット半径(小)
addz = 1    #z方向を大きめに作って切り落とすサイズ(フェイス・エッジ分断解消のため)

#加算/減算ロフト
def loft(
    body,
    sections,
    a_s = 'Additive',   #Additive/Subtractive
    name = 'Loft',
    ruled = True    #線織面で生成するか
):
    lf = body.newObject(f'PartDesign::{a_s}Loft',name)
    lf.Profile = sections[0]
    lf.Sections += sections[1:]
    lf.Ruled = ruled
    for s in sections:
        s.Visibility = False 
    doc.recompute()
    return lf

#加算/減算ロフトによる多面体
def loft_hedron(
    body,
    l_vertices, #頂点リスト[[]]
    a_s = 'Additive',   #Additive/Subtractive
    name = 'alhedron',
    ruled = True
):
    l_w = [Draft.make_wire([App.Vector(p) for p in lp], closed=True) for lp in l_vertices]
    [body.addObject(w) for w in l_w]
    lf = loft(body, l_w, a_s, name, ruled)
    return lf

#フィレット
def fillet(
    body,
    shape,
    fil_edges,
    r = 1,      #フィレット半径
    name = 'Fillet'
):
    fil = body.newObject('PartDesign::Fillet', name)
    fil.Radius = r
    fil.Base = (shape, fil_edges)
    doc.recompute()
    return fil

#エッジ抽出
def edge_sel(
    shape,
    selfunc #エッジ判定コールバック関数
):
    edges = shape.Shape.Edges
    res = []
    for j, ed in enumerate(edges):
        if selfunc(ed):
            res.append(f'Edge{j+1}')
    return res


doc = App.activeDocument()
if doc == None:
    #アクティブドキュメントが存在しなければ新規作成
    doc = App.newDocument()

#ベース
z0 = -addz             #zサイズを大き目に作って最後に切り落とす
z1 = pgSizeZ + addz
for i, (bX, pX) in enumerate(zip(bsX, pgPosX)):
    bd_bs = doc.addObject('PartDesign::Body',f'base{i}')
    xdy = pgSizeY * pgGrdXY  #背面と内側のxサイズの差
    xdz = (z1-z0) * pgGrdXZ  #上下のxサイズの差
    dx = pgSizeX - xdy - (z0 * pgGrdXZ)
    x0 = bX[0] - dx
    x1 = bX[1] + dx
    l2_p = [
        [(x0, 0, z0), (x1, 0, z0), (x1-xdz, 0, z1), (x0+xdz, 0, z1)]
        ,[(x0, wt, z0), (x1, wt, z0), (x1-xdz, wt, z1), (x0+xdz, wt, z1)]
    ]
    bs = loft_hedron(bd_bs, l2_p, 'Additive', f'pg{i}')

    #プラグ
    y0 = wt
    y1 = y0 + pgSizeY2
    y2 = y1 + pgSizeY
    for j, x in enumerate(pX):
        dx = pgSizeX - (z0 * pgGrdXZ)
        x0 = x - dx
        x1 = x + dx
        l2_p = [
            [(x0+xdy, y0, z0), (x1-xdy, y0, z0), (x1-xdz-xdy, y0, z1), (x0+xdz+xdy, y0, z1)]
            ,[(x0+xdy, y1, z0), (x1-xdy, y1, z0), (x1-xdz-xdy, y1, z1), (x0+xdz+xdy, y1, z1)]
            ,[(x0, y2, z0), (x1, y2, z0), (x1-xdz, y2, z1), (x0+xdz, y2, z1)]
        ]
        bs = loft_hedron(bd_bs, l2_p, 'Additive', f'pg{i}{j}')

    #フェイス・エッジ分断解消のために大きめに作った分をカットする
    x0 = bX[0] - pgSizeX
    x1 = bX[1] + pgSizeX
    for j, z10 in enumerate([z0, pgSizeZ]):
        z11 = z10 + addz
        l2_p = [
            [(x0, 0, z10), (x1, 0, z10), (x1, y2, z10), (x0, y2, z10)]
            ,[(x0, 0, z11), (x1, 0, z11), (x1, y2, z11), (x0, y2, z11)]
        ]
        bs = loft_hedron(bd_bs, l2_p, 'Subtractive', f'cut{i}{j}')

    #フィレット
    def esel0(edge): #エッジ判定コールバック関数
        r = ((abs(edge.Vertexes[1].Y - edge.Vertexes[0].Y)) < 0.1)
        r = r and ((wt+pgSizeY+pgSizeY2-0.1) <edge.Vertexes[1].Y)
        r = r and not((abs(edge.Vertexes[1].Z - edge.Vertexes[0].Z)) < 0.1)
        return r
    fil_edges = edge_sel(bs, esel0)
    bs = fillet(bd_bs, bs, fil_edges, pgfilletR, f'bsfil{i}0')

    def esel1(edge): #エッジ判定コールバック関数
        r = ((abs(edge.Vertexes[1].Z - edge.Vertexes[0].Z)) < 0.1)
        r = r and (0.1 < edge.Vertexes[0].Y)
        r = r or ((wt-0.1) < edge.Vertexes[0].Y < (wt+0.1))
        return r
    fil_edges = edge_sel(bs, esel1)
    bs = fillet(bd_bs, bs, fil_edges, pgfilletR2, f'bsfil{i}1')

    #見やすいように位置を移動
    bd_bs.Placement=App.Placement(App.Vector(0, (i*20), 0), App.Rotation(App.Vector(0,0,1),0), App.Vector(0,0,0))
```