FreeCADのPythonスクリプトでスケッチに角丸長方形を作図

FreeCAD の Pythonスクリプトでスケッチに角丸長方形を作図してみました。
いくつかの方法で試行錯誤してみましたが、一旦、LineSegment を一致拘束でつないで長方形を作ってから、一致拘束の情報をもとにArcを追加する方法が良さそうです。
角ごとに半径を変えることもできます。また、他の多角形にも使えそうです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1923193/e5f37a50-df7e-456c-9f6a-340a35db4728.png)

## ポイント
- LineSegment を一致拘束でつないで長方形を作る
- 後の処理でずれないように、縦線のx座標と横線のy座標を拘束しておく
- 角(一致拘束の位置)にArcを追加する
- 一致拘束の接続情報をもとに、ArcとLineSegmentの接線拘束を用意する(まだ追加しない)
- 一致拘束を削除する
(削除により拘束番号詰められてしまうので大きい番号から順に削除したほうが管理しやすい)
- 先ほど用意した接線拘束を追加する
- Arcに半径拘束を追加する


## スクリプトコード
```python
#Sketcherで角丸長方形
import FreeCAD as App
import Part
import Sketcher

# スケッチに長方形を追加する
def sketch_rectangle(
    sketch,
    pos = (0, 0),
    size = (10, 10)
):
    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))]
    #線分を追加
    geos = [sketch.addGeometry(Part.LineSegment(*ln)) for ln in l_ln]
    #拘束を追加
    l_cnst = [('Coincident', geos[i], 2, geos[(i+1)%len(geos)], 1) for i in range(len(geos))]
    l_cnst += [('DistanceY', -1, 1, geos[0], j, App.Units.Quantity(f'{pos[1]} mm')) for j in (1,2)]
    l_cnst += [('DistanceY', -1, 1, geos[2], j, App.Units.Quantity(f'{pos[1]+size[1]} mm')) for j in (1,2)]
    l_cnst += [('DistanceX', -1, 1, geos[1], j, App.Units.Quantity(f'{pos[0]+size[0]} mm')) for j in (1,2)]
    l_cnst += [('DistanceX', -1, 1, geos[3], j, App.Units.Quantity(f'{pos[0]} mm')) for j in (1,2)]
    cnsts = [sketch.addConstraint(Sketcher.Constraint(*con)) for con in l_cnst]
    doc.recompute()
    return geos, cnsts

# スケッチの図形の角を丸める
def sketch_fillet(
    sketch,
    cnsts,  #置き換え対象の拘束番号のリスト
    r = 1   #半径
):
    l_n_delcnst = []    #削除対象拘束番号リスト
    l_cnst = [] #追加する拘束
    for n_cnst in cnsts:
        cnst = sketch.Constraints[n_cnst]
        if cnst.Type != 'Coincident':
            continue
        l_n_delcnst.append(n_cnst)  #削除対象拘束番号リストに追加
        gn = [cnst.First, cnst.Second]  #一致拘束されている2つのGeometry番号
        g = [sketch.Geometry[n] for n in gn]    #一致拘束されている2つのGeometry
        cpn = [cnst.FirstPos, cnst.SecondPos]   #各Geometryの一致側頂点番号
        cp = g[0].EndPoint if (cpn[0]==2) else g[0].StartPoint  #一致点
        vg = [(g[i].StartPoint - cp) if (cpn[i]==2) else (g[i].EndPoint - cp) for i in range(2)]
        p = [cp + (v.normalize() * r) for v in vg]  #各Geometryの一致点からr入ったポイント
        #Arc追加
        gar = sketch.addGeometry(Part.Arc(p[0], cp, p[1]))
        #追加する拘束
        l_cnst += [('Tangent', gn[i], cpn[i], gar, i+1) for i in range(2)]
        l_cnst += [('Radius', gar, App.Units.Quantity(f'{r} mm'))]
    #拘束を削除
    for c2d in sorted(l_n_delcnst, reverse=True):
        sketch.delConstraint(c2d)
    #拘束を追加
    cnsts = [sketch.addConstraint(Sketcher.Constraint(*con)) for con in l_cnst]
    doc.recompute()

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

    sk = doc.addObject("Sketcher::SketchObject", "Sketch")

    geos, cnsts = sketch_rectangle(sk, (0,0), (100, 80))
    sketch_fillet(sk, cnsts[:4], 5) #スケッチの図形の角を丸める

    geos, cnsts = sketch_rectangle(sk, (20,20), (60, 40))
    #複数回に分けて丸めを適用する場合は番号の大きい一致拘束から適用する
    sketch_fillet(sk, cnsts[3:4], 6)
    sketch_fillet(sk, cnsts[2:3], 4)
    sketch_fillet(sk, cnsts[1:2], 2)
```