[Astro] #117 DXF 3D Viewer — dxf-parser + Three.jsによるDXFファイルの2D/3Dビューア

[Astro] #117 DXF 3D Viewer — dxf-parser + Three.jsによるDXFファイルの2D/3Dビューア

概要

CADの標準フォーマットであるDXF(Drawing Exchange Format)ファイルを、ブラウザ上で2D/3D表示するビューアを構築した。サーバーへのアップロード不要で、ファイルはすべてローカル環境(ブラウザ内)で処理される。

動画(GIF)

[Astro] #117 DXF 3D Viewer — dxf-parser + Three.jsによるDXFファイルの2D/3Dビューア

sample data

サンプルデータは、「クワバラ設計企画」のWEBサイトよりお借りしています。

今回の実装内容

機能・モジュール概要
DXFパースdxf-parserによるエンティティ・レイヤー・ブロック定義の解析
9種エンティティ描画LINE, LWPOLYLINE, POLYLINE, CIRCLE, ARC, ELLIPSE, 3DFACE, POINT, SOLID
TEXT/MTEXT描画CanvasTexture + Spriteによるテキストラベル表示
DIMENSION描画寸法線の線分+寸法値テキスト表示
INSERT描画ブロック参照の再帰展開(座標変換・スケール・回転対応)
レイヤー管理AutoCAD Color Index対応、レイヤー別ON/OFF切替
Shift-JIS自動判定日本語DXF(ANSI_932)の自動検出・再読み込み
ビュー制御Top(XY)/Front(XZ)切替、Grid/Axes/Text表示切替
スクリーンショット現在のビューポートをPNG画像として保存

背景 — Point Cloud Viewerとの連携

本ツールの開発動機は、自作のPoint Cloud Viewer(点群ビューア)で実装した「断面ポリライン→DXFエクスポート」機能の出力を検証するためのビューアが必要だったこと。

Point Cloud Viewerでは、点群データを任意の軸でスライスし、断面プロファイルをDXFのPOLYLINEとしてエクスポートする機能がある。その出力されたDXFを、別途CADソフトを起動せずにブラウザ内で即座に確認できる環境を作りたかった。

結果として、自分のツール間でデータの入出力が完結する形になった。

点群データ (PLY/LAS/LAZ)
  → Point Cloud Viewer(スライス&断面生成)
    → DXF Export(POLYLINE)
      → DXF 3D Viewer(確認・スクリーンショット)

スクリーンショット

サンプル図面の表示

DXF 3D Viewer - サンプル図面

Point Cloud Viewer出力の断面DXF

DXF 3D Viewer - 断面プロファイル

技術実装の詳細

1. dxf-parserの出力構造

dxf-parserはDXFファイルをテキストとして受け取り、以下の構造に変換する。

interface IDxf {
  header: Record<string, IPoint | number>;
  entities: IEntity[];          // エンティティ配列(LINE, CIRCLE等)
  blocks: Record<string, IBlock>; // ブロック定義(INSERT参照先)
  tables: ITables;              // レイヤー定義等
}

各エンティティはtype(種別)、layer(所属レイヤー)、colorIndex(色番号)と、種別固有のデータ(頂点座標、中心点、半径等)を持つ。16種のエンティティに対応しているが、本ツールでは実用上重要な9種+TEXT/MTEXT/DIMENSION/INSERTの計13種を描画対象とした。

2. Three.jsでのエンティティ描画

各DXFエンティティをThree.jsのオブジェクトに変換する。基本方針は「LINEベースのエンティティはBufferGeometry + LineBasicMaterial、面を持つエンティティはMesh」。

// LINE: 2頂点を結ぶ線分
function drawLine(entity, color) {
  const points = entity.vertices.map(
    v => new THREE.Vector3(v.x, v.y, v.z || 0)
  );
  const geometry = new THREE.BufferGeometry().setFromPoints(points);
  const material = new THREE.LineBasicMaterial({ color });
  const line = new THREE.Line(geometry, material);
  getLayerGroup(entity.layer).add(line);
}

// CIRCLE: EllipseCurveで円を生成し、LineとしてXY平面に描画
function drawCircle(entity, color) {
  const curve = new THREE.EllipseCurve(
    entity.center.x, entity.center.y,
    entity.radius, entity.radius,
    0, Math.PI * 2, false, 0
  );
  const points = curve.getPoints(64).map(
    p => new THREE.Vector3(p.x, p.y, entity.center.z || 0)
  );
  // ... BufferGeometry + Line
}

// ARC: EllipseCurveのstartAngle/endAngleを指定
// ELLIPSE: majorAxis + axisRatioからradiusX/Yと回転角を計算

3. TEXT/MTEXTの描画(Sprite方式)

DXFのTEXT/MTEXTは2Dラベルとして描画する必要がある。Three.jsのCSS2DRendererも選択肢だが、スクリーンショット出力との相性を考慮し、CanvasTexture + Sprite方式を採用した。

function createTextSprite(text, position, height, color) {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');

  // テキストをCanvasに描画
  ctx.font = `${fontSize}px monospace`;
  ctx.fillStyle = '#' + color.toString(16).padStart(6, '0');
  ctx.fillText(text, 4, canvas.height / 2);

  // CanvasTexture → SpriteMaterial → Sprite
  const texture = new THREE.CanvasTexture(canvas);
  const material = new THREE.SpriteMaterial({ map: texture, transparent: true });
  const sprite = new THREE.Sprite(material);

  // ワールド空間でテキスト高さに合わせてスケール
  sprite.scale.set(worldHeight * aspect, worldHeight, 1);
  sprite.position.set(position.x, position.y, position.z);

  return sprite;
}

Sprite方式の利点は、常にカメラに正対するためどの角度からもテキストが読めること。またdepthTest: falseを指定することで、他のジオメトリに隠れずに常に表示される。

テキストのON/OFF切替は、entityGroup.traverseでSpriteインスタンスを検出してvisibleを切り替えるだけで実現できる。

4. INSERT(ブロック参照)の再帰展開

DXFのINSERTエンティティは、blocksテーブルに定義されたブロックを指定位置・スケール・回転で配置する参照。描画時にはブロック内の全エンティティの座標を変換して展開する。

function drawInsert(entity, dxf, layerColors) {
  const block = dxf.blocks[entity.name];
  const pos = entity.position;
  const cos = Math.cos(entity.rotation * Math.PI / 180);
  const sin = Math.sin(entity.rotation * Math.PI / 180);

  function transformPoint(p) {
    const x = p.x * entity.xScale;
    const y = p.y * entity.yScale;
    return {
      x: x * cos - y * sin + pos.x,
      y: x * sin + y * cos + pos.y,
      z: p.z * entity.zScale + pos.z
    };
  }

  for (const blockEntity of block.entities) {
    // 座標を変換したコピーを作成
    const transformed = JSON.parse(JSON.stringify(blockEntity));
    if (transformed.vertices)
      transformed.vertices = transformed.vertices.map(transformPoint);
    if (transformed.center)
      transformed.center = transformPoint(transformed.center);

    // 既存の描画関数にそのまま渡す
    switch (transformed.type) {
      case 'LINE': drawLine(transformed, color); break;
      case 'CIRCLE': drawCircle(transformed, color); break;
      // ...
    }
  }
}

座標変換後のコピーを既存の描画関数に渡すことで、描画関数自体を変更せずにINSERT対応を実現した。

5. レイヤー管理とAutoCAD Color Index

DXFのレイヤーはtables.layer.layersに定義されており、各レイヤーにAutoCAD Color Index(ACI)が割り当てられている。エンティティの色は以下の優先順で決定される。

  1. エンティティ自体のcolorIndex(256以外の場合)
  2. 所属レイヤーのACI色
  3. デフォルト色
const ACI_COLORS = {
  1: 0xff0000, 2: 0xffff00, 3: 0x00ff00, 4: 0x00ffff,
  5: 0x0000ff, 6: 0xff00ff, 7: 0xffffff, 8: 0x808080, ...
};

レイヤーごとにTHREE.Groupを作成し、UIからの表示切替はgroup.visibleのトグルで実現。

6. Shift-JIS自動判定

日本のCADソフトが出力するDXFはShift-JIS(コードページ ANSI_932)でエンコードされている場合が多い。FileReader.readAsText()のデフォルトはUTF-8のため、日本語レイヤー名やテキストが文字化けする。

対策として、最初にUTF-8で読み込み、文字化け(U+FFFD置換文字)またはヘッダー内のANSI_932指定を検出した場合にShift-JISで再読み込みする2段階方式を採用した。

reader.onload = (e) => {
  let text = e.target.result;

  if (text.includes('\ufffd') || text.includes('ANSI_932')) {
    const reReader = new FileReader();
    reReader.onload = (e2) => parseDxf(e2.target.result);
    reReader.readAsText(file, 'Shift_JIS');
    return;
  }

  parseDxf(text);
};
reader.readAsText(file);

対応エンティティ一覧

エンティティ描画方法備考
LINEBufferGeometry + Line2頂点の線分
LWPOLYLINEBufferGeometry + Line軽量ポリライン、閉合対応
POLYLINEBufferGeometry + Line3Dポリライン
CIRCLEEllipseCurveLine64分割で近似
ARCEllipseCurve(角度指定)→ Line開始角/終了角対応
ELLIPSEEllipseCurve + 長軸/短軸比 → Line回転対応
POINTPointssizeAttenuation: falseで固定サイズ
3DFACEMesh(三角/四角分割)+ ワイヤーフレーム半透明面+輪郭線
SOLIDMesh3DFACEと同一処理
TEXTCanvasTexture + Spriteテキスト高さに合わせたスケーリング
MTEXTCanvasTexture + Spriteフォーマットコード除去、1行目表示
DIMENSIONLine + Sprite寸法線+寸法値テキスト
INSERT再帰展開座標変換・スケール・回転適用

技術スタック

  • Astro — ページ構築
  • Three.js — 3D描画、OrbitControls
  • dxf-parser — DXFファイルパーサー(CDN経由で読み込み)
  • HTML5 Canvas — テキストラベルのテクスチャ生成

更新履歴

  • [v0.1]: 初期実装。LINE/LWPOLYLINE/POLYLINE/CIRCLE/ARC/ELLIPSE/POINT/3DFACE/SOLID描画、レイヤー管理、Shift-JIS自動判定。
  • [v0.2]: TEXT/MTEXT描画(CanvasTexture + Sprite)、テキスト表示ON/OFFチェックボックス追加。
  • [v0.3]: DIMENSION描画(寸法線+テキスト)、INSERT描画(ブロック参照の再帰展開・座標変換)。
  • [v1.0]: スクリーンショットPNG出力機能追加。