オシロスコープのプローブホルダーを着脱式にしてみた

# 着脱式プローブホルダー

先日の[プローブホルダー](https://qiita.com/nachtele/items/e822da2b1283561d504f)をについて、必要に応じてオシロスコープから取り外しできるように着脱式にしてみました。楔型のベース部のみをオシロスコープやラックなどに両面テープで固定しておいて、本体部分を上からはめ込みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1923193/2fbc46ba-4ab8-480d-9d19-9f5cb4fdf76c.png)

楔型のベース部は、厚みツールで肉抜きしたものと、していないものの2種類を作ってみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1923193/8aaa4a80-7527-4499-ae53-90ac1b56f5d5.png)

楔型といっても、底面が台形になっているので加算ウェッジでは生成できません。また、側面の角度が同一でないので押し出しでも生成できません。そこで、加算ロフトを使うことにしました。

ベース部を受ける本体側のソケット部は、加算ロフトで作った雛形をブーリアン演算でカットして形成しています。

```python
import FreeCAD as App
import Part
import Draft
import Sketcher

# パラメーター
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.5    #プローブ穴の深さ
bsPosX = [15, 45, (sizeX-15)]   #着脱ソケット位置(x)
bsSizeX = 10    #着脱ベース底部幅/2
bsSizeY = 3     #着脱ベース厚み
bsSizeZ = 20    #着脱ベース高さ
bsGrdXZ = 1/10  #着脱ベース斜面の傾き(x/z)
bsGrdXY = 1/1   #着脱ベース斜面の傾き(x/y)
bsfilletR = 0.5 #着脱ベース部フィレット半径
scSizeX = bsSizeX + wt  #着脱ソケット幅/2
scSizeY = 3 + wt    #着脱ベース厚み
scSizeZ = sizeZ - 2 #着脱ソケット高さ
scfilletR = 0.2 #着脱ソケット部フィレット半径

def box(
    body,   #追加先ボディ
    name = 'Box',   #名前
    size = (10, 20, 30),    #サイズ
):
    feature = App.ActiveDocument.addObject('PartDesign::AdditiveBox', name)
    feature.Length = size[0]
    feature.Width = size[1]
    feature.Height = size[2]
    b = body.addObject(feature)
    doc.recompute()
    return b[0]

#加算ロフト
def aloft(
    body,
    sections,
    name = 'AdditiveLoft'
):
    lf = body.newObject('PartDesign::AdditiveLoft',name)
    lf.Profile = sections[0]
    lf.Sections += sections[1:]
    for s in sections:
        s.Visibility = False 
    return lf

#加算ロフトによる多面体
def aloft_hedron(
    body,
    l_vertices, #頂点リスト[[]]
    name = 'alhedron'
):
    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 = aloft(body, l_w, name)
    return lf

#ブーリアン演算
def pd_boolean(
    body,   #演算を受けるボディ
    l_obj,  #演算するボディ
    type = 'Fuse',
    name = 'Boolean',
):
    bl = body.newObject('PartDesign::Boolean', name)
    bl.addObjects(l_obj)
    bl.Type = type
    for o in l_obj:
        o.Visibility = False 
    return bl

# 厚みツール
def thickness(
    body,
    shape,  #適用対象図形
    face,
    name = 'Thickness',
    wth = 1 #厚み
):
    th = body.newObject('PartDesign::Thickness', name)
    th.Base = shape, face
    th.Value = wth
    return th

#スケッチを作ってアタッチ
def sketch_attach(
    shape,  #適用対象図形
    face,
    name = 'Sketch'
):
    sk = bd_ph.newObject('Sketcher::SketchObject', name)
    sk.AttachmentSupport = shape, face
    sk.MapMode = 'FlatFace'
    return sk

# スケッチに長方形を追加する
def sketch_square(
    sketch,
    pos = (0, 0),
    size = (10, 10)
):
    n_sg0 = len(sketch.Geometry)    #作業前のスケッチ内の図形要素数
    vt = [  #頂点リスト
        App.Vector(pos[0], pos[1], 0)
        ,App.Vector((pos[0]+size[0]), pos[1], 0)
        ,App.Vector((pos[0]+size[0]), (pos[1]+size[1]), 0)
        ,App.Vector(pos[0], (pos[1]+size[1]), 0)
    ]
    #ラインの頂点リスト
    l_ln = [[vt[i], vt[(i+1) % len(vt)]] for i in range(len(vt))]
    #ラインを追加
    for ln in l_ln:
        sketch.addGeometry(Part.LineSegment(*ln))
    #一致拘束を追加
    for i in range(len(l_ln)):
        ln0 = n_sg0 + i     #基準のライン番号
        ln1 = n_sg0 + (i+1) % len(l_ln) #制約を適用することで移動するライン番号
        sketch.addConstraint(Sketcher.Constraint("Coincident", ln0, 2, ln1, 1))

# スケッチに円を追加する
def sketch_circle(
    sketch,
    pos = (0, 0),
    r = 10      #半径
):
    v_pos = App.Vector(pos[0], pos[1], 0)
    v_ax = App.Vector(0, 0, 1)
    sketch.addGeometry(Part.Circle(v_pos, v_ax, r))

# スケッチに楕円を追加する
def sketch_ellipse(
    sketch,
    center = (0, 0),    #中心座標 
    ma_ax = (0, 20),    #長軸
    mi_ax = (10, 0)     #短軸
):
    v_c = App.Vector(center[0], center[1], 0)
    v_a = App.Vector((center[0]+ma_ax[0]), (center[1]+ma_ax[1]), 0)
    v_i = App.Vector((center[0]+mi_ax[0]), (center[1]+mi_ax[1]), 0)
    sketch.addGeometry(Part.Ellipse(v_a, v_i, v_c))

# 押し出し
def pad(
    body,
    profile,
    name = 'Pad',
    len = 10    #押し出し長
):
    pd = body.newObject('PartDesign::Pad', name)
    pd.Profile = profile
    pd.Length = len
    if type(profile) != list:
        profile.Visibility = False
    return pd

# ポケット作成
def pocket(
    body,
    profile,
    name = 'Pocket',
    ptype = 'Dimension',
    len = 10    
):
    pk = body.newObject('PartDesign::Pocket', name)
    pk.Profile = profile
    pk.Type = ptype
    pk.Length = len
    if type(profile) != list:
        profile.Visibility = False
    return pk


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


#着脱ベース
for i in range(2):  #0:肉抜きなし 1:あり
    bd_bs = doc.addObject('PartDesign::Body',f'base{i}')
    x = sizeX + (30 * (i+1))
    bx0 = x - bsSizeX
    bx1 = x + bsSizeX
    xdy = bsSizeY * bsGrdXY  #背面と内側のxサイズの差
    xdz = bsSizeZ * bsGrdXZ  #上下のxサイズの差
    bz0 = sizeZ - bsSizeZ
    bz1 = sizeZ
    l2_p = [
        [(bx0+xdz+xdy, 0, bz0), (bx1-xdz-xdy, 0, bz0), (bx1-xdy, 0, bz1), (bx0+xdy, 0, bz1)]
        ,[(bx0+xdz, bsSizeY, bz0), (bx1-xdz, bsSizeY, bz0), (bx1, bsSizeY, bz1), (bx0, bsSizeY, bz1)]
    ]
    bs = aloft_hedron(bd_bs, l2_p, f'pg{i}')
    doc.recompute()

    #肉厚分の箱にくり抜く
    if i > 0:
        faces = bs.Shape.Faces
        bf = [f'Face{i+1}' for i,f in enumerate(faces) if (bsSizeY-0.1) < f.CenterOfMass.y < (bsSizeY+0.1)]
        bs = thickness(bd_bs, bs, bf, f'bs_ol{i}', wt)
        doc.recompute()

    #フィレット
    edges = bs.Shape.Edges
    fil_edges = []
    for i, ed in enumerate(edges):
        sel = False
        for vx in ed.Vertexes:
            if (
            ((0+0.1) < vx.Y)
            ):
                sel = True
                break
        if sel:
            fil_edges.append(f'Edge{i+1}')
    fil = bd_bs.newObject('PartDesign::Fillet',f'bsfil{i}')
    fil.Radius = bsfilletR
    fil.Base = (bs, fil_edges)
    doc.recompute()


#プローブホルダーのボディ
bd_ph = doc.addObject('PartDesign::Body','probeholder')

#外形の箱
ph = box(bd_ph, 'outline', (sizeX, sizeY, sizeZ))

#肉厚分の箱にくり抜く
faces = ph.Shape.Faces
bf = [f'Face{i+1}' for i,f in enumerate(faces) if (sizeZ-0.1) < f.CenterOfMass.z < (sizeZ+0.1)]
ph = thickness(bd_ph, ph, bf, 'box_ol', wt)
doc.recompute()

#着脱ソケット用に背面の厚みを増やす
faces = ph.Shape.Faces
bf = [f'Face{i+1}' for i,f in enumerate(faces) if (wt-0.1) < f.CenterOfMass.y < (wt+0.1)]
ph = pad(bd_ph, [ph, bf],'socketwall', (bsSizeY))

#着脱ソケット
for i, x in enumerate(bsPosX):
    bd_sc = doc.addObject('PartDesign::Body',f'socket{i}')
    bx0 = x - bsSizeX
    bx1 = x + bsSizeX
    xdy = bsSizeY * bsGrdXY  #背面と内側のxサイズの差
    xdz = scSizeZ * bsGrdXZ  #上下のxサイズの差
    bz0 = sizeZ - scSizeZ
    bz1 = sizeZ
    l2_p = [
        [(bx0+xdz+xdy, 0, bz0), (bx1-xdz-xdy, 0, bz0), (bx1-xdy, 0, bz1), (bx0+xdy, 0, bz1)]
        ,[(bx0+xdz, bsSizeY, bz0), (bx1-xdz, bsSizeY, bz0), (bx1, bsSizeY, bz1), (bx0, bsSizeY, bz1)]
    ]
    sc = aloft_hedron(bd_sc, l2_p, f'sc{i}')
    ph = pd_boolean(bd_ph, [bd_sc], 'Cut', f'bsc{i}')
doc.recompute()

#プローブ穴の壁を作る
faces = ph.Shape.Faces
bf = [f'Face{i+1}' for i,f in enumerate(faces) if (wt-0.1) < f.CenterOfMass.z < (wt+0.1)]
sk = sketch_attach(ph, bf[0], 'sk_holewall')
for hx in hPosX:
    #プローブ穴の壁
    sketch_circle(sk, [hx, hPosY], ((hDia / 2) + wt))
ph = pad(bd_ph, sk,'holewall', (hDepth-wt))
doc.recompute()

#リブを作る
faces = ph.Shape.Faces
bf = [f'Face{i+1}' for i,f in enumerate(faces) if (wt-0.1) < f.CenterOfMass.z < (wt+0.1)]
sk = sketch_attach(ph, bf[0], 'sk_rib')
#リブ(横)
sketch_square(sk, [0, (hPosY-(wt/2))], [sizeX, wt])
for hx in hPosX:
    #リブ(縦)
    sketch_square(sk, [(hx-(wt/2)), scSizeY], [wt, hPosY-scSizeY-(wt/2)])
    #スリットの壁
    sketch_square(sk, [(hx-(hSlitb_w/2)), (hPosY+(wt/2))], [hSlitb_w, (sizeY-hPosY-(wt/2))])
ph = pad(bd_ph, sk,'rib', (hDepth-wt))
doc.recompute()

#プローブ穴を作る
faces = ph.Shape.Faces
bf = [f'Face{i+1}' for i,f in enumerate(faces) if (0-0.1) < f.CenterOfMass.z < (0+0.1)]
sk = sketch_attach(ph, bf[0], 'sk_hole')
for hx in hPosX:
    #プローブ穴
    sketch_circle(sk, [hx, -(hPosY)], (hDia / 2))
ph = pocket(bd_ph, sk,'hole', 'ThroughAll')
doc.recompute()

#プローブスリットを作る
faces = ph.Shape.Faces
bf = [f'Face{i+1}' for i,f in enumerate(faces) if (0-0.1) < f.CenterOfMass.z < (0+0.1)]
sk = sketch_attach(ph, bf[0], 'sk_slit')
for hx in hPosX:
    #スリット
    sketch_square(sk, [(hx-(hSlit_w/2)), -(hPosY)], [hSlit_w, -(sizeY-hPosY)])
ph = pocket(bd_ph, sk,'slit', 'ThroughAll')
doc.recompute()

#外形の横壁を楕円形にくり抜く
faces = ph.Shape.Faces
bf = [f'Face{i+1}' for i,f in enumerate(faces) if (0-0.1) < f.CenterOfMass.x < (0+0.1)]
sk = sketch_attach(ph, bf[0], 'sk_sidewall')
#楕円
center = (-(sizeY), sizeZ)  #中心座標 
w = sizeY-filletR-bsSizeY-wt    #幅
h = sizeZ-hSlit_t       #高さ
if w > h:
    ma_ax = (w, 0)  #長軸
    mi_ax = (0, h)  #短軸
else:
    ma_ax = (0, h)
    mi_ax = (w, 0)
sketch_ellipse(sk, center, ma_ax, mi_ax)
ph = pocket(bd_ph, sk,'sidewall', 'ThroughAll')
doc.recompute()

#フィレット
edges = ph.Shape.Edges
##外形のエッジのうちフィレット対象を抽出
fil_edges = []
for i, ed in enumerate(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(f'Edge{i+1}')
fil = bd_ph.newObject('PartDesign::Fillet','Fillet')
fil.Radius = filletR
fil.Base = (ph, fil_edges)
doc.recompute()
ph = fil

##着脱ソケット部のフィレット対象を抽出
edges = ph.Shape.Edges
fil_edges = []
for i, ed in enumerate(edges):
    sel = False
    for vx in ed.Vertexes:
        if (
         #着脱ソケット部
         (((0-0.1) < vx.Y < (bsSizeY+0.1)) and ((0+0.1) < vx.Z))
        ):
            sel = True
            break
    if sel:
        fil_edges.append(f'Edge{i+1}')
fil = bd_ph.newObject('PartDesign::Fillet','Fillet')
fil.Radius = scfilletR
fil.Base = (ph, fil_edges)
doc.recompute()
ph = fil
```