「DXFの3DSOLID・BODY・REGIONを読む|ACIS・SAT・SABの保存構造」の内容を表す技術イラスト
UNIT 03CAD・3Dプリント FIG.00 · EYECATCH

DXFの3DSOLID・BODY・REGIONを読む|ACIS・SAT・SABの保存構造

DXFの3DSOLID・BODY・REGIONに埋め込まれるACISデータを、SAT/SABとDXFバージョンの違いから整理。Pythonで保存形式・ハンドル・データ量を棚卸しし、安全な変換と検証手順を解説します。

DOC NO.EKP-03-1142
DATE
REVB
READ約 18 分

3DSOLIDは頂点座標の集まりではない

DXFで3次元形状を調べるとき、3DFACEやMESHは頂点と面の情報を追いやすい一方、3DSOLIDは同じ方法では読めません。3DSOLID、BODY、REGIONなどの形状本体は、ACIS系のモデルデータとして格納されるためです。

ここで重要なのは、DXFのエンティティを発見できることと、その形状を幾何学的に解釈できることを分けることです。

  • エンティティ種別、画層、ハンドル、保存形式は比較的容易に調査できる
  • 体積、面、稜線、穴、曲面を復元するにはACISデータの解釈が必要になる
  • DXFバージョンによって、文字列のSATとバイナリのSABで保存場所や扱いが変わる

この記事では、3次元ソリッドを無理に自作パーサーで復元するのではなく、安全に所在と保存形式を棚卸しし、後工程へ渡す方法を整理します。

目次

3DSOLID・BODY・REGIONの位置づけ

三つともACISベースのデータを持ちますが、CAD上での用途は同じではありません。

エンティティ実務上の見方確認したいこと
3DSOLID体積を持つ3次元ソリッド部品形状、穴、曲面、履歴参照
BODYモデラーが扱う一般的なボディソリッド、シェルなど内部構造の有無
REGION閉じた平面領域押し出し前断面、面積形状、ブーリアン結果

名前が似ているSOLIDは別物です。DXFのSOLIDは塗りつぶされた三角形または四角形であり、3DSOLIDのような境界表現の立体ではありません。3DFACEも基本的には一枚の三角形または四角形です。

したがって、次のような単純な読み替えはできません。

3DSOLIDのグループコードを順に読めば、頂点配列と面配列が直接得られる。

実際には、エンティティ外側のDXF属性と、ACISデータ内部の位相・幾何を別の層として扱う必要があります。

DXF外側とACIS内側を分けて考える

3DSOLIDには、一般的なDXFエンティティ属性に加え、モデラー形式の情報と独自データが続きます。AutodeskのDXFリファレンスでは、主なコードとして次が示されています。

グループコード内容
100AcDbModelerGeometryなどのサブクラス識別子
70モデラー形式のバージョン番号
1独自データの文字列
3コード1に続く追加文字列
3503DSOLIDの履歴オブジェクトへのソフトオーナー参照

コード1と3は、X座標や半径のような単純な数値属性ではありません。文字列を結合した先に、形状カーネルが扱うデータがあります。また、コード350は編集履歴への参照であり、形状本体そのものではありません。

この二層構造を処理の流れにすると、次のようになります。

  1. DXFを開く
  2. 3DSOLID・BODY・REGIONを抽出する
  3. 保存形式を判定する
  4. SATなら文字列データ、SABならバイナリデータを取得する
  5. 対応ライブラリまたはCADへ渡す
  6. 面・稜線・体積を検証する

SATとSABはDXFバージョンで切り替わる

ezdxfの公式ドキュメントでは、DXF R2000からR2010まではSAT、R2013以降はSABとして扱われます。

SATは文字列形式

SATはテキストベースです。古いDXFでは、ACISデータがエンティティ内のコード1と3に分割されて保存されます。ここでの改行や分割位置は、幾何学的な境界とは限りません。生DXFを扱う場合は、値を途中で正規化したり、空白を勝手に削除したりせず、元の順序を保つ必要があります。

SABはバイナリ形式

R2013以降では、SABのバイナリデータが使われます。このデータはACDSDATAセクションに格納されるため、3DSOLIDエンティティだけを抜き出しても、必要なバイナリデータがそろわない場合があります。

DXFを部分抽出するプログラムでは、次の失敗が起きやすくなります。

  • ENTITIESセクションだけを残してACDSDATAを落とす
  • 文字列前提でSABをデコードし、内容を壊す
  • DXFバージョンを変更しただけで、SATとSABが自動変換されたと考える
  • エンティティのハンドルを変更し、関連データとの対応を壊す

拡張子が同じ.dxfでも、保存バージョンを必ず確認する理由がここにあります。

PythonでACISエンティティを棚卸しする

形状変換へ進む前に、ファイル内の対象エンティティを一覧化します。次のコードはezdxfを使い、エンティティ種別、ハンドル、画層、SAT/SAB、データ量、履歴ハンドルをJSONへ出力します。入力ファイルのSHA-256と、使用したezdxfのバージョンも記録します。

対象はモデル空間だけでなく、エンティティデータベース内の3DSOLID・BODY・REGIONです。ブロック定義内のエンティティも含むため、件数はINSERTによる配置数や画面上に見えるソリッド数とは一致しません。

Python 3.10以降とezdxfを使用します。まず、ezdxfをインストールしてください。

python -m pip install ezdxf

次のコードをinspect_acis.pyとして保存します。

from __future__ import annotations

import argparse
import hashlib
import json
import sys
from collections import Counter
from collections.abc import Sequence
from pathlib import Path
from typing import Any

import ezdxf


TARGET_TYPES = {"3DSOLID", "BODY", "REGION"}


def sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as stream:
        for chunk in iter(lambda: stream.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def acis_size(data: bytes | bytearray | Sequence[str]) -> int:
    """SABは実バイト数、SATは改行を除くUTF-8換算バイト数。"""
    if isinstance(data, (bytes, bytearray)):
        return len(data)
    return sum(len(line.encode("utf-8")) for line in data)


def inspect_acis_entities(dxf_path: Path) -> dict[str, Any]:
    doc = ezdxf.readfile(dxf_path)
    entities: list[dict[str, Any]] = []

    for entity in doc.entitydb.values():
        if not entity.is_alive:
            continue

        entity_type = entity.dxftype()
        if entity_type not in TARGET_TYPES:
            continue

        # 履歴ハンドルは3DSOLIDについてのみ取得する。
        history_handle = None
        if entity_type == "3DSOLID":
            history_handle = entity.dxf.get("history_handle", None)

        # ハンドル"0"は有効な履歴オブジェクトへの参照として数えない。
        has_history_reference = history_handle not in (None, "", "0")

        storage = "unknown"
        data_bytes = None
        size_basis = None
        error = None

        try:
            data = entity.acis_data

            # 空データも含め、取得した値の型から形式を記録する。
            if isinstance(data, (bytes, bytearray)):
                storage = "SAB"
                size_basis = "binary_bytes"
            else:
                storage = "SAT"
                size_basis = "utf8_bytes_excluding_line_separators"

            data_bytes = acis_size(data)
        except Exception as exc:
            error = f"{type(exc).__name__}: {exc}"

        entities.append(
            {
                "type": entity_type,
                "handle": entity.dxf.get("handle", None),
                "owner_handle": entity.dxf.get("owner", None),
                "layer": entity.dxf.get("layer", "0"),
                "storage": storage,
                "data_bytes": data_bytes,
                "data_size_basis": size_basis,
                "empty_data": (
                    data_bytes == 0 if data_bytes is not None else None
                ),
                "history_handle": history_handle,
                "has_history_reference": has_history_reference,
                "error": error,
            }
        )

    return {
        "input_file": str(dxf_path.resolve()),
        "input_sha256": sha256_file(dxf_path),
        "dxf_version": doc.dxfversion,
        "ezdxf_version": ezdxf.__version__,
        "scope": "entity_database",
        "entity_count": len(entities),
        "counts_by_type": dict(
            sorted(Counter(row["type"] for row in entities).items())
        ),
        "counts_by_storage": dict(
            sorted(Counter(row["storage"] for row in entities).items())
        ),
        "empty_data_count": sum(
            row["empty_data"] is True for row in entities
        ),
        "read_error_count": sum(
            row["error"] is not None for row in entities
        ),
        "entities": entities,
    }


def main() -> int:
    parser = argparse.ArgumentParser(
        description="DXF内のACISエンティティを棚卸ししてJSONへ出力します。"
    )
    parser.add_argument("input", type=Path, help="入力DXFファイル")
    parser.add_argument(
        "-o",
        "--output",
        type=Path,
        help="出力JSON。省略時は入力名.acis-report.json",
    )
    args = parser.parse_args()

    input_path = args.input
    output_path = (
        args.output
        if args.output is not None
        else input_path.with_suffix(".acis-report.json")
    )

    if not input_path.is_file():
        parser.error(f"入力ファイルが見つかりません: {input_path}")

    if input_path.resolve() == output_path.resolve():
        parser.error("入力ファイルと出力ファイルには別のパスを指定してください。")

    # 既存ファイルへの意図しない上書きを防ぐ。
    if output_path.exists():
        parser.error(f"出力先が既に存在します: {output_path}")

    try:
        report = inspect_acis_entities(input_path)
        payload = json.dumps(report, ensure_ascii=False, indent=2)
        with output_path.open("x", encoding="utf-8") as stream:
            stream.write(payload)
            stream.write("\n")
    except Exception as exc:
        print(
            f"処理に失敗しました: {type(exc).__name__}: {exc}",
            file=sys.stderr,
        )
        return 1

    print(f"出力先: {output_path.resolve()}")
    print(f"対象エンティティ数: {report['entity_count']}")
    print(f"空データ数: {report['empty_data_count']}")
    print(f"データ取得エラー数: {report['read_error_count']}")

    # 個別エンティティの取得エラーも終了コードで検出できるようにする。
    return 2 if report["read_error_count"] else 0


if __name__ == "__main__":
    raise SystemExit(main())

実行例は次のとおりです。

python inspect_acis.py model.dxf -o model.acis-report.json

出力先を省略すると、入力ファイルと同じ場所にmodel.acis-report.jsonを作成します。既存の出力ファイルがある場合は処理を止めるため、再実行時には別の出力名を指定してください。

data_bytesは、SABでは取得したバイナリのバイト数、SATでは各文字列をUTF-8に変換したバイト数の合計です。SATの改行やDXFタグ部分は含まないため、元のDXFファイル内での占有サイズとは異なります。計測方法はdata_size_basisに記録します。

データ取得に失敗した場合は、data_bytesとempty_dataをnullにして、空データと取得エラーを区別します。個別エンティティのエラーがあってもJSONは出力し、終了コードを2にします。正常終了は0、ファイル全体の読み込みや出力に失敗した場合は1です。

このコードの目的は、形状を編集することではありません。変換対象の数、保存形式、データ欠落の疑いを機械的に確認し、対応可能な処理系へ渡すための入口です。

棚卸し結果から判断できること

JSON化すると、図面ごとのばらつきを集計できます。

DXFバージョンと保存形式の関係

dxf_versionと各エンティティのstorageを記録すると、対象図面がどの保存世代に属するかを確認できます。ただし、ezdxfのacis_dataはDXFバージョンに応じてSATまたはSABを返すため、この結果だけで保存形式の不整合を独立に検証できるわけではありません。異常が疑われる場合は、元DXFのセクション構成、変換履歴、ライブラリの対応状況も確認します。

データ量がゼロのエンティティ

エンティティが存在しても、取得できたACISデータが空なら、その結果から形状を復元することはできません。参照先セクションの欠落、途中で行われた部分抽出、未対応形式などを疑います。ただし、データ量だけで形状の正常性は証明できません。

履歴ハンドルの有無

履歴参照は、ソリッド形状と編集履歴の関係を追う手がかりです。一方、参照がないことだけで形状が異常とは判断できません。検査項目は「必須」ではなく「観測値」として保存します。

このコードのhas_history_referenceは、有効な参照を示し得るハンドル値が設定されているかを表します。参照先オブジェクトの存在や履歴の整合性まで検証するものではありません。

形状を解析するときの現実的な選択肢

CADまたは幾何カーネルへ渡す

曲面やブーリアン演算を含むソリッドを正確に扱うなら、そのACISデータに対応するCAD、SDK、幾何カーネルを利用する方法が基本です。単にDXFタグを読めるライブラリと、ソリッドを再構築・修復・演算できるカーネルは役割が異なります。

ezdxfのACIS機能を使う

ezdxfにはezdxf.acis.api.load_dxf()という読み込み用APIがあります。ただし、対応するACIS構造は限定されており、任意のソリッドを読み込んで再出力する用途には使えません。公式ドキュメントでも、ACISカーネルの代替にはならないと説明されています。

import ezdxf
from ezdxf.acis import api as acis

doc = ezdxf.readfile("model.dxf")
modelspace = doc.modelspace()

for solid in modelspace.query("3DSOLID"):
    try:
        bodies = acis.load_dxf(solid)
        print(solid.dxf.handle, len(bodies))
    except Exception as exc:
        print(
            solid.dxf.handle,
            f"{type(exc).__name__}: {exc}",
        )

この処理でボディが返っても、元の形状が完全に復元されたことを意味しません。未対応の構造が含まれていないかを確認し、体積、境界ボックス、面数、穴の数、基準座標、単位、CADでの再表示を別途検証してください。

中間形式へ変換する

利用可能なCADでSTEPなどの中間形式へ変換してから、別のカーネルで扱う方法もあります。ただし、変換は情報を増やす処理ではありません。元データの編集履歴、固有属性、名前、色、レイヤーが維持されるかを、形状精度とは別に評価します。

自動処理で残すべき検証記録

3Dソリッド変換をバッチ化する場合、成功・失敗だけでは後から原因を追えません。最低限、次の項目を記録します。

  • 入力ファイルのハッシュ値
  • DXFバージョン
  • 対象エンティティ数と種別、集計対象の範囲
  • 各エンティティのハンドルと画層
  • SATまたはSABの判定
  • ACISデータのバイト数と計測方法
  • 使用したライブラリとバージョン
  • 変換先形式と許容誤差
  • 体積や境界ボックスなどの変換前後比較
  • エラー種別と、再試行した処理

前掲のコードは、このうち入力ファイルとACISエンティティの棚卸しを担当します。変換先形式、許容誤差、体積などの比較結果は、実際に変換を行う工程で追加してください。

元ファイルを変更せず、派生ファイルと検査レポートを別名で保存することも重要です。とくにバージョンダウン保存やSAT/SAB変換を伴う場合、入力DXFを上書きすると比較対象を失います。

よくある誤解と失敗

3DSOLIDと3DFACEは同じ3Dデータではない

3DFACEは頂点で定義される面ですが、3DSOLIDはソリッドモデラーのデータを保持します。表示が似ていても、穴、曲面、位相、体積の扱いは異なります。

コード1と3を読めば必ず形状化できるわけではない

文字列を結合できても、ACIS構造を解釈できるとは限りません。また、R2013以降のSABでは、エンティティ外のACDSDATAも関係します。タグ抽出と幾何復元を同じ成功条件にしないことが大切です。

メッシュ化は厳密なソリッド変換とは限らない

表示用メッシュが得られても、曲面精度、閉じたシェル、面の向き、微小エッジが保持されているとは限りません。体積計算や加工用途へ使う前に、目的に合った許容誤差と検査方法を定義します。

ファイルが開くこととデータが正しいことは別

CADで開けたとしても、一部のソリッドがproxy表示になっている、面が欠落している、単位が変わっている可能性があります。目視だけでなく、件数と幾何量を比較します。

まとめ

DXFの3DSOLID、BODY、REGIONは、単純な頂点列ではなくACISモデルデータを持つエンティティです。解析では、まずDXF外側の属性とACIS内側の形状を分離して考えます。

R2000からR2010ではSAT、R2013以降ではSABという違いがあり、後者ではACDSDATAセクションも考慮が必要です。最初の実装目標は、無理に形状を復元することではなく、エンティティの所在、保存形式、データ量、参照関係を再現可能なJSONへ記録することです。

その棚卸し結果を基に、対応CAD、ACIS SDK、ezdxfの対応機能、中間形式変換のどれを使うか判断します。変換後は、表示だけでなく体積、境界ボックス、面構成、単位まで検証して初めて、実務データとして利用できます。

参考情報

参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次