Claude CodeでezdxfのDXFを作る — 横断図・縦断図を読み戻して検証
測点・計画高・地盤高のCSVから縦断図と横断図をezdxfでDXFに書き出し、レイヤ名・頂点座標・寸法の測定値を読み戻して照合する反復を、Claude Codeの指示例と実機の出力付きで示します。
ezdxfはPythonでDXFファイルを作り、読み、書き換えるパッケージです。測点ごとの地盤高と計画高の表があれば、縦断図と横断図のDXFはスクリプトから出せます。Claude Codeに任せるときの要点は、書き出したDXFをreadfileで読み戻し、レイヤ名と頂点座標と寸法の値を表から再計算した値と突き合わせるところまでを1回の反復に含めることです。図面は目で見て「それらしい」と感じやすく、座標のずれは見落とされがちです。
この記事では、6測点の縦断図と1断面の横断図を題材に、生成スクリプト、検証スクリプト、Claude Codeへの指示の置き方を順に示します。コードは手元のPython 3.12とezdxf 1.4.5で実行した結果にもとづきます。
土木の2D図面をezdxfで書くときの前提
ezdxfのドキュメントは、対象読者を「プログラマー」とし、DXFの新規作成と既存DXFの読み込み・変更・書き込みができると説明しています。対応するDXFバージョンはR12、R2000、R2004、R2007、R2010、R2013、R2018で、Python 3.11以上が必要です。MITライセンスで、依存パッケージはtyping_extensions、pyparsing、numpy、fontToolsです。
Claude Codeは、コードベースを読み、ファイルを編集し、コマンドを実行するエージェント型のコーディングツールです。したがって「スクリプトを書く」「実行する」「出力を読んで直す」を1つの流れで回せます。CADソフトそのものを操作するのではなく、DXFを吐くPythonを書かせて、吐かれたDXFをPythonで読み戻す構成です。
3D形状は別の記事があります。CadQueryの寸法アサーションは立体が対象で、ここで扱う縦断・横断の2D図面は線とレイヤと寸法の世界になります。
入力は2枚のCSVに固定する
図面の元データは、スクリプトに数値を埋め込まずCSVに分けます。Claude Codeに「この表から図を描いて」と頼むときも、表が1か所にあれば、差し替えや検算の対象がはっきりします。
縦断図の元になる測点表は、次の形です。追加距離は起点からの距離(m)、地盤高と計画高は標高(m)です。
station,distance_m,ground_m,design_m
No.0,0,10.20,10.50
No.1,20,10.05,10.40
No.2,40,9.70,10.30
No.3,60,9.40,10.10
No.4,80,9.55,10.00
No.5,100,9.90,9.90横断図は、No.3の断面について、中心線からのオフセット(左がマイナス)と地盤高を並べます。
offset_m,ground_m
-8,9.10
-4,9.45
0,9.40
4,9.30
8,9.00数値はこの記事用の例で、実在の路線とは関係ありません。実務では測量成果や設計図書の表から起こすことになります。
縦断図と横断図を書き出すスクリプト
縦断図は追加距離をx、(高さ − 基準高) × 縦倍率をyにとります。縦倍率は、延長100mに対して高低差が1m前後の縦断図を読める形にするための設定で、ここでは10倍の例にしました。横断図は縦横とも等倍で、縦断図の下にずらして同じモデル空間に置いています。
python3 -m venv .venv
.venv/bin/pip install ezdxf縦倍率・基準高・レイヤ名は、生成と検証の両方が使う値なので、設定ファイルconfig.pyに1か所だけ置きます。
V_EXAG = 10 # 縦断図の縦倍率
DATUM_M = 8.0 # 縦断図の基準高(m)
LAYERS = {"GROUND": 3, "DESIGN": 1, "DATUM": 8, "TEXT": 7, "DIM": 2,
"XS-GROUND": 3}生成スクリプトmake_dxf.pyは、このconfig.pyを読み込みます。
import csv
import ezdxf
from ezdxf import units
from config import V_EXAG, DATUM_M, LAYERS
doc = ezdxf.new("R2018", setup=True)
doc.units = units.M
for name, color in LAYERS.items():
doc.layers.add(name, color=color)
msp = doc.modelspace()
# 縦断図: 追加距離を x、(高さ - 基準高) x 倍率を y にする
rows = list(csv.DictReader(open("stations.csv")))
def profile(key):
return [(float(r["distance_m"]), (float(r[key]) - DATUM_M) * V_EXAG)
for r in rows]
msp.add_lwpolyline(profile("ground_m"), dxfattribs={"layer": "GROUND"})
msp.add_lwpolyline(profile("design_m"), dxfattribs={"layer": "DESIGN"})
msp.add_line((0, 0), (100, 0), dxfattribs={"layer": "DATUM"})
for r in rows:
msp.add_text(
f'{r["station"]} GH={r["ground_m"]} FH={r["design_m"]}',
height=0.8,
dxfattribs={"layer": "TEXT", "rotation": 90,
"insert": (float(r["distance_m"]), -3)},
)
dim = msp.add_linear_dim(base=(0, -8), p1=(0, 0), p2=(100, 0),
dimstyle="EZDXF", dxfattribs={"layer": "DIM"})
dim.render()
# 横断図(No.3): 縦横とも等倍。原点を縦断図の下へずらして置く
OX, OY = 0, -40
cross = list(csv.DictReader(open("cross.csv")))
msp.add_lwpolyline(
[(OX + float(c["offset_m"]), OY + float(c["ground_m"])) for c in cross],
dxfattribs={"layer": "XS-GROUND"},
)
doc.saveas("sections.dxf")押さえておく点は4つあります。
setup=Trueを付けると既定の寸法スタイル(EZDXF)が定義され、add_linear_dimで使えます。- 寸法は
add_linear_dimだけでは完結せず、render()で寸法線の図形をブロックとして作る2段階です。ドキュメントのサンプルにも「Necessary second step」とあります。 - レイヤ名は例です。発注者がCAD製図基準でレイヤ名を指定している案件では、その表記に合わせます。
doc.units = units.Mは、モデル空間の単位をメートルにする設定です。
読み戻して表と照合する検証スクリプト
書き出しの直後に、別のスクリプトでDXFを読み直します。ここで見るのは「描けたか」ではなく「表の値と一致しているか」です。
import csv
import ezdxf
from ezdxf import recover
from config import V_EXAG, DATUM_M, LAYERS
doc, auditor = recover.readfile("sections.dxf")
assert not auditor.has_errors, auditor.errors
msp = doc.modelspace()
# 1. レイヤ: 定義済みで、図形が使うレイヤもすべて定義済み
defined = {layer.dxf.name for layer in doc.layers}
used = {e.dxf.layer for e in msp}
assert used <= defined, f"未定義レイヤ: {used - defined}"
for name in LAYERS:
assert name in defined, f"レイヤがない: {name}"
# 2. 縦断図: 頂点を表の値から再計算した座標と突き合わせる
rows = list(csv.DictReader(open("stations.csv")))
for layer, key in [("GROUND", "ground_m"), ("DESIGN", "design_m")]:
pl = msp.query(f'LWPOLYLINE[layer=="{layer}"]').first
pts = list(pl.get_points("xy"))
assert len(pts) == len(rows), (layer, len(pts))
for (x, y), r in zip(pts, rows):
assert abs(x - float(r["distance_m"])) < 1e-6, (layer, r)
assert abs(y - (float(r[key]) - DATUM_M) * V_EXAG) < 1e-6, (layer, r)
# 3. 横断図: 横方向の幅が表の最大オフセット幅と一致する
cross = list(csv.DictReader(open("cross.csv")))
xs = [x for x, _ in msp.query('LWPOLYLINE[layer=="XS-GROUND"]')
.first.get_points("xy")]
width = max(float(c["offset_m"]) for c in cross) - min(
float(c["offset_m"]) for c in cross)
assert abs((max(xs) - min(xs)) - width) < 1e-6
# 4. 寸法: 追加距離の延長 100 m と寸法の測定値が一致する
dim = msp.query("DIMENSION").first
assert abs(dim.get_measurement() - float(rows[-1]["distance_m"])) < 1e-6
print("OK", len(rows), "stations,", len(msp), "entities")読み込みにrecover.readfileを使っている理由は、構造に傷のあるDXFでも読める点にあります。recoverは、通常のezdxf.readfile()では読み込めない構造的欠陥のあるASCII DXFを「復旧」するモジュールです。recover.readfile()は可能な限り欠陥を修復し、監査(audit)を自動で走らせて、その結果をauditorとして返します。auditor.has_errorsでエラーの有無を確かめられます。
エンティティの取り出しには、msp.query('LWPOLYLINE[layer=="DESIGN"]')のように、エンティティ名と属性条件を組み合わせた問い合わせ文字列を使っています。ドキュメントにもLINE[layer=="construction"]の例があります。レイヤ名で絞れるので、「計画高の線だけ取り出して頂点を見る」が1行で書けます。
実行すると、次の1行が出ます。
OK 6 stations, 11 entities11は、折れ線3本(地盤・計画・横断)、基準線1本、測点ラベル6個、寸法1個の合計です。
検証が実際に捕まえた2つの失敗
検証スクリプトが働くかどうかは、わざと壊して確かめます。手元で2通りの壊し方を試しました。
1つ目は、縦倍率の掛け忘れです。make_dxf.pyのprofile関数から* V_EXAGを外して書き出すと、DXFはエラーなく保存されます。読み戻しのチェックでは、次のように止まります。
AssertionError: ('GROUND', {'station': 'No.0', 'distance_m': '0',
'ground_m': '10.20', 'design_m': '10.50'})どの層のどの測点で座標が合わないかが、表の行ごと出ます。図面を開いて「縦断が少し平べったいかな」と感じる程度のずれは、人間の目では見逃しやすい種類の誤りです。
2つ目は、レイヤの定義漏れです。LAYERSから"DESIGN"を消して書き出したところ、スクリプトも保存もエラーになりませんでした。図形のlayer属性にはDESIGNが入ったままで、レイヤ表にはDESIGNが存在しません。検証では次のように止まります。
AssertionError: 未定義レイヤ: {'DESIGN'}「図形が使うレイヤ」と「定義されたレイヤ」の集合を比べる1行(used <= defined)が効いた形です。レイヤ名の綴りゆれ、たとえばDESIGNとDESIGNNの混在も、同じ検査で拾えます。綴りゆれは、図面を開く前のDXFの時点で潰せます。
Claude Codeに渡す指示とCLAUDE.md
反復を任せるには、手順を毎回プロンプトに書くより、CLAUDE.mdに規約として置くほうが安定します。たとえば次の断片です。
## DXF生成の規約
- 元データは stations.csv と cross.csv。スクリプトに数値を埋め込まない
- 生成は `.venv/bin/python make_dxf.py`、検証は `.venv/bin/python verify_dxf.py`
- DXFを書き出したら、必ず verify_dxf.py を実行して OK の行を確認してから報告する
- 縦倍率・基準高・レイヤ名は config.py だけに置く。make_dxf.py と verify_dxf.py はそこから import し、値を直書きしない
- レイヤを追加するときは config.py の LAYERS だけを更新するそのうえで、依頼は結果の条件から書きます。
stations.csv と cross.csv から縦断図と横断図の DXF を作って。
作り終えたら verify_dxf.py を実行し、失敗したら原因を直して再実行。
OK が出たら、レイヤ一覧と各折れ線の頂点数を報告して。こうしておくと、Claude Codeはスクリプトの生成、実行、失敗の読み取り、修正という反復を自分で回します。前節の2つの失敗は、人が図面を開く前に反復の内側で拾われます。CLAUDE.mdのconfig.pyに関する2行は、片側だけ直して対になる側を残す事故を防ぐためのものです。縦倍率は生成側と検証側の両方に現れるため、定数の置き場所が割れていると、検証が生成の誤りを追認する形になります。
寸法の測定値と単位で気をつけること
寸法(DIMENSION)は、検証のうえで扱いがやや特殊です。ドキュメントによると、AutoCADは寸法エンティティだけでは寸法線を表示せず、LINEやTEXTなどのプリミティブで作った匿名ブロック(寸法線のレンダリング)も必要とします。render()はそのブロックを作る呼び出しです。同じページには、ezdxfはDIMSTYLEの変数をすべて考慮するわけではないこと、レンダリング結果はCADアプリと一致するとは限らないこと、テキストの実幅はMatplotlibが入っている場合にだけ得られることが書かれています。
つまり、get_measurement()で確かめられるのは「寸法エンティティが測っている長さ」で、CADソフトで開いたときの見た目の位置や文字の収まりではありません。今回の検証は前者を保証します。後者は、CADで開くか、ezdxfのdrawingアドオンでPNGやSVGに変換して目視する別の工程になります。drawingアドオンはDXFを可視化してPNG・PDF・SVGに保存できる、とドキュメントに説明があります。
単位にも注意が必要です。ドキュメントは、DXFの長さや座標の値は、そのままでは単位を持たないと説明しています。単位はヘッダ変数$INSUNITSなどの文脈で決まり、ブロックを異なる単位のモデル空間に挿入するときの換算も自動では行われません。この記事の縦断図は縦だけを10倍にしているため、CAD上で図形を測ると縦の寸法は実際の高低差の10倍になります。縦倍率を使った図面では、図枠や注記に倍率を書いておく運用が前提になります。
つまずきやすい点
- 保存は通るのに、図が空に見える: レイヤが未定義、またはレイヤの色や表示状態が意図と違う場合があります。
used <= definedの検査と、レイヤ一覧の出力を先に見ます。 - 縦断図の線が横断図に重なる: 同じモデル空間に置くため、原点をずらさないと重なります。この記事では横断図の
OYを−40にして縦断図の下に離しました。 - 検証側が生成側の誤りを写してしまう: 縦倍率や基準高の定数を、両方のスクリプトで別々に持つと起きます。この記事のように
config.pyへ1か所で持たせ、両方のスクリプトから読み込みます。 - DXFバージョンの差: 寸法の公差表示(限界寸法)にはMTEXTの機能を使うため、R12のDXFでは表示されないとドキュメントにあります。R12を出力形式にする案件では、寸法の仕様を先に確かめます。
数値の検証と目視の確認の分担
この反復で機械的に確かめられるのは、レイヤの定義、頂点の座標、断面の幅、寸法の測定値です。確かめられないのは、文字が重なっていないか、寸法線が読みやすい位置にあるか、図枠に収まっているかといった見た目の話です。数値の検証で落とし、見た目は最後にdrawingアドオンの画像か、CADソフトで開いて目で見る、という分担が現実的です。同じ分担は立体でも使えます。画像中心の反復はOpenSCADのPNG確認、建築のIFCモデルならifcopenshellで部屋面積を面積表と突き合わせる手順が同じ考え方です。
まとめ
縦断図・横断図のDXF生成は、元データのCSV、生成スクリプト、読み戻しの検証スクリプトの3点を揃え、Claude Codeに「検証がOKになるまで回す」ことを求めると形になります。書き出しの成否よりも、表から再計算した頂点座標と、図形が使うレイヤの定義の2点を検査に含める価値が大きいです。縦倍率の掛け忘れも、レイヤの定義漏れも、保存時にはエラーにならず、読み戻しで初めて止まりました。見た目の確認は、検証とは別の工程として最後に置きます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。