DXFのハンドルと参照関係を読む|コード5・330・360と所有構造

「DXFのハンドルと参照関係を読む|コード5・330・360と所有構造」の内容を表す技術イラスト

DXFのLINEやCIRCLEを読むだけなら、座標、半径、レイヤーなどを取得すれば形状を再現できます。

しかし、ブロック、寸法、ポリライン、辞書、拡張データまで正しく扱うには、各要素を単独のデータとして読むだけでは不十分です。

DXF内部では、図形や非表示オブジェクトがハンドルによって相互に参照されています。

エンティティ
    ↓ ハンドル参照
BLOCK_RECORD
    ↓
ブロック定義

DIMENSION
    ↓
寸法スタイル・表示ブロック

POLYLINE
    ↓
VERTEX・SEQEND

ハンドルを無視しても、単純な線や円は表示できるかもしれません。しかし、データ同士の所属関係や依存関係が失われるため、複雑なDXFでは構造を正しく復元できません。

この記事では、DXFのハンドルを「識別子」と「参照関係」に分け、コード5、105、320〜369、1005の意味とPythonでの構造化方法を整理します。

目次

DXFのハンドルとは

ハンドルは、DXF内のオブジェクトを識別するための値です。

AutodeskのDXF Referenceでは、データベースオブジェクトのハンドルについて、

  • 同じ図面ファイル内で一意
  • 図面が存続する間は一定
  • 16進数の文字列として記録

という性質が示されています。

たとえば、次のエンティティを考えます。

0
LINE
5
2A
100
AcDbEntity
8
OUTLINE

コード5に続く2Aが、このLINEのハンドルです。2A16​=4210​

と数値へ変換できますが、DXFを解析するときは通常、"2A"のような文字列として扱う方が安全です。

ハンドルは長さ、個数、作成順を表す値ではありません。計算に使う数値ではなく、データを結び付けるIDです。

コード5はオブジェクト自身のハンドル

多くのエンティティやオブジェクトでは、グループコード5が自身のハンドルを表します。

0
CIRCLE
5
3F
100
AcDbEntity
8
0

この場合、CIRCLEのハンドルは3Fです。

内部データへ変換するなら、次のように保持できます。

{
  "type": "CIRCLE",
  "handle": "3F",
  "layer": "0"
}

図面全体を読み取るときは、ハンドルをキーとする索引を作成すると便利です。

objects_by_handle = {
    "2A": line_entity,
    "3F": circle_entity,
    "A1": block_record,
}

この索引があれば、別のエンティティに3Fへの参照が記録されていた場合、対象のCIRCLEを直接取得できます。

DIMSTYLEではコード105を使う

コード5には例外があります。

DIMSTYLEテーブルレコードでは、自身のハンドルにコード105を使用します。AutodeskのDXF Referenceでは、DIMSTYLEだけがこの例外に該当すると説明されています。

0
DIMSTYLE
105
27
2
ISO-25

このDIMSTYLEのハンドルは27です。

したがって、DXFパーサーで、

handle = entity.get(5)

だけを実行すると、DIMSTYLEのハンドルを取得できません。

エンティティ型を確認し、

if entity_type == "DIMSTYLE":
    handle_code = 105
else:
    handle_code = 5

のように処理を分ける必要があります。

自身のハンドルと参照ハンドルは違う

コード5または105は、基本的に「このオブジェクト自身は誰か」を表します。

一方、コード320〜369は、別のオブジェクトを指すための参照ハンドルです。

コード範囲参照の種類
320〜329arbitrary handle
330〜339soft-pointer handle
340〜349hard-pointer handle
350〜359soft-owner handle
360〜369hard-owner handle

つまり、

5
2A

と、

330
1F

では役割が異なります。

5   → 自分の識別子
330 → 別オブジェクトへの参照

この違いをデータモデルで分離しておくことが重要です。

コード330は所有先などへの参照に現れる

一般的な図形エンティティでは、コード330が、そのエンティティを収容するBLOCK_RECORDへの参照として現れます。

0
LINE
5
2A
330
1F
100
AcDbEntity
8
OUTLINE

この例では、

  • LINE自身のハンドル:2A
  • 参照先ハンドル:1F

です。

1Fをハンドル索引で検索すると、対応するBLOCK_RECORDを取得できます。

LINE 2A
   └── 330 → BLOCK_RECORD 1F

ただし、コード330の詳細な意味はエンティティ型や記録位置によって確認する必要があります。

「330なら常に同じ所有関係」と決め打ちするのではなく、対象エンティティのDXF仕様と組み合わせて解釈します。

ポインタと所有関係の違い

Autodeskの仕様では、オブジェクト間参照を大きくpointerとownershipに分けています。

pointerは、別のオブジェクトを利用・参照している関係です。

参照元は参照先を使用しますが、そのオブジェクトを所有しているとは限りません。一つのオブジェクトに対して複数のpointerが存在できます。

ownershipは、あるオブジェクトが別のオブジェクトへ責任を持つ関係です。

仕様上、オブジェクトは複数のpointerを持てますが、ownerは一つだけです。

概念的には次の違いです。

pointer
A ──参照──→ B
C ──参照──→ B

ownership
Owner A
   └── owns → Child B

独自パーサーでは、どちらも単なる文字列参照として保存するのではなく、関係の種類を保持する方が後工程で扱いやすくなります。

hardとsoftの違い

pointerとownershipには、それぞれhardとsoftがあります。

AutodeskのDXF Referenceでは、hard参照は参照先オブジェクトがPURGEによって除去されることを防ぎ、soft参照は防がないと説明されています。

参照概要
soft pointer参照するが、PURGEから保護しない
hard pointer参照し、PURGEから保護する
soft owner所有関係を表すが、PURGEから保護しない
hard owner所有関係を表し、PURGEから保護する

ここでいうhardとsoftは、「参照が必須か任意か」という一般的なプログラミング用語だけで判断しない方が安全です。

DXFでは、PURGE時の保持動作に関係する明確な意味を持ちます。

Autodeskは例として、ブロック定義や複合エンティティが構成要素のhard ownerになること、シンボルテーブルや辞書が要素のsoft ownerになることを示しています。

また、旧形式のPOLYLINEはVERTEXとSEQENDを、属性付きINSERTはATTRIBとSEQENDをhard ownershipの構造で管理します。

コード360と拡張辞書

コード360はhard-owner handleの範囲に含まれます。

代表例の一つが、オブジェクトに関連付けられた拡張辞書です。

102
{ACAD_XDICTIONARY
360
A5
102
}

この場合、A5は拡張辞書へのhard-owner参照です。

コード102はアプリケーション定義グループの境界を表すため、360だけを抜き出すのではなく、どのグループ内に現れたかも保持すると安全です。

{
  "code": 360,
  "handle": "A5",
  "reference_type": "hard_owner",
  "context": "ACAD_XDICTIONARY"
}

同じグループコードでも、周囲のサブクラスやグループ構造によって具体的な役割が決まります。

コピー時に参照先が変換される

330〜369の参照ハンドルは、INSERT、XREF、WBLOCKなどでオブジェクト群が別の図面へ移されるとき、コピー後のオブジェクトを指すように変換されます。

たとえば、元図面で次の関係があるとします。

A ──参照──→ B

AとBを別図面へコピーし、新しいハンドルがそれぞれA2とB2になった場合、参照も次のように更新されます。

A2 ──参照──→ B2

一方、320〜329のarbitrary handleは、この変換対象になりません。

arbitrary handleは値をそのまま保持する用途であり、外部DXFやDWG内のオブジェクトを示す場合などに利用できます。

したがって独自のDXF変換処理で、すべてのハンドル文字列を一括置換してはいけません。

XDATAのコード1005

拡張データでエンティティハンドルを保持するときは、コード1005が使用されます。

1001
MY_APPLICATION
1005
2A

Autodeskの仕様では、1005の参照はsoft pointerと同様の動作をします。

つまり、対象オブジェクト群が別の図面へ統合される場合、関係するハンドルは変換されます。

ただし、1005は通常の330〜369とは異なり、XDATA内に存在します。解析時には、

  • 登録アプリケーション名を表す1001
  • その後に続くXDATA
  • 1005の参照先

をひとまとまりとして保存します。

Pythonでハンドルを正規化する

ハンドルを比較するときは、大文字へ統一し、16進文字列として検証すると扱いやすくなります。

import re


HANDLE_PATTERN = re.compile(r"^[0-9A-F]{1,16}$")


def normalize_handle(value: str) -> str:
    handle = value.strip().upper()

    if not HANDLE_PATTERN.fullmatch(handle):
        raise ValueError(
            f"invalid DXF handle: {value!r}"
        )

    return handle

参照コードの種類は、次のように分類できます。

def reference_type(code: int) -> str | None:
    if 320 <= code <= 329:
        return "arbitrary"
    if 330 <= code <= 339:
        return "soft_pointer"
    if 340 <= code <= 349:
        return "hard_pointer"
    if 350 <= code <= 359:
        return "soft_owner"
    if 360 <= code <= 369:
        return "hard_owner"
    if code == 1005:
        return "xdata_soft_pointer"

    return None

エンティティ内に同じグループコードが複数回現れる場合があるため、単純な辞書へ上書きせず、タグ列として保持します。

def extract_references(
    tags: list[tuple[int, str]],
) -> list[dict[str, object]]:
    references = []

    for code, value in tags:
        ref_type = reference_type(code)

        if ref_type is None:
            continue

        references.append({
            "code": code,
            "handle": normalize_handle(value),
            "type": ref_type,
        })

    return references

参照グラフを構築する

図面全体を解析するなら、最初に全オブジェクトを読み、ハンドル索引を作成します。

def build_handle_index(
    objects: list[dict],
) -> dict[str, dict]:
    index = {}

    for obj in objects:
        handle = obj.get("handle")

        if handle is None:
            continue

        handle = normalize_handle(handle)

        if handle in index:
            raise ValueError(
                f"duplicate handle: {handle}"
            )

        index[handle] = obj

    return index

その後、各参照を索引へ解決します。

def resolve_references(
    objects: list[dict],
    index: dict[str, dict],
) -> list[dict]:
    unresolved = []

    for obj in objects:
        for ref in obj.get("references", []):
            target = index.get(ref["handle"])

            if target is None:
                unresolved.append({
                    "source": obj.get("handle"),
                    "target": ref["handle"],
                    "type": ref["type"],
                })
            else:
                ref["target"] = target

    return unresolved

これにより、DXFを単なるエンティティ配列ではなく、参照グラフとして扱えます。

DXFタグ列
    ↓
オブジェクト抽出
    ↓
ハンドル索引
    ↓
参照エッジ生成
    ↓
所有構造・依存関係の検査

検出できるデータ不整合

ハンドルと参照を構造化すると、次のような検査が可能になります。

  • 同じハンドルを持つオブジェクトが複数存在する
  • 参照先ハンドルが図面内に存在しない
  • 必要なownerを解決できない
  • 所有関係が循環している
  • 一つの子オブジェクトに複数のowner候補がある
  • コピー後も旧図面のハンドルを参照している
  • DIMSTYLEのコード105を読み落としている
  • XDATAの1005を通常文字列として無視している

ただし、参照切れを発見しただけで、自動削除や任意の参照先への付け替えを行うのは危険です。

まず、

問題の位置
参照元
参照先ハンドル
参照形式
エンティティ型

を診断情報として出力し、修復規則を別工程にします。

実装で起こりやすい失敗

ハンドルを10進数の連番として扱う。
ハンドルは16進文字列です。数値へ変換できても、順序や欠番に意味があるとは限りません。

コード5だけを読んでDIMSTYLEを落とす。
DIMSTYLEの自己ハンドルはコード105です。

330を見つけたら形状データとして保存する。
330は参照ハンドルの範囲です。座標や寸法値とは分けて保存します。

すべての参照ハンドルを同じ種類として扱う。
pointerとownership、hardとsoftでは意味が異なります。

320〜369を一括して書き換える。
320〜329のarbitrary handleは、INSERTやXREFで変換されない参照です。

辞書形式へ変換して重複コードを上書きする。
DXFでは同じコードが複数回現れることがあります。Raw層では順序付きタグ列を保持します。

まとめ

DXFのハンドルは、図面内のオブジェクトを結び付ける識別子です。

基本となるコードは次のとおりです。

5       → 通常の自己ハンドル
105     → DIMSTYLEの自己ハンドル
320–329 → arbitrary handle
330–339 → soft pointer
340–349 → hard pointer
350–359 → soft owner
360–369 → hard owner
1005    → XDATA内のハンドル

pointerは別オブジェクトの利用・参照を表し、ownershipは親が子へ責任を持つ関係を表します。

hard参照は参照先をPURGEから保護しますが、soft参照は保護しません。

DXFパーサーでは、

自己ハンドル
参照先ハンドル
参照の種類
出現した構造

を分離して保存する必要があります。

ハンドルを解析できるようになると、個別図形を読むだけのパーサーから、ブロック、寸法、辞書、複合エンティティを含む図面全体の依存関係を復元できるパーサーへ発展させられます。

参考情報

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

この記事を書いた人

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

目次