DXFのXLINEとRAYは、補助線や投影方向を表すための無限図形です。LINEのような終点を持たないため、そのままSVGやCanvasへ変換しても描画できません。表示・印刷・交差判定に使うには、基点と方向ベクトルからパラメトリック直線を組み立て、有限の表示範囲へ切り出す必要があります。
この記事では、XLINEとRAYのDXFグループコード、両者の違い、矩形範囲との交差計算、Python実装を一つの流れで整理します。
XLINEとRAYの違い
XLINEは基点を通り、正方向と負方向の両方へ無限に延びる構築線です。RAYは基点から正方向だけへ延びる半直線です。
基点を $\mathbf{P}_0$、方向ベクトルを $\mathbf{d}$、パラメータを $t$ とすると、どちらも次式で表せます。
$$ \mathbf{P}(t)=\mathbf{P}_0+t\mathbf{d} $$
違いは $t$ の範囲です。
- XLINE:$-\infty
- RAY:$0\leq t<\infty$
この制約をデータモデルに残しておけば、同じ交差計算を初期範囲だけ変えて共用できます。
DXFのグループコードを読む
AutodeskのDXFリファレンスでは、XLINEとRAYに同じ座標コードが定義されています。
| コード | 内容 | 座標系 |
|---|---|---|
| 100 | サブクラスマーカー | AcDbXlineまたはAcDbRay |
| 10・20・30 | 基点 | WCS |
| 11・21・31 | 単位方向ベクトル | WCS |
XLINEのコード10は「基点」、RAYでは「開始点」です。コード11は終点ではなく単位方向ベクトルです。LINEと同じ感覚でコード10と11を2点として結ぶと、長さ1の短い線に誤変換されます。
ezdxfの公式ドキュメントでは、両エンティティはDXF R2000以降で利用できるとされ、startとunit_vectorをWCSの3次元値として扱います。
方向ベクトルを検証する
仕様上、コード11・21・31は単位方向ベクトルです。しかし、外部システムから生成されたDXFでは丸め誤差や不正な値が混じる可能性があります。読み取り時はベクトル長を求めます。
$$ \lVert\mathbf{d}\rVert=\sqrt{d_x^2+d_y^2+d_z^2} $$
長さが0に近ければ方向を定義できないため、エラーとして隔離します。長さが1からわずかにずれているだけなら、用途に応じて正規化できます。
$$ \hat{\mathbf{d}}=\frac{\mathbf{d}}{\lVert\mathbf{d}\rVert} $$
ただし、元値も監査用に保存します。無条件に補正すると、生成元の異常を見落とすためです。
無限図形を表示範囲へ切り出す
2Dビューアでは、XLINEやRAYを表示矩形へクリッピングし、矩形内にある有限線分へ変換します。ここではXY平面へ投影済みで、表示範囲を次の軸平行矩形とします。
$$ x_{\min}\leq x\leq x_{\max},\qquad y_{\min}\leq y\leq y_{\max} $$
X成分は $x(t)=x_0+td_x$ です。$d_x\neq0$ なら、左右境界に達するパラメータは次の2値です。
$$ t_{x1}=\frac{x_{\min}-x_0}{d_x},\qquad t_{x2}=\frac{x_{\max}-x_0}{d_x} $$
大小を並べ替え、X範囲内に入る区間を $[t_{x\min},t_{x\max}]$ とします。Y方向も同様に計算し、両方を満たす共通区間を取ります。
$$ t_{\min}=\max(t_{x\min},t_{y\min}),\qquad t_{\max}=\min(t_{x\max},t_{y\max}) $$
$t_{\min}\leq t_{\max}$ なら交差があり、切り出した端点は $\mathbf{P}(t_{\min})$ と $\mathbf{P}(t_{\max})$ です。
XLINEとRAYで初期区間を変える
XLINEは両方向へ延びるため、計算開始時の範囲を $(-\infty,\infty)$ とします。RAYは負方向を持たないので、$[0,\infty)$ から始めます。この初期値だけで、基点より後方にある矩形をRAYの交差として誤採用することを防げます。
方向成分が0の場合は割り算を行いません。たとえば $d_x=0$ なら線はX方向へ移動しないため、$x_0$ がX範囲外なら交差なし、範囲内ならX軸から追加の制約はありません。
数値例で違いを確認する
基点を $(8,5)$、方向を $(1,0)$、表示範囲を $0\leq x\leq6$、$0\leq y\leq10$ とします。
XLINEは負方向へ延びるため、矩形内の $(0,5)$ から $(6,5)$ までが表示線分になります。一方、RAYは $t\geq0$ であり、基点のX座標8から右へ延びます。表示矩形は基点より後方にあるため、交差しません。
同じ基点と方向ベクトルでも、XLINEとRAYを区別しなければ結果が変わる例です。
PythonでDXF値を切り出す
次のコードは、2Dへ投影した基点と方向ベクトルを軸平行矩形へクリッピングします。DXFを直接読む部分と幾何計算を分離しているため、ezdxf、自作パーサー、JSON中間形式のいずれからも利用できます。
from dataclasses import dataclass
from math import hypot, inf, isfinite
@dataclass(frozen=True)
class InfiniteEntity2D:
kind: str # "XLINE" or "RAY"
start: tuple[float, float]
direction: tuple[float, float]
def normalize2d(direction: tuple[float, float], eps: float = 1e-12):
dx, dy = direction
length = hypot(dx, dy)
if not isfinite(length) or length <= eps:
raise ValueError("方向ベクトルが不正です")
return dx / length, dy / length
def clip_to_box(entity: InfiniteEntity2D,
box: tuple[float, float, float, float],
eps: float = 1e-12):
xmin, ymin, xmax, ymax = box
if not (xmin <= xmax and ymin <= ymax):
raise ValueError("表示範囲の大小関係が不正です")
if entity.kind not in {"XLINE", "RAY"}:
raise ValueError("kindはXLINEまたはRAYです")
x0, y0 = entity.start
dx, dy = normalize2d(entity.direction, eps)
t_min = -inf if entity.kind == "XLINE" else 0.0
t_max = inf
for origin, delta, lower, upper in (
(x0, dx, xmin, xmax),
(y0, dy, ymin, ymax),
):
if abs(delta) <= eps:
if origin < lower - eps or origin > upper + eps:
return None
continue
t1 = (lower - origin) / delta
t2 = (upper - origin) / delta
enter, leave = sorted((t1, t2))
t_min = max(t_min, enter)
t_max = min(t_max, leave)
if t_min > t_max + eps:
return None
p1 = (x0 + t_min * dx, y0 + t_min * dy)
p2 = (x0 + t_max * dx, y0 + t_max * dy)
return p1, p2
box = (0.0, 0.0, 6.0, 10.0)
print(clip_to_box(InfiniteEntity2D("XLINE", (8.0, 5.0), (1.0, 0.0)), box))
print(clip_to_box(InfiniteEntity2D("RAY", (8.0, 5.0), (1.0, 0.0)), box))
出力は、XLINEが((0.0, 5.0), (6.0, 5.0))、RAYがNoneです。
ASCII DXFから値を取り出す
自作パーサーでエンティティ単位のコード・値ペアを得ている場合は、次のように基点と方向を作れます。
def first(record: list[tuple[int, str]], code: int, default="0") -> str:
return next((value for c, value in record if c == code), default)
def parse_xline_or_ray(record: list[tuple[int, str]]) -> InfiniteEntity2D:
kind = first(record, 0, "")
return InfiniteEntity2D(
kind=kind,
start=(float(first(record, 10)), float(first(record, 20))),
direction=(float(first(record, 11)), float(first(record, 21))),
)
実用パーサーでは、Z成分、セクション境界、バイナリDXF、非有限値、重複コードも検証します。XY平面へ単純に落とせない3D図面では、ビュー座標系へ変換してから2Dクリッピングを行います。
データモデルは無限性を失わない
表示用の有限線分を計算しても、保存用モデルをLINEへ置き換えるべきではありません。切り出し結果は表示範囲に依存するため、ズームや印刷範囲が変われば端点も変わります。
中間データには少なくとも次を保持します。
- エンティティ種別がXLINEかRAYか
- WCSの基点
- WCSの元方向ベクトル
- 正規化後の方向ベクトル
- レイヤーや色などの共通属性
- 表示時に使った座標変換とクリップ範囲
この構造なら、DXFへの再出力、別ビューでの再描画、交差判定、補助線の抽出に同じ幾何情報を再利用できます。
実務上の注意
- コード11を終点として扱わない
- RAYのパラメータ下限を0にする
- 方向成分が0の軸で除算しない
- ゼロベクトルと非有限値を読み取り時に除外する
- 3D値を画面へ出す前に、対象ビューの座標系へ変換する
- 表示範囲をDXF本体の形状として保存しない
- 変換先が無限図形を持たない場合だけ、表示用LINEへ一時変換する
交点計算でも同じ区別が必要です。XLINE同士の交点が存在しても、RAYでは交点に対応する $t$ が負なら対象外です。単なる直線の交点計算だけで終わらず、各エンティティのパラメータ範囲を検査します。
まとめ
XLINEとRAYは、基点と単位方向ベクトルで定義される無限図形です。XLINEは両方向、RAYは正方向だけへ延びます。
DXFではコード10・20・30が基点、コード11・21・31が方向ベクトルです。有限表示へ変換するときは、パラメトリック表現と矩形の各軸区間を使い、XLINEを $(-\infty,\infty)$、RAYを $[0,\infty)$ からクリッピングします。
無限性を中間データに残し、表示時だけ有限線分を生成すれば、ズーム、印刷、SVG変換、交差判定、DXF再出力へ安全に展開できます。
