# 間取り図PDFをAIに渡すだけで自分の家を3D化する手順書｜Claude Code / Codex用

URL: https://roresan.com/blog/floorplan-3d-ai/
Category: 間取り・設計
Tags: 3D, 間取り, AI, ClaudeCode, 一条工務店, 図面
Published: 2026-08-01

> 一条工務店の図面PDFとこの記事のURLをClaude CodeまたはCodexに渡すだけで、自分の家をブラウザで動く3Dモデルにできます。AIがそのまま読んで実行できる完全手順書。専門知識・課金ツール不要、所要1〜2時間。

export const Box = ({children, tone = "warm"}) => (
  <div style={{
    background: tone === "warn" ? "#fdf2ec" : "#fff8f0",
    border: tone === "warn" ? "none" : "2px solid #e8a33d",
    borderLeft: tone === "warn" ? "6px solid #d9534f" : "2px solid #e8a33d",
    borderRadius: tone === "warn" ? "0 8px 8px 0" : "10px",
    padding: "16px 20px", margin: "20px 0",
  }}>{children}</div>
);

export const Step = ({title, children}) => (
  <details open={false} style={{
    border: "1px solid #e5d9c5", borderRadius: "10px",
    padding: "12px 18px", margin: "16px 0", background: "#fffdfa",
  }}>
    <summary style={{cursor: "pointer", fontWeight: "bold", color: "#7a4a00"}}>
      {title}
    </summary>
    <div style={{marginTop: "12px"}}>{children}</div>
  </details>
);

自分の家の間取り図を、ブラウザでぐるぐる回せる3Dモデルにしてみませんか。

しかも、あなたがやることは実質3ステップだけ。図面のPDFを用意して、AIを起動して、この記事のURLを渡す。それだけです。

実際に私の家（一条工務店 GRAND SMART 平屋）でやってみたところ、間取りはもちろん、招き屋根の形・ハイドロテクトタイルの黒い外壁・太陽光パネル・大開口の窓まで、図面どおりの3Dが出来上がりました。

<img src="/img/floorplan-3d-ai/exterior-south.jpg" alt="図面から生成した3Dモデルの南側外観。ハイドロテクトタイル黒の外壁、太陽光一体の招き屋根、大開口スライディングとウッドデッキ" loading="lazy" style={{ width: '100%', borderRadius: 8, border: '1px solid #e2e8f0', margin: '1rem 0 0.4rem' }} />
<p style={{ fontSize: '0.85rem', color: '#6b7280', margin: '0 0 1.6rem', textAlign: 'center' }}>図面PDFだけで生成した3Dモデル（南面）。招き屋根・黒いタイル外壁・大開口の窓・ウッドデッキまで再現されています</p>

---

## 📖 この記事の使い方（人間が読むのはここだけ）

正直に言うと、**この記事の本体は人間向けに書かれていません**。AI（Claude Code / Codex）がそのまま読んで実行するための手順書です。

なので、あなたが読むのはこのセクションだけで大丈夫です。

### 用意するもの

| 必要なもの | 説明 |
|---|---|
| 間取り図のPDF | 一条工務店の契約者なら、貸与タブレットの図面アプリから書き出せます。平面図・立面図・電気図面があると精度が上がります |
| Claude Code または Codex | AIにパソコンを操作してもらうためのツール。どちらか片方でOK |
| パソコン | Mac / Windows / Linux どれでも。3Dが動く程度のスペックがあれば十分 |

プログラミングの知識、有料の3Dソフト、CADの経験、どれもいりません。

### やること（3ステップ）

**1. 図面PDFを手元に置く**

一条工務店のタブレットから図面PDFを書き出して、パソコンのわかりやすい場所（デスクトップなど）に保存します。ファイル名は `図面.pdf` のように日本語でも大丈夫です。

**2. Claude Code または Codex を起動する**

ターミナル（Macなら「ターミナル」アプリ、Windowsなら「PowerShell」）を開いて、AIを起動します。

**3. AIに「この記事を読んで」と伝える**

起動したAIに、次のようにお願いするだけです。

```
https://roresan.com/blog/floorplan-3d-ai/ この記事を読んで、
その手順どおりに私の家の3Dモデルを作ってください。

図面PDFはここにあります: ~/Desktop/図面.pdf
```

あとはAIが、3Dエディタのダウンロードから、図面の読み取り、3Dデータの生成、ブラウザでの表示確認まで全部やってくれます。所要時間は1〜2時間くらいです。

### できあがるもの

- ブラウザでぐるぐる回せる自分の家の3Dモデル
- 部屋の中を歩き回れるウォークスルー表示
- 共有できるJSONファイル（家族や友人に渡せます）
- GLB形式の3Dモデル（動画制作やBlenderにも持ち込めます）

### つまずいたときは

AIが途中で失敗したら、そのエラーメッセージをそのままAIに見せて「この記事の『よくある失敗と対処』を見て直して」と伝えてください。想定される失敗パターンと対処法も記事内に書いてあるので、AIが自力で復旧できます。

---

以下は **AI向けの手順書** です。人間が読む必要はありませんが、興味があれば折りたたみを開いてみてください。

---

## 🤖 AI向け完全手順書

**AIへの指示**: あなたはこれから、ユーザーの家の間取り図PDFを読み取り、Webブラウザで動作する3Dモデルを生成します。以下のセクションを **上から順に** 実行してください。各セクションの折りたたみ（details）の中身がすべて実行内容です。省略・要約せずに読んでください。

**成果物**: `~/Documents/floorplan-3d/` 配下に生成スクリプトとシーンJSON、`~/Documents/pascal-editor/` に3Dエディタ本体。

**前提の確認**: 作業開始前に `node --version`（v20以上）と `python3 --version`（3.9以上）を確認し、Bun未導入なら Step 1 でインストールしてください。

### Step 0. 全体像とゴール設定

#### このタスクの本質

間取り図PDF → 3Dモデルの変換は、画像解析AIに丸投げするのではなく、**「図面を人間と同じように読んで、座標テーブルに転記し、コードで3Dシーンを生成する」** というアプローチを取ります。理由は次のとおりです。

- 日本の住宅図面（特に一条工務店）は910mmモジュールのグリッドに完全に乗っている。グリッド番号を数えるだけで座標が確定するため、画像から寸法を推定するより圧倒的に正確
- 既存のAI自動変換ツール（FloorplanToBlender3d、CubiCasa5K等）は欧米の間取りで学習されており、日本の図面記号・尺モジュールでの精度が低い
- 生成スクリプトを経由することで、後から一部だけ修正・再生成が容易になる

#### 使用ツール

| ツール | 用途 | ライセンス |
|---|---|---|
| [pascalorg/editor](https://github.com/pascalorg/editor) | React Three Fiber + WebGPU製の3D建築エディタ。壁・床・屋根・建具・家具をJSONで定義できる | MIT |
| Bun | pascal-editorのビルド・実行 | MIT |
| Python 3 | シーンJSON生成スクリプト | PSF |
| pdftoppm (poppler) | PDFを高解像度PNG化 | GPL |

#### 作業フロー

```
図面PDF
  ↓ ① pdftoppmで200dpi PNG化 + 4分割クロップ
図面画像（精読可能な解像度）
  ↓ ② グリッド番号で壁・建具・部屋を座標テーブルに転記
座標テーブル（Pythonコード内）
  ↓ ③ generate_scene.py 実行
シーンJSON（{nodes, rootNodeIds}形式）
  ↓ ④ pascal-editorにLoad Build
ブラウザ3Dモデル
  ↓ ⑤ 2D/3D表示を図面と突き合わせて差分修正（2〜3周）
完成
```

#### 品質基準（これを満たすまで完成としない）

1. 全部屋の位置・広さが平面図と一致（帖数の検算が合う）
2. 全開口部（窓・ドア）が図面の位置・サイズと一致
3. 屋根形状が立面図4面すべてと一致
4. 外装の色・素材が立面図（カラー）と一致
5. 図面に描かれている設備（キッチン・浴槽・トイレ等）が配置されている

### Step 1. 環境構築

#### 1-1. 必要なコマンドの確認とインストール

```bash
# バージョン確認
node --version    # v20以上が必要
python3 --version # 3.9以上が必要
bun --version     # 未インストールなら次のコマンド
```

Bunが未インストールの場合:

```bash
# macOS / Linux
curl -fsSL https://bun.sh/install | bash

# Windows (PowerShell)
powershell -c "irm bun.sh/install.ps1 | iex"
```

pdftoppm（poppler）が未インストールの場合:

```bash
# macOS
brew install poppler

# Ubuntu / Debian
sudo apt install poppler-utils

# Windows: https://github.com/oschwartz10612/poppler-windows/releases からダウンロード
```

#### 1-2. 3Dエディタのクローンとセットアップ

**重要**: ユーザーのプロジェクトリポジトリの **外側** にクローンしてください。中に置くとgitが入れ子になります。

```bash
cd ~/Documents
git clone --depth 1 https://github.com/pascalorg/editor.git pascal-editor
cd pascal-editor
bun install    # 数分かかる。node_modulesは1GB超になる
```

#### 1-3. 開発サーバー起動

```bash
cd ~/Documents/pascal-editor
bun run dev    # → http://localhost:3002 で起動
```

バックグラウンド実行し、`curl -s -o /dev/null -w "%{http_code}" http://localhost:3002` が200を返すまで待機してください。

#### 1-4. 作業ディレクトリの作成

```bash
mkdir -p ~/Documents/floorplan-3d
```

#### つまずきポイント

- `bun install` が失敗する場合、Node v20未満の可能性。nodebrew/nvm等でv20以上に切り替える
- ポート3002が使用中なら `lsof -ti:3002 | xargs kill -9` で解放
- WebGPU非対応ブラウザ（古いSafari等）では表示されない。Chrome最新版を推奨

### Step 2. 図面PDFの画像化と精読

#### 2-1. PDFの構成を把握する

まずPDFの各ページに何が描かれているかを一覧化します。

```bash
# 全ページのテキストから図面種別を抽出
for i in $(seq 1 $(pdfinfo ~/Desktop/図面.pdf | grep Pages | awk '{print $2}')); do
  echo "=== Page $i ==="
  pdftotext -f $i -l $i ~/Desktop/図面.pdf - 2>/dev/null | grep -E "平面図|立面図|電気|矩計|配置図|仕上表" | head -3
done
```

一条工務店の図面PDFの典型的な構成:

| ページ | 内容 | 3D化での用途 |
|---|---|---|
| 平面図 | 間取り・寸法・グリッド番号 | **最重要**。壁・部屋・建具の座標源 |
| 立面図（4面） | 東西南北の外観・屋根形状・軒の出 | **重要**。屋根と高さの決定 |
| 電気図面 | コンセント・スイッチ・照明の位置と高さ | 設備配置（発展）|
| カラー立面図 | 外壁・屋根の実際の色 | マテリアル設定 |
| 矩計図 | 天井高・階高の断面 | 高さの検証 |
| 仕上表 | 内外装の材料一覧 | マテリアル設定 |

#### 2-2. 精読可能な解像度で画像化

**重要**: デフォルトの低解像度PNGでは寸法線や記号が読み取れません。必ず200dpiで書き出してください。

```bash
cd ~/Documents/floorplan-3d
# 平面図（ページ番号は2-1で特定したもの）
pdftoppm -f 3 -l 3 -r 200 -png ~/Desktop/図面.pdf plan
# 立面図
pdftoppm -f 4 -l 4 -r 150 -png ~/Desktop/図面.pdf elev
# 電気図面
pdftoppm -f 2 -l 2 -r 200 -png ~/Desktop/図面.pdf elec
```

#### 2-3. 4分割クロップして読む

200dpiの全体画像（3300×2300px程度）をそのまま読むと、AIの画像認識では細部が潰れます。**必ず4分割して2倍に拡大**してから読んでください。

```python
from PIL import Image
im = Image.open('plan-03.png')
# 図面本体の領域を推定クロップ（余白・図枠を除く）
body = im.crop((350, 100, 2150, 2150))
bw, bh = body.size
quadrants = {
 'nw': (0, 0, bw//2+100, bh//2+100),
 'ne': (bw//2-100, 0, bw, bh//2+100),
 'sw': (0, bh//2-100, bw//2+100, bh),
 'se': (bw//2-100, bh//2-100, bw, bh),
}
for k, box in quadrants.items():
    c = body.crop(box)
    c.resize((c.width*2, c.height*2), Image.LANCZOS).save(f'plan-{k}.png')
```

各象限を Read ツールで画像として読み込み、記号・寸法・室名を読み取ります。

### Step 3. 図面の読み解き方（日本の住宅図面の癖）

#### 3-1. 910mmモジュール（尺モジュール）が最重要

日本の在来工法・一条工務店の図面は、**910mm（3尺）のグリッド**に完全に乗っています。図面の外周に `X0 X1 X2 ...` `Y0 Y1 Y2 ...` という通り芯番号が振られており、これが910mmピッチです。

つまり、**寸法を測る必要はありません。グリッドを数えるだけです。**

| 図面上の値 | 意味 |
|---|---|
| 1グリッド | 910mm（3尺） |
| 0.5グリッド | 455mm（1.5尺）。トイレ・廊下幅などで頻出 |
| 0.25グリッド | 227.5mm。壁のずれ・ニッチで登場 |
| 1帖 | 0.5グリッド × 1グリッド（910×1820mm） |

座標変換は単純です。

```python
M = 0.91  # 1グリッド = 910mm = 0.91m
def p(mx, my):
    return [mx * M, -my * M]   # 北をマイナスZにする（pascal-editorの座標系）
```

#### 3-2. 検算式（転記ミスの早期発見）

読み取ったグリッド数が正しいか、図面に書かれた面積表と突き合わせます。

- 建物寸法の検算: `10,010mm = 11グリッド × 910`、`8,190mm = 9グリッド × 910`
- 床面積の検算: `グリッドマス数 × 0.8281㎡`（0.91² = 0.8281）
- 帖数の検算: `グリッドマス数 ÷ 2`（例: 9マス = 4.5帖）

図面の「延床面積 69.66㎡ / 21.07坪」のような記載と一致すれば転記成功です。

#### 3-3. 一条工務店の図面記号の読み方

**建具記号（窓・ドア）**

一条の窓記号は `J` `JK` `JF` `VS` などの記号 + 4桁数字で構成されます。数字は **幅×高さを尺貫法または実寸で表現** しています。

| 記号例 | 読み方 |
|---|---|
| `J8371` | 大開口スライディング。幅約2,515mm × 高さ約2,151mm |
| `JF5915 +1677` | 横すべり出し窓。幅約1,790 × 高さ455、`+1677` は床からの取付高さ |
| `JK2430N × 2` | たてすべり出し窓を2連。`+1238` が付けば取付高さ1,238mm |
| `VS1030 (B)` | 縦すべり出し窓。`(B)` は網戸・仕様の区別 |
| `E-31ER` | 玄関ドア（電子錠付きの型番） |
| `SG311-1` `SG300` `SG33B` | 室内引戸・開き戸。数字が違えば別サイズ |
| `SGC-30B` `SGC-45E` `SGC-60E` | クローゼット。数字が幅（30=900mm、45=1,350mm、60=1,800mm 目安） |

**重要**: 数字から寸法を推定する際、確信が持てないものは図面の建具表（別ページにあることが多い）で確認してください。推定値を使う場合は、その旨をユーザーに報告します。

**設備記号**

| 記号例 | 意味 |
|---|---|
| `KG19-27WDR` | キッチン（グレイスキッチン系）。27 = 幅2,700mm |
| `CS19-B180D` | カップボード。180 = 幅1,800mm |
| `GM243R` / `GM2430` | グレイスドレッサー（洗面台）。24 = 幅1,200mm前後 |
| `1.25坪` `1616` | 浴室サイズ。1.25坪 = 1坪半サイズ |
| `TD-12` | 玄関ドア・土間まわりの記号 |
| `AC①(セ)` `AC②(ス)` | エアコン設置位置と種類 |
| `SA1` `RA1` `VA1` | 換気の給気口(SA)・還気口(RA)・排気(VA) |
| `床暖房1-A` | 床暖房のゾーン区分 |
| `CH=2200` | その部屋の天井高（標準は2,400mm） |
| `天井下地(G)` | 天井への下地補強（照明・物干し設置用） |
| `壁下地 H=0〜1820` | 壁への下地補強と範囲（TV壁掛け等） |

**図面の線種**

- 太い実線: 壁（外壁は特に太い）
- 細い一点鎖線: 通り芯（グリッド線）
- 破線: 上部にあるもの（吊戸棚・下がり天井・小屋裏）
- ハッチング（斜線）: 土間・タイル・デッキなどの床仕上げ違い

#### 3-4. 電気図面の読み方（コンセント・スイッチ・照明）

電気図面は平面図に赤字で設備が重ねて描かれています。**位置と高さの両方**が読み取れます。

| 記号 | 意味 | 標準取付高さ |
|---|---|---|
| ⊐ または二重丸 | コンセント（アース無は3口が標準） | 床から250mm（記載なき場合） |
| `H=900` | その設備の取付高さ指定 | 図に明記された値を優先 |
| `H=1300` | キッチン腰上コンセント等 | 1,300mm |
| ● / ○ | ダウンライト・シーリングライト | 天井 |
| `WTC57583W` 等の型番 | パナソニック製スイッチ・リモコン | 図の`H=`指定に従う |
| `とったらリモコン` | 着脱式リモコン（床暖房・給湯） | H=1300前後 |
| `E付` | アース付きコンセント（洗濯機・冷蔵庫・エアコン） | 設備に応じる |
| `200V AEYH用` | エコキュート等の200V専用回路 | H=1FL+40 |
| `専用E付` | 電子レンジ・食洗機等の専用回路 | H=900〜1,300 |
| `情報ボックス` `PC用ルーター` | 弱電盤・LAN機器 | H=2100等 |
| `分電盤用` | 分電盤 | H=1800前後 |
| `火災警報器` | 煙感知器（壁/天井） | 天井または壁上部 |

**電気設備を3Dに反映する方法**

pascal-editorでは、コンセント・スイッチは **壁に取り付くitem** として配置できます。

```python
# 壁付けアイテムの配置例
# wallId で壁を指定し、wallT (0〜1) で壁上の位置、position[1] で高さを指定
{
  "type": "item",
  "parentId": "<wall_id>",
  "wallId": "<wall_id>",
  "wallT": 0.35,                     # 壁の始点から35%の位置
  "position": [0.35 * wall_length, 0.25, 0],   # [壁沿い距離, 高さ, 壁からのオフセット]
  "asset": {
    "id": "power-outlet",
    "src": "/items/power-outlet/model.glb",
    "thumbnail": "/items/power-outlet/thumbnail.webp",
    "dimensions": [0.12, 0.12, 0.03],
    "attachTo": "wall",
    ...
  }
}
```

利用可能なGLBアセットは `~/Documents/pascal-editor/apps/editor/public/items/` を `ls` して確認してください。照明系は `ceiling-light` `ceiling-lamp` `circular-ceiling-light` `floor-lamp` などがあります。

**注意**: コンセントは1軒で50〜80箇所あるため、全部配置すると視認性が落ちます。ユーザーに「全部配置するか、主要なものだけにするか」を確認してください。

#### 3-5. 図面読み取りの落とし穴（実際にハマったもの）

<Box tone="warn">

**落とし穴1: 窓がない壁に窓を付けてしまう**

平面図の壁の切れ目や、設備の記号線を窓と誤認しやすいです。窓・ドアは必ず **立面図と照合** してください。立面図に描かれていない開口部は存在しません。逆に立面図にあって平面図で見落としているケースもあります。

**落とし穴2: 設備の出っ張り形状を単純化しすぎる**

洗面台やカウンターは、図面上では「壁から何mm出っ張っているか」が正確に描かれています。GLBアセットのデフォルトサイズをそのまま使うと形が合いません。`scale` で図面の寸法に合わせ、L字・段差がある場合は複数のアイテムやslabを組み合わせてください。

**落とし穴3: 室名の隣が別室**

「押入」の隣が「洗面台」など、狭い区画が連続する水回りは特に取り違えやすいです。室名テキストがどの区画に属しているかは、囲んでいる壁線をたどって確認してください。

**落とし穴4: 通り芯と壁面を混同する**

図面の寸法は原則「壁芯（かべしん）」基準です。壁の内法（部屋の実際の広さ）は壁厚の半分ずつ小さくなります。3Dの壁も壁芯を`start`/`end`に指定すれば整合します。

**落とし穴5: 「上部アケル」「フキアゲ」等の注記を見落とす**

天井の抜け・吹き上げ・下がり天井は3D形状に影響します。`CH=2200` `上部アケル` `天井下地` などの注記は必ず拾ってください。

</Box>

### Step 4. 座標テーブルへの転記とシーンJSON生成

#### 4-1. pascal-editorのシーンJSON仕様

シーン全体は `{"nodes": {id: node}, "rootNodeIds": [...]}` という形式です。

**座標系**: メートル単位、Y-up（Yが高さ）、右手系。2D座標は `[x, z]` で、北が −Z 方向。

**ノード階層**: `site → building → level → 壁・床・部屋・屋根・家具`

**ID規則（重要）**: IDは型プレフィックス必須です。間違えるとインポートが拒否されます。

| ノード種別 | IDプレフィックス |
|---|---|
| 壁 | `wall_` |
| 窓 | `window_` |
| ドア | `door_` |
| 床スラブ | `slab_` |
| 部屋（ゾーン） | `zone_` |
| 屋根グループ | `roof_` |
| 屋根セグメント | `rseg_` |
| 家具 | `item_` |
| 階 | `level_` |
| 建物 | `building_` |
| 敷地 | `site_` |

#### 4-2. 生成スクリプトのテンプレート

`~/Documents/floorplan-3d/generate_scene.py` として作成します。以下は実際に動作した完全なテンプレートです。**座標テーブル部分（壁・窓・部屋の定義）だけをユーザーの図面に合わせて書き換えてください。**

```python
#!/usr/bin/env python3
"""間取り図 → pascal-editor シーンJSON 生成

座標系: メートル, Y-up。x = 東(+), z = 南(+)（北 = -z）
モジュール座標 (mx, my): グリッド番号。my は北向きに増加
変換: x = mx * M, z = -my * M
"""
import json

M = 0.91       # 1モジュール = 910mm（メーターモジュールなら 1.0）
EXT_T = 0.16   # 外壁厚
INT_T = 0.12   # 間仕切壁厚
WALL_H = 2.40  # 天井高（一条標準 2400）

nodes = {}

def p(mx, my):
    return [round(mx * M, 4), round(-my * M, 4)]

def add(node):
    nodes[node["id"]] = node
    return node["id"]

def base(id_, type_, name, parent, **extra):
    n = {"object": "node", "id": id_, "type": type_, "name": name,
         "parentId": parent, "visible": True, "metadata": {}}
    n.update(extra)
    return n

LEVEL = "level_1f"

# ============ 壁 ============
# 【向きの規約・重要】進行方向の左手が屋外になる順序で描く
# （back面 = 左手側 = exterior 扱いのため。逆だと外装が室内側に付く）
walls = []
def wall(id_, s, e, t, name):
    walls.append((id_, s, e, t, name))

# 外周壁（グリッド座標で指定。ここをユーザーの図面に合わせて書き換える）
wall("wall_ext_s",  (11, 1), (0, 1),  EXT_T, "外壁 南")
wall("wall_ext_e",  (11, 8), (11, 1), EXT_T, "外壁 東")
wall("wall_ext_n",  (0, 8),  (11, 8), EXT_T, "外壁 北")
wall("wall_ext_w",  (0, 1),  (0, 8),  EXT_T, "外壁 西")
# 間仕切壁
wall("wall_p1", (0, 5.5), (2, 5.5), INT_T, "浴室 南")

wall_children = {w[0]: [] for w in walls}

# ============ 開口部（窓・ドア）============
# dist = 壁の start からの中心距離[m]
def window(id_, wall_id, dist_m, width, height, center_y, wtype="sliding", name=""):
    add(base(id_, "window", name or id_, wall_id,
             position=[round(dist_m, 4), round(center_y, 4), 0],
             rotation=[0, 0, 0], wallId=wall_id,
             width=width, height=height,
             windowType=wtype, openingKind="window"))
    wall_children[wall_id].append(id_)

def door(id_, wall_id, dist_m, width, height, dtype="hinged", name=""):
    add(base(id_, "door", name or id_, wall_id,
             position=[round(dist_m, 4), round(height / 2, 4), 0],
             rotation=[0, 0, 0], wallId=wall_id,
             width=width, height=height, doorType=dtype))
    wall_children[wall_id].append(id_)

# 例: 南面の大開口（start が X11 なので dist は X11 からの距離）
window("window_main", "wall_ext_s", (11 - 5.65) * M, 2.51, 2.15, 0.05 + 2.15/2,
       "sliding", "大開口スライディング")
door("door_entrance", "wall_ext_n", 1.5 * M, 0.98, 2.30, "hinged", "玄関ドア")

# ============ 壁ノードの登録 ============
EXT_TILE = {"properties": {"color": "#3d4045", "roughness": 0.65, "metalness": 0.05}}
for wid, s, e, t, name in walls:
    extra, front, back = {}, "unknown", "unknown"
    if wid.startswith("wall_ext_"):
        extra["exteriorMaterial"] = EXT_TILE
        front, back = "interior", "exterior"
    add(base(wid, "wall", name, LEVEL,
             start=p(*s), end=p(*e), thickness=t, height=WALL_H,
             children=wall_children[wid],
             frontSide=front, backSide=back, **extra))

# ============ 床スラブ ============
outline = [(0, 1), (11, 1), (11, 8), (0, 8)]
add(base("slab_floor", "slab", "1階床", LEVEL,
         polygon=[p(*pt) for pt in outline], holes=[], holeMetadata=[],
         elevation=0.02, thickness=0.2, autoFromWalls=False))

# ============ ゾーン（部屋）============
zone_defs = [
    ("zone_ldk", "LDK (22.42帖)", [(0,1),(8,1),(8,8),(0,8)], "#f59e0b"),
]
for zid, name, poly, color in zone_defs:
    add(base(zid, "zone", name, LEVEL,
             polygon=[p(*pt) for pt in poly],
             autoFromWalls=False, boundaryWallIds=[],
             spaceRole="room", roomNumber="", enclosureStatus="auto",
             floorFinish="", wallFinish="", ceilingFinish="",
             ceilingHeight=WALL_H, occupancy="",
             clearDimensionPolicy="none", color=color))

# ============ 屋根 ============
# 【重要】roof-segment は必ず roof ノードの子にする
PITCH = 19.29   # 3.5寸勾配 = atan(3.5/10)。4寸なら21.8、5寸なら26.57
PI = 3.14159265
SOLAR_TOP = {"properties": {"color": "#1f2630", "roughness": 0.3, "metalness": 0.25}}
ROOF_EDGE = {"properties": {"color": "#2a2d31", "roughness": 0.5, "metalness": 0.1}}
DARK_TILE = {"properties": {"color": "#3d4045", "roughness": 0.65, "metalness": 0.05}}

def rseg(id_, name, mx0, mx1, my0, my1, rot, pos_y, wall_h, **extra):
    cx, cz = (mx0 + mx1) / 2 * M, -(my0 + my1) / 2 * M
    return add(base(id_, "roof-segment", name, "roof_1",
                    position=[round(cx, 3), pos_y, round(cz, 3)], rotation=rot,
                    width=round((mx1 - mx0) * M, 3), depth=round((my1 - my0) * M, 3),
                    roofType="shed", pitch=PITCH, wallHeight=wall_h, overhang=0.25,
                    wallThickness=0.1, deckThickness=0.1, shingleThickness=0.05,
                    children=[], **extra))

# shed（片流れ）は local -Z 側が高い。rotation=PI で南向きに反転
rseg("rseg_main", "大屋根", 0, 11, 0.5, 5.75, 0.0, 2.40, 0.10)
rseg("rseg_north", "北下屋", 0, 11, 5.75, 8.5, PI, 2.40, 0.62)

add(base("roof_1", "roof", "屋根", LEVEL, position=[0,0,0], rotation=0,
         topMaterial=SOLAR_TOP, edgeMaterial=ROOF_EDGE, wallMaterial=DARK_TILE,
         children=["rseg_main", "rseg_north"]))

# ============ 家具・設備 ============
# 【重要】asset.thumbnail が必須。無いとインポート拒否される
ITEM_IDS = []
def item(id_, asset_id, dims, offset, mx, my, ry=0.0, scale=(1,1,1), name=""):
    ITEM_IDS.append(id_)
    add(base(id_, "item", name or asset_id, LEVEL,
             position=[round(mx*M, 3), 0, round(-my*M, 3)],
             rotation=[0, ry, 0], scale=list(scale), children=[],
             asset={"id": asset_id, "name": name or asset_id, "category": "furniture",
                    "thumbnail": f"/items/{asset_id}/thumbnail.webp",
                    "src": f"/items/{asset_id}/model.glb",
                    "dimensions": dims, "offset": offset,
                    "rotation": [0,0,0], "scale": [1,1,1]}))

HPI = 1.5708  # 90度
item("item_kitchen", "kitchen", [2.5,1.1,1], [0,0,0], 5.5, 5.85, 0, (1.08,1,0.75), "キッチン")

# ============ 階層 ============
add(base(LEVEL, "level", "1階", "building_1", level=0, height=3.0,
         children=[w[0] for w in walls] + ["slab_floor"]
                  + [z[0] for z in zone_defs] + ["roof_1"] + ITEM_IDS))
add(base("building_1", "building", "自邸", "site_1",
         position=[0,0,0], rotation=[0,0,0], children=[LEVEL]))
add(base("site_1", "site", "敷地", None,
         polygon={"type": "polygon",
                  "points": [p(-3,-3), p(14,-3), p(14,14), p(-3,14)]},
         children=["building_1"]))

graph = {"nodes": nodes, "rootNodeIds": ["site_1"]}
import os
out = os.path.expanduser("~/Documents/floorplan-3d/my-house.json")
with open(out, "w", encoding="utf-8") as f:
    json.dump(graph, f, ensure_ascii=False, indent=1)
print(f"nodes: {len(nodes)} -> {out}")
```

#### 4-3. 家具アセットの一覧を確認する

```bash
ls ~/Documents/pascal-editor/apps/editor/public/items/
```

主要なアセットと標準寸法 `[幅, 高さ, 奥行]`:

| asset_id | dimensions | 用途 |
|---|---|---|
| `kitchen` | [2.5, 1.1, 1] | システムキッチン |
| `kitchen-counter` | [2, 0.8, 1] | カップボード |
| `fridge` | [1, 2, 1] | 冷蔵庫 |
| `dining-table` | [2.5, 0.8, 1] | ダイニングテーブル |
| `dining-chair` | [0.5, 1, 0.5] | 椅子 |
| `sofa` | [2.5, 0.8, 1.5] | ソファ |
| `coffee-table` | [2, 0.4, 1.5] | ローテーブル |
| `tv-stand` | [2, 0.4, 0.5] | TVボード |
| `double-bed` | [2, 0.8, 2.5] | ダブルベッド |
| `closet` | [2, 2.5, 1] | クローゼット |
| `toilet` | [1, 0.9, 1] | トイレ |
| `bathroom-sink` | [2, 1, 1.5] | 洗面台 |
| `bathtub` | [2.5, 0.8, 1.5] | 浴槽 |
| `washing-machine` | [1, 1, 1] | 洗濯機 |
| `drying-rack` | [2, 1.1, 1] | 物干し |
| `piano` | - | ピアノ |
| `indoor-plant` | - | 観葉植物 |

`dimensions` はアセット固有の値なので上表のとおりに指定し、**実際のサイズ調整は `scale` で行います**（例: 幅1,800mmのカップボードなら `scale=(0.9, 1, 0.5)`）。

### Step 5. インポートと検証ループ

#### 5-1. シーンの生成とインポート

```bash
cd ~/Documents/floorplan-3d && python3 generate_scene.py
```

ブラウザで `http://localhost:3002` を開き、次の手順でインポートします。

1. 左サイドバーの **歯車アイコン**（設定）をクリック
2. **SAVE & LOAD** セクションの **Load Build** をクリック
3. 生成した `my-house.json` を選択
4. スキーマ検証ダイアログが出る → エラーがなければ **Replace current scene**

**AIが自動でこれを行う場合**: chrome-devtools MCPが使える環境なら、`Load Build` ボタンのuidに対して `upload_file` を実行することでファイル選択ダイアログを経由せず直接インポートできます。ブラウザ操作MCPがない場合は、ユーザーに手動操作を依頼してください。

#### 5-2. 検証の2軸

| 検証軸 | 操作 | 照合先 |
|---|---|---|
| 平面の正確さ | 上部の **2D** ボタン | 平面図 |
| 立体の正確さ | **3D** + 画面下の **Orbit Left/Right**（45°刻み） | 立面図4面 |
| 内観 | **Preview** ボタン → ウォークスルー | 完成イメージ |
| 屋根内部 | **Full height** トグルで屋根を透過 | 平面図 |

#### 5-3. 修正ループ

差分を見つけたら `generate_scene.py` の座標テーブルを修正 → 再生成 → 再インポート、を繰り返します。**2〜3周は必ず必要**です。

チェックリスト:

- [ ] 全部屋の位置・広さが平面図と一致しているか
- [ ] 帖数の検算が合っているか
- [ ] 窓・ドアの位置とサイズが図面どおりか
- [ ] **立面図に描かれていない窓を付けていないか**（頻出ミス）
- [ ] 屋根の勾配・段差・軒の出が立面図4面と一致しているか
- [ ] 外壁・屋根の色がカラー立面図と一致しているか
- [ ] 設備（キッチン・浴槽・トイレ・洗面台）が図面の位置にあるか
- [ ] 設備の出っ張り・向きが図面どおりか

### Step 6. よくある失敗と対処

| 症状 | 原因 | 対処 |
|---|---|---|
| インポートで `Invalid input` エラー（roof-segment） | IDプレフィックスが `roofseg_` になっている | `rseg_` に修正 |
| インポートで `asset.thumbnail` エラー | itemノードにthumbnailがない | `asset.thumbnail` を追加（`/items/<id>/thumbnail.webp`） |
| **屋根が全く表示されない** | roof-segmentをlevel直下に置いている | 必ず `roof` ノードを作りその子にする。levelのchildrenには`roof`のIDだけ入れる |
| 外壁の色が室内側に付く | 壁の描画方向が逆 | 壁の`start`/`end`を入れ替える（進行方向の左手が屋外） |
| 窓・ドアが表示されない | 壁の`children`に登録していない | `parentId`設定に加え、親壁の`children`配列にもIDを追加 |
| 建物が真っ黒で何も見えない | カメラが屋根の中に入っている | ホイールでズームアウト、または`Top View`ボタン |
| 家具が床にめり込む／浮く | `scale`のY値が不適切 | Y方向のscaleは1.0を基本にし、幅・奥行だけ調整 |
| `bun run dev`が起動しない | ポート3002が使用中 | `lsof -ti:3002 \| xargs kill -9` |
| 3D画面が真っ白 | WebGPU非対応 | Chrome最新版を使う |
| ブラウザ操作で要素が見つからない | ページ再描画でuidが変わった | snapshotを取り直してから操作 |

#### カメラ操作（AIがスクリプトで操作する場合）

```javascript
// ズーム
const canvas = document.querySelector('canvas');
const r = canvas.getBoundingClientRect();
canvas.dispatchEvent(new WheelEvent('wheel', {
  clientX: r.left + r.width/2, clientY: r.top + r.height/2,
  deltaY: 120, bubbles: true, cancelable: true   // 正=ズームアウト
}));
```

回転は画面下部の `Orbit Left` / `Orbit Right` ボタン（45°刻み）を使うのが確実です。ポインタイベントでのドラッグ回転は座標計算が煩雑になります。

### Step 7. 完成データの活用

すべてのエクスポートは 設定（歯車）→ **EXPORT** セクションから行えます。

| 形式 | 用途 |
|---|---|
| **Save Build**（JSON） | 編集後のシーンを保存。**このファイルを渡せば誰でも同じ3Dを再現できる** |
| **Export GLB** | 3Dモデル標準形式。Blender・Three.js・動画制作（Remotion等）に持ち込める |
| **Export STL** | 3Dプリント用 |
| **Export OBJ** | 汎用3D形式 |
| **Full floor plan** | 平面図として書き出し |

#### 発展: さらに精度を上げるには

1. **電気設備の配置** — Step 3-4の電気図面の読み方に従い、コンセント・スイッチ・照明を壁付けitemとして配置
2. **下がり天井の再現** — `CH=2200` などの記載がある部屋は `ceiling` ノードで個別に天井を作る
3. **敷地・外構** — 配置図から敷地形状・駐車場・アプローチを`slab`と`fence`で追加
4. **太陽光パネルの分割表現** — `solar-panel` ノードで実際の枚数・配列を再現
5. **家具の実物寸法化** — 実際に購入する家具のサイズで`scale`を調整すれば、家具配置シミュレーションになる

---

## 実際に作ってみた結果

私の家（一条工務店 GRAND SMART 平屋・埼玉県狭山市）で実際にやってみた結果です。

<img src="/img/floorplan-3d-ai/exterior-north.jpg" alt="3Dモデルの北側外観。玄関ドア、トイレの小窓、防音室の窓、玄関ポーチの下屋、招き屋根の段差" loading="lazy" style={{ width: '100%', borderRadius: 8, border: '1px solid #e2e8f0', margin: '1rem 0 0.4rem' }} />
<p style={{ fontSize: '0.85rem', color: '#6b7280', margin: '0 0 1.6rem', textAlign: 'center' }}>北面（玄関側）。玄関ポーチの下屋、招き屋根の段差、窓の位置と大きさも図面どおりです</p>

<img src="/img/floorplan-3d-ai/floorplan-2d.jpg" alt="生成した3Dモデルの2D平面表示。LDK22.42帖、洋室2、防音室、脱衣室、浴室、押入B、トイレ、玄関ホール、収納の各室名と家具の配置" loading="lazy" style={{ width: '100%', borderRadius: 8, border: '1px solid #e2e8f0', margin: '1rem 0 0.4rem' }} />
<p style={{ fontSize: '0.85rem', color: '#6b7280', margin: '0 0 1.6rem', textAlign: 'center' }}>2D表示に切り替えると平面図として確認できます。部屋名・帖数・天井高・家具・建具まで入っています</p>

<img src="/img/floorplan-3d-ai/interior-cutaway.jpg" alt="壁を低くして室内を見せた3Dモデル。フローリング、ダイニングテーブル、観葉植物、ベッドなどの家具が配置されている" loading="lazy" style={{ width: '100%', borderRadius: 8, border: '1px solid #e2e8f0', margin: '1rem 0 0.4rem' }} />
<p style={{ fontSize: '0.85rem', color: '#6b7280', margin: '0 0 1.6rem', textAlign: 'center' }}>壁を下げる表示にすると内部が見えます。図面の設備記号に対応する家具を配置しています</p>

**再現できたもの**

- 全部屋の位置と広さ（LDK 22.42帖、防音室・洋室 各4.5帖、1.25坪バス、脱衣室、玄関ホール、トイレ、押入、収納）
- 全開口部（大開口スライディング、高窓、たてすべり出し窓×4、玄関ドア、室内建具6箇所）
- 3.5寸勾配の招き屋根（段違い片流れ）、軒の出、妻壁
- ハイドロテクトタイル・ブラックの外壁、太陽光一体屋根
- ウッドデッキ、玄関ポーチ
- 家具・設備21点（キッチン、カップボード、冷蔵庫、浴槽、洗面台、トイレ、洗濯機、ピアノ、ベッド、ソファ等）

**ノード数**: 78（壁21・ドア7・窓10・部屋9・屋根3・家具21ほか）
**所要時間**: 環境構築込みで約2時間

**まだできていないこと**

- 電気設備（コンセント・スイッチ・照明）の配置
- 下がり天井（CH=2200等）・垂れ壁
- 太陽光パネルの枚数分割表現

---

## なぜこの方法がいいのか

「間取り図 → 3D」はAIで自動変換するツールもありますが（FloorplanToBlender3d、CubiCasa5Kなど）、いずれも欧米の間取りで学習されており、日本の尺モジュール・一条の図面記号では精度が出ません。

一方この方法は、**910mmグリッドという日本の住宅図面の性質を利用して、寸法推定を一切せずに座標を確定させる**アプローチです。グリッドを数えるだけなので、AIが図面を読み間違えるリスクが大幅に下がります。

商用ツールではFloorplanner（FML形式のAPI公開）が唯一の対抗馬ですが、無料枠は透かし付き・低解像度で、本格利用は有料前提です。今回使ったpascalorg/editorはMITライセンスの完全無料OSSで、すべてローカルで完結します。

---

## おわりに

家づくり中の方にとって、間取り図って何度も見返すものだと思います。でも2次元の図面だけだと、実際の空間の広がりや、窓から見える景色、家具を置いたときの余白って、なかなかイメージしきれないんですよね。

3Dにしてぐるぐる回してみると、「ここの動線ちょっと狭いかも」「この窓の位置だと隣家と目線が合うな」みたいな気づきが出てきます。打ち合わせ前にやっておくと、質問の質が変わってくるはずです。

この記事のURLをAIに渡すだけなので、よかったら試してみてください。
