DXFでは、線や円の座標が常に「画面で見えている向き」の数値として保存されるとは限りません。図面には世界座標系のWCSだけでなく、作業面を定義するUCS(User Coordinate System)も保存できます。
UCSを読み落とすと、名前付き作業座標系の原点や軸方向を復元できません。一方、UCSをすべてのエンティティ座標へ無条件に適用すると、すでにWCSやOCSで表現されている座標を二重変換する危険があります。
この記事では、AutodeskのDXF仕様を基準に、TABLESセクションのUCSテーブル、原点とX軸・Y軸、直交UCS情報、WCSとの座標変換、Pythonでの検証方法を整理します。
UCSテーブルは名前付き作業座標系を保存する
DXFのTABLESセクションには、レイヤー、線種、文字スタイルなどと並んでUCSテーブルがあります。各UCSレコードは、CAD上で保存された名前付きユーザー座標系を表します。
UCSレコードの中心情報は次の3点です。
- 原点をWCSで表した3次元点
- UCSのX軸方向をWCSで表した3次元ベクトル
- UCSのY軸方向をWCSで表した3次元ベクトル
つまり、UCSは「ローカル座標値の一覧」ではなく、ローカル座標系をWCS空間へ配置するための基準です。
UCSレコードの主要グループコード
AutodeskのDXF Referenceで定義される主要コードは次のとおりです。
| グループコード | 内容 |
|---|---|
| 0 | レコード型UCS |
| 2 | UCS名 |
| 70 | 標準フラグ |
| 10・20・30 | 原点のX・Y・Z値(WCS) |
| 11・21・31 | X軸方向のX・Y・Z成分(WCS) |
| 12・22・32 | Y軸方向のX・Y・Z成分(WCS) |
| 79 | 常に0 |
| 146 | 標高 |
| 346 | 直交UCSの基準UCSハンドル |
| 71 | 直交方向の種類 |
| 13・23・33 | 直交方向ごとの相対原点 |
コード10、11、12だけを読むのではなく、それぞれに続くY成分とZ成分を組み合わせて3次元値にします。
10 → 原点X 20 → 原点Y 30 → 原点Z
11 → X軸X 21 → X軸Y 31 → X軸Z
12 → Y軸X 22 → Y軸Y 32 → Y軸Z
コード70はUCS固有の幾何情報ではなく、外部参照への依存や参照済み状態などを示す標準フラグです。座標変換だけが目的でも、元の値を失わず保持しておくと再出力や診断に利用できます。
DXF上のUCS例を読む
次の例は、原点をWCSの(100, 200, 50)に置き、UCSのX軸をWCSの正Y方向、UCSのY軸をWCSの負X方向へ向けた名前付きUCSです。
0
UCS
5
2A
100
AcDbSymbolTableRecord
100
AcDbUCSTableRecord
2
MACHINE_TOP
70
0
10
100.0
20
200.0
30
50.0
11
0.0
21
1.0
31
0.0
12
-1.0
22
0.0
32
0.0
79
0
146
0.0
構造化すると次のようになります。
{
"name": "MACHINE_TOP",
"origin_wcs": [100.0, 200.0, 50.0],
"x_axis_wcs": [0.0, 1.0, 0.0],
"y_axis_wcs": [-1.0, 0.0, 0.0],
"elevation": 0.0
}
ここで、コード11と12の値は点ではなく方向ベクトルです。原点の平行移動成分を加えず、向きとして扱います。
WCSとUCSを混同しない
WCSは図面全体の固定基準です。UCSはWCS内に配置された作業用の座標系です。さらに、多くのDXFエンティティはOCS(Object Coordinate System)を使って、自身の押し出し方向に基づく平面座標を保存します。
| 座標系 | 主な役割 | 基準 |
|---|---|---|
| WCS | 図面全体の固定座標 | グローバル |
| UCS | 利用者が定義する作業座標 | WCS内の原点と軸 |
| OCS | エンティティ固有の平面座標 | 押し出し方向など |
UCSテーブルが存在しても、すべてのENTITIES座標がそのUCS値で保存されているとは限りません。各エンティティのDXF仕様を確認し、WCS、OCS、その他の座標表現のどれかを判定します。
UCSレコードは、保存された作業座標系の定義として独立して解析するのが安全です。
UCS座標からWCS座標へ変換する
UCS原点を $\mathbf{o}$、単位X軸を $\mathbf{e}_x$、単位Y軸を $\mathbf{e}_y$ とします。右手系のZ軸は外積から求めます。
$$ \mathbf{e}_z = \mathbf{e}_x \times \mathbf{e}_y $$
UCS座標が $(u,v,w)$ の点をWCSへ変換する式は次のとおりです。
$$ \mathbf{p}_{WCS} = \mathbf{o} + u\mathbf{e}_x + v\mathbf{e}_y + w\mathbf{e}_z $$
前の例で、UCS座標を(20, 10, 5)とすると、
$$ \mathbf{o}=(100,200,50) $$
$$ \mathbf{e}_x=(0,1,0),\quad \mathbf{e}_y=(-1,0,0),\quad \mathbf{e}_z=(0,0,1) $$
したがって、
$$ \mathbf{p}_{WCS}=(100,200,50)+20(0,1,0)+10(-1,0,0)+5(0,0,1) $$
変換結果はWCSの(90, 220, 55)です。
WCS座標からUCS座標へ戻す
3本の軸が単位長で互いに直交している場合、回転行列を次のように定義できます。
$$ R=\begin{bmatrix}\mathbf{e}_x & \mathbf{e}_y & \mathbf{e}_z\end{bmatrix} $$
直交行列では逆行列が転置行列に等しいため、WCS点からUCS座標への変換は次式になります。
$$ \begin{bmatrix}u\\v\\w\end{bmatrix}=R^T(\mathbf{p}_{WCS}-\mathbf{o}) $$
ただし、入力軸が正規化されていない、X軸とY軸が直交していない、または外積がゼロに近い場合、この前提は成立しません。変換前に軸を検証し、不正なデータを黙って補正しないことが重要です。
直交UCSの追加情報を扱う
UCSレコードでは、コード79は常に0と定義されています。直交方向に関する追加情報は、コード71とコード13・23・33の組で表されます。
コード71の値は次の方向を示します。
| 値 | 直交方向 |
|---|---|
| 1 | Top |
| 2 | Bottom |
| 3 | Front |
| 4 | Back |
| 5 | Left |
| 6 | Right |
一つのUCSレコードに複数のコード71と原点の組が現れる可能性があります。単純な辞書へ上書きせず、出現順を保つ配列として読み取ります。
コード346は、直交UCSの基準となるUCSレコードのハンドルです。仕様上、該当する参照が省略され、直交状態が示されている場合はWORLDが基準とみなされます。ハンドル索引を使って参照を解決し、未解決なら診断情報を残します。
PythonでUCSを検証して変換する
次の例は、UCSの軸が単位長かつ直交していることを確認し、UCS座標をWCS座標へ変換します。外部ライブラリを使わず、計算境界が分かる形にしています。
from dataclasses import dataclass
from math import isclose, sqrt
Vector3 = tuple[float, float, float]
def dot(a: Vector3, b: Vector3) -> float:
return sum(x * y for x, y in zip(a, b))
def cross(a: Vector3, b: Vector3) -> Vector3:
return (
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
)
def norm(v: Vector3) -> float:
return sqrt(dot(v, v))
@dataclass(frozen=True)
class UCS:
name: str
origin: Vector3
x_axis: Vector3
y_axis: Vector3
def validated_axes(self, tol: float = 1e-9) -> tuple[Vector3, Vector3, Vector3]:
if not isclose(norm(self.x_axis), 1.0, abs_tol=tol):
raise ValueError("x_axis is not a unit vector")
if not isclose(norm(self.y_axis), 1.0, abs_tol=tol):
raise ValueError("y_axis is not a unit vector")
if not isclose(dot(self.x_axis, self.y_axis), 0.0, abs_tol=tol):
raise ValueError("x_axis and y_axis are not orthogonal")
z_axis = cross(self.x_axis, self.y_axis)
if not isclose(norm(z_axis), 1.0, abs_tol=tol):
raise ValueError("UCS axes do not form a valid basis")
return self.x_axis, self.y_axis, z_axis
def to_wcs(self, point: Vector3) -> Vector3:
x_axis, y_axis, z_axis = self.validated_axes()
u, v, w = point
return tuple(
self.origin[i]
+ u * x_axis[i]
+ v * y_axis[i]
+ w * z_axis[i]
for i in range(3)
)
machine_top = UCS(
name="MACHINE_TOP",
origin=(100.0, 200.0, 50.0),
x_axis=(0.0, 1.0, 0.0),
y_axis=(-1.0, 0.0, 0.0),
)
print(machine_top.to_wcs((20.0, 10.0, 5.0)))
出力は次のとおりです。
(90.0, 220.0, 55.0)
実務では、許容差を図面の精度や生成元CADに合わせて決めます。わずかな丸め誤差を許容する処理と、明らかに不正な軸を拒否する処理を分けると診断しやすくなります。
パーサーで保持したいデータ
UCSを将来の変換処理やWebツールへ利用するなら、元タグと解釈済みデータを分けて保存します。
{
"name": "MACHINE_TOP",
"handle": "2A",
"flags": 0,
"origin_wcs": [100.0, 200.0, 50.0],
"x_axis_wcs": [0.0, 1.0, 0.0],
"y_axis_wcs": [-1.0, 0.0, 0.0],
"elevation": 0.0,
"base_ucs_handle": null,
"orthographic_origins": [],
"validation": {
"unit_axes": true,
"orthogonal": true,
"right_handed": true
}
}
Rawタグを残しておけば、未知のコードや同一コードの繰り返しを失わず、後から仕様解釈を更新できます。
実装で起こりやすい失敗
UCS原点だけを読み、軸方向を無視する。 平行移動しか復元できず、回転した作業座標系を正しく扱えません。
コード11と12を座標点として扱う。 これらはWCSで表した方向ベクトルです。原点を加える対象ではありません。
X軸とY軸からZ軸を作らない。 3次元変換では、右手系のZ軸を外積から構成する必要があります。
軸を検証せず転置行列を逆行列として使う。 軸が正規直交でなければ、転置による逆変換は成立しません。
UCSテーブルを現在の表示状態と決めつける。 UCSテーブルは名前付き定義です。現在のモデル空間・ペーパー空間UCSやビューに関連付くUCS情報は、HEADER変数やVIEWなど別の場所も確認します。
すべてのエンティティ座標へUCS変換を適用する。 エンティティごとの座標仕様を確認せず変換すると、OCSやWCSの値を二重に回転させる可能性があります。
コード71の繰り返しを上書きする。 直交方向ごとの原点は複数保持できるため、順序付きの組として保存します。
まとめ
DXFのUCSレコードは、名前付き作業座標系をWCS内へ配置するための定義です。中心となる情報は次の3つです。
- コード10・20・30:原点
- コード11・21・31:X軸方向
- コード12・22・32:Y軸方向
Z軸はX軸とY軸の外積から構成し、UCS点からWCS点への変換は次式で表せます。
$$ \mathbf{p}_{WCS} = \mathbf{o} + u\mathbf{e}_x + v\mathbf{e}_y + w(\mathbf{e}_x \times \mathbf{e}_y) $$
解析では、原点と方向ベクトルを区別し、軸の長さ・直交性・右手系を検証します。また、UCS定義の存在だけを理由に、全エンティティへ同じ変換を適用してはいけません。
UCSテーブルを構造化できれば、保存作業面の再現、ローカル座標での寸法・位置計算、座標変換API、CADデータ検査ツールへ展開できます。
