户型图生成edit icon

创建者:
邓朝元
Fork(复制)
下载
嵌入
BUG反馈
index.html
index.html
            
            <!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能户型图生成器 v2.0</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    font-family: -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    color: #2c3e50;
  }
  .app {
    max-width: 1600px;
    margin: 0 auto;
    padding: 20px;
  }
  header {
    text-align: center;
    color: #fff;
    margin-bottom: 20px;
  }
  header h1 {
    font-size: 2em;
    margin-bottom: 6px;
    text-shadow: 0 2px 10px rgba(0,0,0,0.2);
  }
  header p { opacity: 0.9; font-size: 0.95em; }
  .main-grid {
    display: grid;
    grid-template-columns: 280px 1fr 280px;
    gap: 16px;
  }
  @media (max-width: 1200px) {
    .main-grid { grid-template-columns: 1fr; }
  }
  .panel {
    background: rgba(255,255,255,0.97);
    border-radius: 14px;
    padding: 18px;
    box-shadow: 0 8px 32px rgba(0,0,0,0.12);
    backdrop-filter: blur(10px);
  }
  .panel h3 {
    font-size: 1em;
    margin-bottom: 12px;
    color: #667eea;
    border-bottom: 2px solid #f0f0f0;
    padding-bottom: 8px;
    display: flex;
    align-items: center;
    gap: 6px;
  }
  .form-group { margin-bottom: 12px; }
  .form-group label {
    display: block;
    font-size: 0.8em;
    color: #666;
    margin-bottom: 4px;
    font-weight: 500;
  }
  .form-group select,
  .form-group input {
    width: 100%;
    padding: 8px 10px;
    border: 1px solid #e0e0e0;
    border-radius: 8px;
    font-size: 0.9em;
    background: #fafafa;
    transition: all 0.2s;
  }
  .form-group select:focus,
  .form-group input:focus {
    outline: none;
    border-color: #667eea;
    background: #fff;
  }
  .checkbox-group {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
  }
  .checkbox-group label {
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: 0.85em;
    cursor: pointer;
    padding: 4px 8px;
    background: #f5f5f5;
    border-radius: 6px;
    transition: all 0.2s;
  }
  .checkbox-group label:hover { background: #eef; }
  .checkbox-group input { width: auto; }
  .btn {
    width: 100%;
    padding: 10px;
    border: none;
    border-radius: 8px;
    font-size: 0.9em;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.2s;
    margin-bottom: 8px;
  }
  .btn-primary {
    background: linear-gradient(135deg, #667eea, #764ba2);
    color: #fff;
  }
  .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(102,126,234,0.4); }
  .btn-secondary {
    background: #f0f0f0;
    color: #333;
  }
  .btn-secondary:hover { background: #e0e0e0; }
  .btn-group { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
  .canvas-wrap {
    background: #fff;
    border-radius: 14px;
    padding: 16px;
    box-shadow: 0 8px 32px rgba(0,0,0,0.12);
    position: relative;
    min-height: 600px;
  }
  canvas {
    display: block;
    margin: 0 auto;
    max-width: 100%;
    cursor: crosshair;
    background: #fafafa;
    border-radius: 8px;
  }
  .tooltip {
    position: absolute;
    background: rgba(0,0,0,0.85);
    color: #fff;
    padding: 8px 12px;
    border-radius: 8px;
    font-size: 0.85em;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.15s;
    z-index: 100;
    max-width: 200px;
  }
  .tooltip.show { opacity: 1; }
  .stats { font-size: 0.85em; }
  .stat-row {
    display: flex;
    justify-content: space-between;
    padding: 6px 0;
    border-bottom: 1px dashed #eee;
  }
  .stat-row:last-child { border-bottom: none; }
  .stat-label { color: #666; }
  .stat-value { font-weight: 600; color: #2c3e50; }
  .pie-chart {
    width: 100%;
    height: 160px;
    margin: 10px 0;
  }
  .room-list {
    max-height: 280px;
    overflow-y: auto;
    font-size: 0.85em;
  }
  .room-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 6px 8px;
    border-radius: 6px;
    margin-bottom: 3px;
    cursor: pointer;
    transition: all 0.15s;
  }
  .room-item:hover { background: #f5f5f5; }
  .room-item.active { background: #eef; }
  .room-color {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 3px;
    margin-right: 6px;
    vertical-align: middle;
  }
  .legend {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 10px;
    font-size: 0.75em;
  }
  .legend-item {
    display: flex;
    align-items: center;
    gap: 3px;
    padding: 2px 6px;
    background: #f5f5f5;
    border-radius: 4px;
  }
  .badge {
    display: inline-block;
    padding: 2px 8px;
    background: #667eea;
    color: #fff;
    border-radius: 10px;
    font-size: 0.75em;
    font-weight: 600;
  }
  .alert {
    padding: 10px;
    background: #fff3cd;
    border-left: 3px solid #ffc107;
    border-radius: 4px;
    font-size: 0.8em;
    margin-bottom: 10px;
  }
  .seed-display {
    font-family: monospace;
    background: #f5f5f5;
    padding: 4px 8px;
    border-radius: 4px;
    font-size: 0.8em;
    color: #667eea;
  }
  @media print {
    body { background: #fff; }
    .panel:not(.canvas-wrap) { display: none; }
    .main-grid { grid-template-columns: 1fr; }
    header { color: #000; }
  }
</style>
</head>
<body>
<div class="app">
  <header>
    <h1>🏗️ 智能户型图生成器 v2.0</h1>
    <p>基于真实户型逻辑 · 模板驱动 · 邻接规则验证</p>
  </header>

  <div class="main-grid">
    <!-- 左侧控制面板 -->
    <aside class="panel">
      <h3>⚙️ 户型配置</h3>
      <div class="form-group">
        <label>户型类型</label>
        <select id="houseType">
          <option value="one-bed">一室一厅</option>
          <option value="two-bed" selected>两室一厅</option>
          <option value="three-bed">三室一厅</option>
          <option value="three-bed-two">三室两厅</option>
        </select>
      </div>
      <div class="form-group">
        <label>布局模板</label>
        <select id="template">
          <option value="classic">经典南北通透</option>
          <option value="compact">紧凑实用</option>
          <option value="luxury">豪华舒适</option>
        </select>
      </div>
      <div class="form-group">
        <label>变化方式</label>
        <select id="variation">
          <option value="auto">自动随机</option>
          <option value="normal">标准</option>
          <option value="mirror">镜像翻转</option>
          <option value="swap">同类互换</option>
        </select>
      </div>
      <div class="form-group">
        <label>装修风格</label>
        <select id="theme">
          <option value="modern">现代简约</option>
          <option value="chinese">新中式</option>
          <option value="nordic">北欧</option>
          <option value="minimal">极简</option>
        </select>
      </div>
      <div class="form-group">
        <label>显示选项</label>
        <div class="checkbox-group">
          <label><input type="checkbox" id="showFurniture" checked> 家具</label>
          <label><input type="checkbox" id="showDimensions" checked> 尺寸</label>
          <label><input type="checkbox" id="showGrid"> 网格</label>
          <label><input type="checkbox" id="showFlow"> 动线</label>
          <label><input type="checkbox" id="showDoors" checked> 门窗</label>
          <label><input type="checkbox" id="showLabels" checked> 标注</label>
        </div>
      </div>
      <button class="btn btn-primary" onclick="regenerate()">🎲 重新生成</button>
      <div class="btn-group">
        <button class="btn btn-secondary" onclick="saveLayout()">💾 保存</button>
        <button class="btn btn-secondary" onclick="loadLayout()">📂 加载</button>
      </div>
      <button class="btn btn-secondary" onclick="exportPNG()">🖼️ 导出PNG</button>
      <button class="btn btn-secondary" onclick="window.print()">🖨️ 打印</button>

      <h3 style="margin-top:16px;">🎯 当前种子</h3>
      <div class="seed-display" id="seedDisplay">--</div>
    </aside>

    <!-- 中间画布 -->
    <main class="canvas-wrap">
      <canvas id="canvas" width="900" height="680"></canvas>
      <div class="tooltip" id="tooltip"></div>
      <div class="legend" id="legend"></div>
    </main>

    <!-- 右侧信息面板 -->
    <aside class="panel">
      <h3>📊 户型信息</h3>
      <div class="stats" id="stats">
        <div class="stat-row"><span class="stat-label">总面积</span><span class="stat-value" id="sTotal">--</span></div>
        <div class="stat-row"><span class="stat-label">套内面积</span><span class="stat-value" id="sInner">--</span></div>
        <div class="stat-row"><span class="stat-label">得房率</span><span class="stat-value" id="sRate">--</span></div>
        <div class="stat-row"><span class="stat-label">卧室数</span><span class="stat-value" id="sBed">--</span></div>
        <div class="stat-row"><span class="stat-label">卫生间</span><span class="stat-value" id="sBath">--</span></div>
        <div class="stat-row"><span class="stat-label">布局类型</span><span class="stat-value" id="sLayout">--</span></div>
        <div class="stat-row"><span class="stat-label">朝向</span><span class="stat-value" id="sOrient">南北通透</span></div>
      </div>

      <h3 style="margin-top:16px;">🥧 面积分布</h3>
      <canvas class="pie-chart" id="pieChart" width="240" height="160"></canvas>

      <h3 style="margin-top:16px;">🏠 房间列表</h3>
      <div class="room-list" id="roomList"></div>
    </aside>
  </div>
</div>

<script>
// ========== 主题配色 ==========
const THEMES = {
  modern: {
    '主卧': '#E8B4BC', '次卧': '#B8D4E3', '次卧1': '#B8D4E3', '次卧2': '#C5D5E4',
    '客厅': '#F4E4BC', '餐厅': '#E8D5B7', '厨房': '#D4B896',
    '卫生间': '#A8D5E2', '卫': '#A8D5E2', '主卫': '#89C4D9',
    '阳台': '#C8E6C9', '走廊': '#E0E0E0', '玄关': '#FFE0B2',
    '衣帽': '#E1BEE7', '储物': '#D7CCC8'
  },
  chinese: {
    '主卧': '#D4A574', '次卧': '#B8956A', '次卧1': '#B8956A', '次卧2': '#C4A07A',
    '客厅': '#C9302C', '餐厅': '#A52A2A', '厨房': '#8B4513',
    '卫生间': '#4682B4', '卫': '#4682B4', '主卫': '#5F9EA0',
    '阳台': '#6B8E23', '走廊': '#D2B48C', '玄关': '#CD853F',
    '衣帽': '#8B0000', '储物': '#A0522D'
  },
  nordic: {
    '主卧': '#F5E6D3', '次卧': '#E8D5C4', '次卧1': '#E8D5C4', '次卧2': '#EDDCCE',
    '客厅': '#FFF8E7', '餐厅': '#F0E4D0', '厨房': '#D4C5A9',
    '卫生间': '#B5C7D3', '卫': '#B5C7D3', '主卫': '#9DB4C0',
    '阳台': '#C1D5B0', '走廊': '#E8E8E8', '玄关': '#E8D4B8',
    '衣帽': '#D4B8C8', '储物': '#C9B8A8'
  },
  minimal: {
    '主卧': '#F0F0F0', '次卧': '#E8E8E8', '次卧1': '#E8E8E8', '次卧2': '#ECECEC',
    '客厅': '#FAFAFA', '餐厅': '#F5F5F5', '厨房': '#DCDCDC',
    '卫生间': '#D0D8E0', '卫': '#D0D8E0', '主卫': '#C0C8D0',
    '阳台': '#E0E8D8', '走廊': '#E5E5E5', '玄关': '#EBE5DC',
    '衣帽': '#E0D8E0', '储物': '#DDD8D0'
  }
};

// ========== 户型模板 (基于真实户型逻辑设计) ==========
// 每个模板是二维网格,同名字符合并为同一房间
// 设计原则:玄关在入口、厨房邻餐厅、主卫通主卧、阳台靠外墙、走廊连通各室
const TEMPLATES = {
  'one-bed': {
    classic: [
      ['主卧','主卧','主卧','卫','卫','客厅'],
      ['主卧','主卧','主卧','卫','卫','客厅'],
      ['主卧','主卧','主卧','走廊','走廊','客厅'],
      ['厨房','厨房','餐厅','餐厅','走廊','客厅'],
      ['厨房','厨房','餐厅','餐厅','客厅','客厅'],
      ['玄关','玄关','玄关','玄关','阳台','阳台'],
    ]
  },
  'two-bed': {
    classic: [
      ['主卧','主卧','主卧','卫','卫','次卧','次卧','次卧'],
      ['主卧','主卧','主卧','卫','卫','次卧','次卧','次卧'],
      ['主卧','主卧','主卧','走廊','走廊','走廊','走廊','走廊'],
      ['厨房','厨房','餐厅','餐厅','走廊','客厅','客厅','客厅'],
      ['厨房','厨房','餐厅','餐厅','走廊','客厅','客厅','阳台'],
      ['玄关','玄关','玄关','餐厅','走廊','客厅','客厅','阳台'],
      ['玄关','玄关','玄关','玄关','玄关','阳台','阳台','阳台'],
    ],
    compact: [
      ['主卧','主卧','主卧','卫','次卧','次卧','次卧'],
      ['主卧','主卧','主卧','卫','次卧','次卧','次卧'],
      ['走廊','走廊','走廊','走廊','走廊','走廊','走廊'],
      ['厨房','餐厅','餐厅','客厅','客厅','客厅','阳台'],
      ['厨房','餐厅','餐厅','客厅','客厅','客厅','阳台'],
      ['玄关','玄关','玄关','客厅','客厅','阳台','阳台'],
    ],
    luxury: [
      ['主卧','主卧','主卧','主卫','走廊','次卧','次卧','卫'],
      ['主卧','主卧','主卧','主卫','走廊','次卧','次卧','卫'],
      ['主卧','主卧','衣帽','衣帽','走廊','次卧','次卧','卫'],
      ['厨房','厨房','餐厅','餐厅','走廊','走廊','走廊','走廊'],
      ['厨房','厨房','餐厅','餐厅','客厅','客厅','客厅','阳台'],
      ['厨房','厨房','餐厅','餐厅','客厅','客厅','客厅','阳台'],
      ['储物','玄关','玄关','玄关','客厅','客厅','客厅','阳台'],
      ['储物','储物','玄关','玄关','阳台','阳台','阳台','阳台'],
    ]
  },
  'three-bed': {
    classic: [
      ['主卧','主卧','主卧','主卫','走廊','次卧1','次卧1','次卧2','次卧2','卫'],
      ['主卧','主卧','主卧','主卫','走廊','次卧1','次卧1','次卧2','次卧2','卫'],
      ['主卧','主卧','主卧','走廊','走廊','走廊','走廊','走廊','走廊','走廊'],
      ['厨房','厨房','餐厅','餐厅','走廊','客厅','客厅','客厅','客厅','阳台'],
      ['厨房','厨房','餐厅','餐厅','走廊','客厅','客厅','客厅','客厅','阳台'],
      ['玄关','玄关','餐厅','餐厅','走廊','客厅','客厅','客厅','客厅','阳台'],
      ['玄关','玄关','玄关','玄关','玄关','阳台','阳台','阳台','阳台','阳台'],
    ],
    compact: [
      ['主卧','主卧','主卧','卫','次卧1','次卧1','次卧2','次卧2'],
      ['主卧','主卧','主卧','卫','次卧1','次卧1','次卧2','次卧2'],
      ['走廊','走廊','走廊','走廊','走廊','走廊','走廊','走廊'],
      ['厨房','餐厅','餐厅','客厅','客厅','客厅','客厅','阳台'],
      ['厨房','餐厅','餐厅','客厅','客厅','客厅','客厅','阳台'],
      ['玄关','玄关','玄关','客厅','客厅','阳台','阳台','阳台'],
    ],
    luxury: [
      ['主卧','主卧','主卧','主卧','主卫','走廊','次卧1','次卧1','次卧2','次卧2','卫'],
      ['主卧','主卧','主卧','主卧','主卫','走廊','次卧1','次卧1','次卧2','次卧2','卫'],
      ['主卧','主卧','衣帽','衣帽','主卧','走廊','次卧1','次卧1','次卧2','次卧2','卫'],
      ['厨房','厨房','厨房','餐厅','餐厅','走廊','走廊','走廊','走廊','走廊','走廊'],
      ['厨房','厨房','厨房','餐厅','餐厅','客厅','客厅','客厅','客厅','客厅','阳台'],
      ['厨房','厨房','厨房','餐厅','餐厅','客厅','客厅','客厅','客厅','客厅','阳台'],
      ['储物','玄关','玄关','玄关','餐厅','客厅','客厅','客厅','客厅','客厅','阳台'],
      ['储物','储物','玄关','玄关','玄关','阳台','阳台','阳台','阳台','阳台','阳台'],
    ]
  },
  'three-bed-two': {
    classic: [
      ['主卧','主卧','主卧','主卧','主卫','主卫','走廊','次卧1','次卧1','次卧2','次卧2','卫'],
      ['主卧','主卧','主卧','主卧','主卫','主卫','走廊','次卧1','次卧1','次卧2','次卧2','卫'],
      ['主卧','主卧','主卧','衣帽','衣帽','主卧','走廊','次卧1','次卧1','次卧2','次卧2','卫'],
      ['厨房','厨房','厨房','餐厅','餐厅','餐厅','走廊','走廊','走廊','走廊','走廊','走廊'],
      ['厨房','厨房','厨房','餐厅','餐厅','餐厅','客厅','客厅','客厅','客厅','客厅','阳台'],
      ['厨房','厨房','厨房','餐厅','餐厅','餐厅','客厅','客厅','客厅','客厅','客厅','阳台'],
      ['储物','玄关','玄关','玄关','餐厅','餐厅','客厅','客厅','客厅','客厅','客厅','阳台'],
      ['储物','储物','玄关','玄关','玄关','玄关','阳台','阳台','阳台','阳台','阳台','阳台'],
    ]
  }
};

// ========== 邻接规则 (用于验证) ==========
// 必须邻接: 两房间必须共享一段墙
const MUST_ADJACENT = [
  ['厨房','餐厅'],
  ['玄关','客厅'],
  ['玄关','走廊'],
  ['主卫','主卧'],
];
// 允许邻接: 可以但不是必须
const ALLOW_ADJACENT = [
  ['客厅','餐厅'], ['客厅','走廊'], ['客厅','阳台'],
  ['主卧','走廊'], ['次卧','走廊'], ['次卧1','走廊'], ['次卧2','走廊'],
  ['卫','走廊'], ['卫生间','走廊'],
  ['厨房','玄关'], ['储物','玄关'], ['储物','厨房'],
  ['主卧','阳台'], ['次卧','阳台'], ['次卧1','阳台'], ['次卧2','阳台'],
  ['衣帽','主卧'],
];

// ========== 状态 ==========
let currentPlan = null;
let selectedRoom = null;
let seed = 0;

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const tooltip = document.getElementById('tooltip');

// ========== 伪随机生成器 ==========
function mulberry32(a) {
  return function() {
    a |= 0; a = a + 0x6D2B79F5 | 0;
    let t = Math.imul(a ^ a >>> 15, 1 | a);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  }
}
let rng = mulberry32(Date.now());

// ========== 核心: 网格解析为房间 ==========
function parseGridToRooms(grid, cellW, cellH, offsetX, offsetY) {
  const rows = grid.length, cols = grid[0].length;
  const visited = Array(rows).fill(0).map(() => Array(cols).fill(false));
  const rooms = [];

  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      if (visited[r][c] || grid[r][c] === '-') continue;
      const type = grid[r][c];
      // BFS找所有相连的同类型格子
      const queue = [[r, c]];
      visited[r][c] = true;
      let minR = r, maxR = r, minC = c, maxC = c;
      const cells = [];
      while (queue.length) {
        const [cr, cc] = queue.shift();
        cells.push([cr, cc]);
        minR = Math.min(minR, cr); maxR = Math.max(maxR, cr);
        minC = Math.min(minC, cc); maxC = Math.max(maxC, cc);
        for (const [dr, dc] of [[0,1],[0,-1],[1,0],[-1,0]]) {
          const nr = cr+dr, nc = cc+dc;
          if (nr>=0 && nr<rows && nc>=0 && nc<cols && !visited[nr][nc] && grid[nr][nc] === type) {
            visited[nr][nc] = true;
            queue.push([nr, nc]);
          }
        }
      }
      rooms.push({
        type, cells,
        gridMinR: minR, gridMaxR: maxR,
        gridMinC: minC, gridMaxC: maxC,
        x: offsetX + minC * cellW,
        y: offsetY + minR * cellH,
        w: (maxC - minC + 1) * cellW,
        h: (maxR - minR + 1) * cellH,
        gridW: maxC - minC + 1,
        gridH: maxR - minR + 1
      });
    }
  }
  return rooms;
}

// ========== 应用变化 ==========
function applyVariation(grid, variation) {
  const g = grid.map(row => [...row]);
  if (variation === 'mirror') {
    return g.map(row => [...row].reverse());
  }
  if (variation === 'swap') {
    // 互换次卧1和次卧2
    return g.map(row => row.map(c => {
      if (c === '次卧1') return '次卧2';
      if (c === '次卧2') return '次卧1';
      return c;
    }));
  }
  if (variation === 'auto') {
    const options = ['normal', 'mirror', 'swap'];
    const pick = options[Math.floor(rng() * options.length)];
    return applyVariation(g, pick);
  }
  return g;
}

// ========== 邻接验证 ==========
function checkAdjacency(rooms, aType, bType) {
  const a = rooms.find(r => r.type === aType);
  const b = rooms.find(r => r.type === bType);
  if (!a || !b) return true; // 房间不存在则跳过
  // 检查两个房间是否共享一段墙
  // 通过检查是否有相邻的格子
  for (const [ar, ac] of a.cells) {
    for (const [br, bc] of b.cells) {
      if ((Math.abs(ar-br) === 1 && ac === bc) || (Math.abs(ac-bc) === 1 && ar === br)) {
        return true;
      }
    }
  }
  return false;
}

function validateLayout(rooms) {
  const issues = [];
  for (const [a, b] of MUST_ADJACENT) {
    const aExists = rooms.some(r => r.type === a);
    const bExists = rooms.some(r => r.type === b);
    if (aExists && bExists && !checkAdjacency(rooms, a, b)) {
      issues.push(`${a} 应与 ${b} 相邻`);
    }
  }
  // 检查阳台必须靠外墙
  const balcony = rooms.find(r => r.type === '阳台');
  if (balcony) {
    // 外墙 = 至少一个边在网格边界
    const plan = currentPlan;
    const isOuter = balcony.gridMinR === 0 || balcony.gridMaxR === plan.gridRows - 1 ||
                    balcony.gridMinC === 0 || balcony.gridMaxC === plan.gridCols - 1;
    if (!isOuter) issues.push('阳台未靠外墙');
  }
  return issues;
}

// ========== 生成布局 ==========
function generateLayout() {
  const houseType = document.getElementById('houseType').value;
  const templateName = document.getElementById('template').value;
  const variation = document.getElementById('variation').value;

  // 选择模板
  let templates = TEMPLATES[houseType];
  let grid = templates[templateName] || templates[Object.keys(templates)[0]];
  if (!grid) {
    // 如果该户型没有对应模板,用第一个
    grid = Object.values(TEMPLATES)[0].classic;
  }

  // 应用变化
  grid = applyVariation(grid, variation);

  const gridRows = grid.length;
  const gridCols = grid[0].length;

  // 计算像素尺寸,留出边距
  const padding = 50;
  const availW = canvas.width - padding * 2;
  const availH = canvas.height - padding * 2;
  const cellW = availW / gridCols;
  const cellH = availH / gridRows;
  const cellSize = Math.min(cellW, cellH);

  // 居中
  const offsetX = (canvas.width - gridCols * cellSize) / 2;
  const offsetY = (canvas.height - gridRows * cellSize) / 2;

  const rooms = parseGridToRooms(grid, cellSize, cellSize, offsetX, offsetY);

  // 计算面积 (假设每个格子 2.5㎡ 基准)
  const cellArea = 2.5;
  rooms.forEach(r => {
    r.cellsCount = r.cells.length;
    r.area = r.cellsCount * cellArea;
  });

  // 根据户型调整面积
  const areaMultipliers = {
    'one-bed': 1.8, 'two-bed': 2.2, 'three-bed': 2.4, 'three-bed-two': 2.8
  };
  const mult = areaMultipliers[houseType] || 2.2;
  rooms.forEach(r => r.area = Math.round(r.cellsCount * mult));

  currentPlan = {
    rooms, grid, gridRows, gridCols,
    cellSize, offsetX, offsetY,
    houseType, templateName,
    totalArea: rooms.reduce((s, r) => s + r.area, 0)
  };

  // 验证
  const issues = validateLayout(rooms);
  if (issues.length > 0) {
    console.warn('布局问题:', issues);
  }

  return rooms;
}

// ========== 渲染 ==========
function draw() {
  if (!currentPlan) return;
  const { rooms, cellSize, offsetX, offsetY, gridRows, gridCols } = currentPlan;
  const theme = THEMES[document.getElementById('theme').value] || THEMES.modern;
  const showFurniture = document.getElementById('showFurniture').checked;
  const showDimensions = document.getElementById('showDimensions').checked;
  const showGrid = document.getElementById('showGrid').checked;
  const showFlow = document.getElementById('showFlow').checked;
  const showDoors = document.getElementById('showDoors').checked;
  const showLabels = document.getElementById('showLabels').checked;

  // 清空画布
  ctx.fillStyle = '#fafafa';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // 绘制网格
  if (showGrid) {
    ctx.strokeStyle = 'rgba(0,0,0,0.05)';
    ctx.lineWidth = 1;
    for (let i = 0; i <= gridCols; i++) {
      ctx.beginPath();
      ctx.moveTo(offsetX + i * cellSize, offsetY);
      ctx.lineTo(offsetX + i * cellSize, offsetY + gridRows * cellSize);
      ctx.stroke();
    }
    for (let i = 0; i <= gridRows; i++) {
      ctx.beginPath();
      ctx.moveTo(offsetX, offsetY + i * cellSize);
      ctx.lineTo(offsetX + gridCols * cellSize, offsetY + i * cellSize);
      ctx.stroke();
    }
  }

  // 绘制房间填充
  rooms.forEach(room => {
    const color = theme[room.type] || '#ddd';
    ctx.fillStyle = selectedRoom === room ? shadeColor(color, -15) : color;
    ctx.fillRect(room.x, room.y, room.w, room.h);

    // 阳台斜纹
    if (room.type === '阳台') {
      ctx.save();
      ctx.beginPath();
      ctx.rect(room.x, room.y, room.w, room.h);
      ctx.clip();
      ctx.strokeStyle = 'rgba(0,100,0,0.15)';
      ctx.lineWidth = 1;
      for (let i = -room.h; i < room.w + room.h; i += 10) {
        ctx.beginPath();
        ctx.moveTo(room.x + i, room.y);
        ctx.lineTo(room.x + i - room.h, room.y + room.h);
        ctx.stroke();
      }
      ctx.restore();
    }
  });

  // 绘制墙体 (只在相邻不同房间或外墙处画)
  ctx.strokeStyle = '#1a1a1a';
  ctx.lineWidth = 3;
  ctx.lineCap = 'square';

  // 外墙
  ctx.strokeRect(offsetX, offsetY, gridCols * cellSize, gridRows * cellSize);

  // 内墙 (遍历格子边界)
  ctx.lineWidth = 2;
  const grid = currentPlan.grid;
  for (let r = 0; r < gridRows; r++) {
    for (let c = 0; c < gridCols; c++) {
      const cur = grid[r][c];
      if (cur === '-') continue;
      // 右边界
      if (c < gridCols - 1 && grid[r][c+1] !== cur && grid[r][c+1] !== '-') {
        const x = offsetX + (c + 1) * cellSize;
        const y = offsetY + r * cellSize;
        ctx.beginPath();
        ctx.moveTo(x, y);
        ctx.lineTo(x, y + cellSize);
        ctx.stroke();
      }
      // 下边界
      if (r < gridRows - 1 && grid[r+1][c] !== cur && grid[r+1][c] !== '-') {
        const x = offsetX + c * cellSize;
        const y = offsetY + (r + 1) * cellSize;
        ctx.beginPath();
        ctx.moveTo(x, y);
        ctx.lineTo(x + cellSize, y);
        ctx.stroke();
      }
    }
  }

  // 绘制门窗
  if (showDoors) {
    drawDoorsAndWindows(rooms);
  }

  // 绘制家具
  if (showFurniture) {
    rooms.forEach(room => drawFurniture(room, theme));
  }

  // 绘制标注
  if (showLabels) {
    rooms.forEach(room => {
      const cx = room.x + room.w / 2;
      const cy = room.y + room.h / 2;
      ctx.fillStyle = '#2c3e50';
      ctx.font = `bold ${Math.max(11, Math.min(16, room.w / 6))}px "Microsoft YaHei", sans-serif`;
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(room.type, cx, cy - 8);
      ctx.font = `${Math.max(9, Math.min(12, room.w / 8))}px sans-serif`;
      ctx.fillStyle = '#666';
      ctx.fillText(`${room.area}㎡`, cx, cy + 10);
    });
  }

  // 绘制尺寸
  if (showDimensions) {
    drawDimensions(rooms);
  }

  // 绘制动线
  if (showFlow) {
    drawFlowPath(rooms);
  }

  // 绘制指北针
  drawCompass();

  // 绘制比例尺
  drawScale();

  // 更新侧边栏
  updateInfoPanel();
  updateRoomList();
  updateLegend(rooms, theme);
  drawPieChart(rooms, theme);
}

// ========== 绘制门窗 ==========
function drawDoorsAndWindows(rooms) {
  const { grid, cellSize, offsetX, offsetY, gridRows, gridCols } = currentPlan;

  // 绘制门: 在相邻房间共享墙处画开门弧线
  const doorPairs = [
    ['玄关','客厅'], ['玄关','走廊'], ['客厅','餐厅'], ['客厅','走廊'],
    ['走廊','主卧'], ['走廊','次卧'], ['走廊','次卧1'], ['走廊','次卧2'],
    ['走廊','卫'], ['走廊','卫生间'], ['主卧','主卫'], ['主卧','衣帽'],
    ['厨房','餐厅'], ['客厅','阳台'], ['主卧','阳台']
  ];

  const drawnDoors = new Set();
  for (const [a, b] of doorPairs) {
    const ra = rooms.find(r => r.type === a);
    const rb = rooms.find(r => r.type === b);
    if (!ra || !rb) continue;

    // 查找共享墙段
    const segments = findSharedSegments(ra, rb);
    for (const seg of segments) {
      const key = `${Math.round(seg.x1)},${Math.round(seg.y1)}-${Math.round(seg.x2)},${Math.round(seg.y2)}`;
      if (drawnDoors.has(key)) continue;
      drawnDoors.add(key);
      drawDoorArc(seg);
      break; // 每对房间只画一个门
    }
  }

  // 绘制窗户: 在靠外墙的客厅/卧室/阳台处
  const windowRooms = rooms.filter(r =>
    ['客厅','主卧','次卧','次卧1','次卧2','阳台'].includes(r.type));
  for (const room of windowRooms) {
    drawWindows(room);
  }

  // 绘制入户门 (玄關外墙)
  const entrance = rooms.find(r => r.type === '玄关');
  if (entrance) {
    drawEntranceDoor(entrance);
  }
}

function findSharedSegments(ra, rb) {
  const segments = [];
  for (const [ar, ac] of ra.cells) {
    for (const [br, bc] of rb.cells) {
      if (ar === br && Math.abs(ac - bc) === 1) {
        // 垂直共享墙
        const wallC = Math.max(ac, bc);
        const { cellSize, offsetX, offsetY } = currentPlan;
        segments.push({
          x1: offsetX + wallC * cellSize,
          y1: offsetY + ar * cellSize,
          x2: offsetX + wallC * cellSize,
          y2: offsetY + (ar + 1) * cellSize,
          vertical: true
        });
      } else if (ac === bc && Math.abs(ar - br) === 1) {
        // 水平共享墙
        const wallR = Math.max(ar, br);
        const { cellSize, offsetX, offsetY } = currentPlan;
        segments.push({
          x1: offsetX + ac * cellSize,
          y1: offsetY + wallR * cellSize,
          x2: offsetX + (ac + 1) * cellSize,
          y2: offsetY + wallR * cellSize,
          vertical: false
        });
      }
    }
  }
  return segments;
}

function drawDoorArc(seg) {
  const doorLen = Math.min(25, Math.abs(seg.vertical ? seg.y2 - seg.y1 : seg.x2 - seg.x1) * 0.6);
  ctx.save();
  ctx.strokeStyle = '#555';
  ctx.lineWidth = 1.5;

  // 清除墙段
  ctx.strokeStyle = '#fafafa';
  ctx.lineWidth = 4;
  ctx.beginPath();
  if (seg.vertical) {
    const mid = (seg.y1 + seg.y2) / 2;
    ctx.moveTo(seg.x1, mid - doorLen/2);
    ctx.lineTo(seg.x1, mid + doorLen/2);
  } else {
    const mid = (seg.x1 + seg.x2) / 2;
    ctx.moveTo(mid - doorLen/2, seg.y1);
    ctx.lineTo(mid + doorLen/2, seg.y1);
  }
  ctx.stroke();

  // 画开门弧线
  ctx.strokeStyle = '#555';
  ctx.lineWidth = 1;
  if (seg.vertical) {
    const mid = (seg.y1 + seg.y2) / 2;
    ctx.beginPath();
    ctx.moveTo(seg.x1, mid - doorLen/2);
    ctx.lineTo(seg.x1 + doorLen, mid - doorLen/2);
    ctx.stroke();
    ctx.beginPath();
    ctx.arc(seg.x1, mid - doorLen/2, doorLen, 0, Math.PI/2);
    ctx.stroke();
  } else {
    const mid = (seg.x1 + seg.x2) / 2;
    ctx.beginPath();
    ctx.moveTo(mid - doorLen/2, seg.y1);
    ctx.lineTo(mid - doorLen/2, seg.y1 - doorLen);
    ctx.stroke();
    ctx.beginPath();
    ctx.arc(mid - doorLen/2, seg.y1, doorLen, -Math.PI/2, 0);
    ctx.stroke();
  }
  ctx.restore();
}

function drawWindows(room) {
  const { cellSize, offsetX, offsetY, gridRows, gridCols } = currentPlan;
  ctx.save();
  ctx.strokeStyle = '#4FC3F7';
  ctx.lineWidth = 3;

  // 检查四条边是否靠外墙
  if (room.gridMinR === 0) {
    // 上边
    const x = room.x + room.w * 0.25;
    const w = room.w * 0.5;
    const y = room.y;
    ctx.fillStyle = '#fafafa';
    ctx.fillRect(x - 2, y - 2, w + 4, 6);
    ctx.beginPath();
    ctx.moveTo(x, y); ctx.lineTo(x + w, y);
    ctx.moveTo(x, y + 3); ctx.lineTo(x + w, y + 3);
    ctx.stroke();
  }
  if (room.gridMaxR === gridRows - 1) {
    const x = room.x + room.w * 0.25;
    const w = room.w * 0.5;
    const y = room.y + room.h;
    ctx.fillStyle = '#fafafa';
    ctx.fillRect(x - 2, y - 4, w + 4, 6);
    ctx.beginPath();
    ctx.moveTo(x, y); ctx.lineTo(x + w, y);
    ctx.moveTo(x, y - 3); ctx.lineTo(x + w, y - 3);
    ctx.stroke();
  }
  if (room.gridMinC === 0) {
    const y = room.y + room.h * 0.25;
    const h = room.h * 0.5;
    const x = room.x;
    ctx.fillStyle = '#fafafa';
    ctx.fillRect(x - 2, y - 2, 6, h + 4);
    ctx.beginPath();
    ctx.moveTo(x, y); ctx.lineTo(x, y + h);
    ctx.moveTo(x + 3, y); ctx.lineTo(x + 3, y + h);
    ctx.stroke();
  }
  if (room.gridMaxC === gridCols - 1) {
    const y = room.y + room.h * 0.25;
    const h = room.h * 0.5;
    const x = room.x + room.w;
    ctx.fillStyle = '#fafafa';
    ctx.fillRect(x - 4, y - 2, 6, h + 4);
    ctx.beginPath();
    ctx.moveTo(x, y); ctx.lineTo(x, y + h);
    ctx.moveTo(x - 3, y); ctx.lineTo(x - 3, y + h);
    ctx.stroke();
  }
  ctx.restore();
}

function drawEntranceDoor(entrance) {
  const { gridRows, gridCols } = currentPlan;
  ctx.save();
  // 找到玄关靠外墙的一边
  let x, y, w, h;
  if (entrance.gridMaxR === gridRows - 1) {
    // 底部外墙
    x = entrance.x + entrance.w * 0.4;
    y = entrance.y + entrance.h;
    w = entrance.w * 0.3;
    ctx.fillStyle = '#8B4513';
    ctx.fillRect(x, y - 3, w, 6);
    ctx.strokeStyle = '#8B4513';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.lineTo(x, y - w);
    ctx.stroke();
    ctx.beginPath();
    ctx.arc(x, y, w, -Math.PI/2, 0);
    ctx.stroke();
    // 标记
    ctx.fillStyle = '#8B4513';
    ctx.font = 'bold 10px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('入户门', x + w/2, y + 18);
  } else if (entrance.gridMinC === 0) {
    x = entrance.x;
    y = entrance.y + entrance.h * 0.4;
    h = entrance.h * 0.3;
    ctx.fillStyle = '#8B4513';
    ctx.fillRect(x - 3, y, 6, h);
    ctx.strokeStyle = '#8B4513';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.lineTo(x + h, y);
    ctx.stroke();
    ctx.beginPath();
    ctx.arc(x, y, h, 0, Math.PI/2);
    ctx.stroke();
  }
  ctx.restore();
}

// ========== 绘制家具 ==========
function drawFurniture(room, theme) {
  ctx.save();
  const cx = room.x + room.w / 2;
  const cy = room.y + room.h / 2;
  ctx.strokeStyle = 'rgba(80,80,80,0.6)';
  ctx.fillStyle = 'rgba(200,200,200,0.4)';
  ctx.lineWidth = 1;

  switch(room.type) {
    case '主卧':
      // 双人床
      drawRect(cx - room.w*0.25, cy - room.h*0.3, room.w*0.5, room.h*0.5);
      // 枕头
      drawRect(cx - room.w*0.2, cy - room.h*0.28, room.w*0.15, room.h*0.08);
      drawRect(cx + room.w*0.05, cy - room.h*0.28, room.w*0.15, room.h*0.08);
      // 床头柜
      drawRect(cx - room.w*0.38, cy - room.h*0.25, room.w*0.1, room.h*0.12);
      drawRect(cx + room.w*0.28, cy - room.h*0.25, room.w*0.1, room.h*0.12);
      break;
    case '次卧': case '次卧1': case '次卧2':
      // 单人床
      drawRect(cx - room.w*0.2, cy - room.h*0.3, room.w*0.4, room.h*0.5);
      drawRect(cx - room.w*0.15, cy - room.h*0.28, room.w*0.3, room.h*0.08);
      // 书桌
      drawRect(cx + room.w*0.15, cy + room.h*0.1, room.w*0.3, room.h*0.15);
      break;
    case '客厅':
      // 沙发 (L型)
      drawRect(cx - room.w*0.35, cy + room.h*0.1, room.w*0.5, room.h*0.15);
      drawRect(cx + room.w*0.15, cy - room.h*0.1, room.w*0.15, room.h*0.35);
      // 茶几
      drawRect(cx - room.w*0.12, cy - room.h*0.05, room.w*0.24, room.h*0.12);
      // 电视柜
      drawRect(cx - room.w*0.3, cy - room.h*0.35, room.w*0.6, room.h*0.08);
      break;
    case '餐厅':
      // 餐桌
      ctx.beginPath();
      ctx.ellipse(cx, cy, room.w*0.22, room.h*0.2, 0, 0, Math.PI*2);
      ctx.fill(); ctx.stroke();
      // 椅子
      for (let i = 0; i < 4; i++) {
        const angle = (i / 4) * Math.PI * 2;
        const px = cx + Math.cos(angle) * room.w * 0.32;
        const py = cy + Math.sin(angle) * room.h * 0.28;
        ctx.beginPath();
        ctx.arc(px, py, 4, 0, Math.PI*2);
        ctx.fill(); ctx.stroke();
      }
      break;
    case '厨房':
      // L型橱柜
      drawRect(room.x + 4, room.y + 4, room.w*0.18, room.h - 8);
      drawRect(room.x + 4, room.y + 4, room.w - 8, room.h*0.18);
      // 灶台
      ctx.beginPath();
      ctx.arc(room.x + room.w*0.4, room.y + room.h*0.12, 4, 0, Math.PI*2);
      ctx.arc(room.x + room.w*0.6, room.y + room.h*0.12, 4, 0, Math.PI*2);
      ctx.fill(); ctx.stroke();
      break;
    case '卫生间': case '卫':
      // 马桶
      drawRect(room.x + room.w*0.6, room.y + room.h*0.5, room.w*0.25, room.h*0.35);
      // 洗手台
      drawRect(room.x + 4, room.y + 4, room.w*0.4, room.h*0.15);
      // 淋浴
      ctx.setLineDash([3, 3]);
      drawRect(room.x + 4, room.y + room.h*0.5, room.w*0.35, room.h*0.4);
      ctx.setLineDash([]);
      break;
    case '主卫':
      // 浴缸
      drawRect(room.x + 4, room.y + 4, room.w*0.6, room.h*0.3);
      // 马桶
      drawRect(room.x + room.w*0.65, room.y + room.h*0.5, room.w*0.25, room.h*0.35);
      // 洗手台
      drawRect(room.x + 4, room.y + room.h*0.5, room.w*0.4, room.h*0.15);
      break;
    case '书房':
      // 书桌
      drawRect(cx - room.w*0.3, cy - room.h*0.1, room.w*0.6, room.h*0.2);
      // 椅子
      ctx.beginPath();
      ctx.arc(cx, cy + room.h*0.15, 6, 0, Math.PI*2);
      ctx.fill(); ctx.stroke();
      // 书架
      drawRect(room.x + 4, room.y + 4, room.w*0.15, room.h - 8);
      break;
    case '阳台':
      // 洗衣机
      drawRect(room.x + room.w*0.1, room.y + room.h*0.2, room.w*0.25, room.h*0.4);
      ctx.beginPath();
      ctx.arc(room.x + room.w*0.225, room.y + room.h*0.4, room.w*0.08, 0, Math.PI*2);
      ctx.stroke();
      // 绿植
      ctx.beginPath();
      ctx.arc(room.x + room.w*0.7, room.y + room.h*0.5, 8, 0, Math.PI*2);
      ctx.fillStyle = 'rgba(100,180,100,0.5)';
      ctx.fill(); ctx.stroke();
      break;
    case '衣帽':
      // 衣柜
      drawRect(room.x + 4, room.y + 4, room.w - 8, room.h*0.2);
      drawRect(room.x + 4, room.y + room.h*0.8 - 4, room.w - 8, room.h*0.2);
      break;
    case '储物':
      // 架子
      for (let i = 0; i < 3; i++) {
        drawRect(room.x + 4, room.y + 4 + i * room.h*0.3, room.w*0.15, room.h*0.25);
      }
      break;
  }
  ctx.restore();

  function drawRect(x, y, w, h) {
    ctx.fillRect(x, y, w, h);
    ctx.strokeRect(x, y, w, h);
  }
}

// ========== 尺寸标注 ==========
function drawDimensions(rooms) {
  const { cellSize } = currentPlan;
  ctx.save();
  ctx.strokeStyle = '#999';
  ctx.fillStyle = '#666';
  ctx.lineWidth = 0.8;
  ctx.font = '9px sans-serif';
  ctx.textAlign = 'center';

  rooms.forEach(room => {
    const wM = (room.gridW * 0.8).toFixed(1); // 假设每格0.8米宽
    const hM = (room.gridH * 0.8).toFixed(1);

    // 宽度
    const dy = room.y - 8;
    ctx.beginPath();
    ctx.moveTo(room.x, dy);
    ctx.lineTo(room.x + room.w, dy);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(room.x, dy - 3); ctx.lineTo(room.x, dy + 3);
    ctx.moveTo(room.x + room.w, dy - 3); ctx.lineTo(room.x + room.w, dy + 3);
    ctx.stroke();
    ctx.fillStyle = '#fafafa';
    ctx.fillRect(room.x + room.w/2 - 12, dy - 8, 24, 10);
    ctx.fillStyle = '#666';
    ctx.fillText(`${wM}m`, room.x + room.w/2, dy);

    // 高度
    const dx = room.x - 8;
    ctx.beginPath();
    ctx.moveTo(dx, room.y);
    ctx.lineTo(dx, room.y + room.h);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(dx - 3, room.y); ctx.lineTo(dx + 3, room.y);
    ctx.moveTo(dx - 3, room.y + room.h); ctx.lineTo(dx + 3, room.y + room.h);
    ctx.stroke();
    ctx.save();
    ctx.translate(dx - 3, room.y + room.h/2);
    ctx.rotate(-Math.PI/2);
    ctx.fillStyle = '#fafafa';
    ctx.fillRect(-12, -8, 24, 10);
    ctx.fillStyle = '#666';
    ctx.fillText(`${hM}m`, 0, 0);
    ctx.restore();
  });
  ctx.restore();
}

// ========== 动线可视化 ==========
function drawFlowPath(rooms) {
  const entrance = rooms.find(r => r.type === '玄关');
  const living = rooms.find(r => r.type === '客厅');
  if (!entrance || !living) return;

  ctx.save();
  ctx.strokeStyle = 'rgba(255, 87, 34, 0.6)';
  ctx.lineWidth = 3;
  ctx.setLineDash([8, 6]);
  ctx.beginPath();
  ctx.moveTo(entrance.x + entrance.w/2, entrance.y + entrance.h/2);
  ctx.lineTo(living.x + living.w/2, living.y + living.h/2);
  ctx.stroke();

  // 箭头
  const angle = Math.atan2(
    living.y + living.h/2 - (entrance.y + entrance.h/2),
    living.x + living.w/2 - (entrance.x + entrance.w/2)
  );
  const ax = living.x + living.w/2 - Math.cos(angle) * 15;
  const ay = living.y + living.h/2 - Math.sin(angle) * 15;
  ctx.setLineDash([]);
  ctx.fillStyle = 'rgba(255, 87, 34, 0.8)';
  ctx.beginPath();
  ctx.moveTo(living.x + living.w/2, living.y + living.h/2);
  ctx.lineTo(ax - Math.sin(angle)*5, ay + Math.cos(angle)*5);
  ctx.lineTo(ax + Math.sin(angle)*5, ay - Math.cos(angle)*5);
  ctx.closePath();
  ctx.fill();

  // 标签
  ctx.fillStyle = 'rgba(255, 87, 34, 0.9)';
  ctx.font = 'bold 11px sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText('主要动线', (entrance.x + living.x)/2 + entrance.w/2,
                          (entrance.y + living.y)/2 + entrance.h/2 - 10);
  ctx.restore();
}

// ========== 指北针 ==========
function drawCompass() {
  const cx = canvas.width - 50, cy = 50, r = 22;
  ctx.save();
  ctx.beginPath();
  ctx.arc(cx, cy, r, 0, Math.PI*2);
  ctx.fillStyle = 'rgba(255,255,255,0.95)';
  ctx.fill();
  ctx.strokeStyle = '#333';
  ctx.lineWidth = 2;
  ctx.stroke();

  // N 箭头
  ctx.beginPath();
  ctx.moveTo(cx, cy - r + 4);
  ctx.lineTo(cx - 5, cy + 2);
  ctx.lineTo(cx, cy - 2);
  ctx.closePath();
  ctx.fillStyle = '#E53935';
  ctx.fill();
  ctx.beginPath();
  ctx.moveTo(cx, cy - r + 4);
  ctx.lineTo(cx + 5, cy + 2);
  ctx.lineTo(cx, cy - 2);
  ctx.closePath();
  ctx.fillStyle = '#FFCDD2';
  ctx.fill();

  ctx.fillStyle = '#E53935';
  ctx.font = 'bold 11px sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText('N', cx, cy - r - 6);
  ctx.fillStyle = '#666';
  ctx.fillText('S', cx, cy + r + 12);
  ctx.restore();
}

// ========== 比例尺 ==========
function drawScale() {
  const x = 30, y = canvas.height - 30;
  const barW = 80;
  ctx.save();
  ctx.strokeStyle = '#333';
  ctx.fillStyle = '#333';
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.moveTo(x, y); ctx.lineTo(x + barW, y);
  ctx.stroke();
  ctx.beginPath();
  ctx.moveTo(x, y - 4); ctx.lineTo(x, y + 4);
  ctx.moveTo(x + barW, y - 4); ctx.lineTo(x + barW, y + 4);
  ctx.moveTo(x + barW/2, y - 2); ctx.lineTo(x + barW/2, y + 2);
  ctx.stroke();
  ctx.fillRect(x, y - 2, barW/2, 4);
  ctx.font = '9px sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText('0', x, y + 14);
  ctx.fillText('1m', x + barW/2, y + 14);
  ctx.fillText('2m', x + barW, y + 14);
  ctx.restore();
}

// ========== 信息面板更新 ==========
function updateInfoPanel() {
  if (!currentPlan) return;
  const { rooms, totalArea, houseType, templateName } = currentPlan;
  const innerArea = Math.round(totalArea * 0.82);
  const rate = ((innerArea / totalArea) * 100).toFixed(1);
  const beds = rooms.filter(r => ['主卧','次卧','次卧1','次卧2'].includes(r.type)).length;
  const baths = rooms.filter(r => ['卫','卫生间','主卫'].includes(r.type)).length;

  document.getElementById('sTotal').textContent = `${totalArea}㎡`;
  document.getElementById('sInner').textContent = `${innerArea}㎡`;
  document.getElementById('sRate').textContent = `${rate}%`;
  document.getElementById('sBed').textContent = `${beds}间`;
  document.getElementById('sBath').textContent = `${baths}间`;

  const layoutNames = {
    'classic': '南北通透',
    'compact': '紧凑实用',
    'luxury': '豪华舒适'
  };
  document.getElementById('sLayout').textContent = layoutNames[templateName] || templateName;
}

function updateRoomList() {
  if (!currentPlan) return;
  const theme = THEMES[document.getElementById('theme').value];
  const list = document.getElementById('roomList');
  list.innerHTML = currentPlan.rooms.map(r => `
    <div class="room-item ${selectedRoom === r ? 'active' : ''}" onclick="selectRoom('${r.type}')">
      <span><span class="room-color" style="background:${theme[r.type]||'#ddd'}"></span>${r.type}</span>
      <span class="badge">${r.area}㎡</span>
    </div>
  `).join('');
}

function updateLegend(rooms, theme) {
  const types = [...new Set(rooms.map(r => r.type))];
  document.getElementById('legend').innerHTML = types.map(t =>
    `<div class="legend-item">
      <span class="room-color" style="background:${theme[t]||'#ddd'}"></span>
      <span>${t}</span>
    </div>`
  ).join('');
}

// ========== 饼图 ==========
function drawPieChart(rooms, theme) {
  const pieCanvas = document.getElementById('pieChart');
  const pctx = pieCanvas.getContext('2d');
  pctx.clearRect(0, 0, pieCanvas.width, pieCanvas.height);

  const total = rooms.reduce((s, r) => s + r.area, 0);
  const cx = 80, cy = 80, r = 60;
  let start = -Math.PI / 2;

  // 合并同类房间
  const grouped = {};
  rooms.forEach(room => {
    const key = room.type.replace(/\d/g, '');
    if (!grouped[key]) grouped[key] = { area: 0, color: theme[room.type] || '#ddd' };
    grouped[key].area += room.area;
  });

  const entries = Object.entries(grouped);
  entries.forEach(([name, data]) => {
    const angle = (data.area / total) * Math.PI * 2;
    pctx.beginPath();
    pctx.moveTo(cx, cy);
    pctx.arc(cx, cy, r, start, start + angle);
    pctx.closePath();
    pctx.fillStyle = data.color;
    pctx.fill();
    pctx.strokeStyle = '#fff';
    pctx.lineWidth = 2;
    pctx.stroke();
    start += angle;
  });

  // 图例
  pctx.font = '10px sans-serif';
  pctx.textAlign = 'left';
  pctx.textBaseline = 'middle';
  entries.slice(0, 8).forEach(([name, data], i) => {
    const lx = 155, ly = 10 + i * 18;
    pctx.fillStyle = data.color;
    pctx.fillRect(lx, ly, 10, 10);
    pctx.strokeStyle = '#999';
    pctx.lineWidth = 0.5;
    pctx.strokeRect(lx, ly, 10, 10);
    pctx.fillStyle = '#333';
    const pct = ((data.area / total) * 100).toFixed(0);
    pctx.fillText(`${name} ${pct}%`, lx + 15, ly + 5);
  });
}

// ========== 房间选择 ==========
function selectRoom(type) {
  selectedRoom = currentPlan.rooms.find(r => r.type === type) || null;
  draw();
}

// ========== 颜色工具 ==========
function shadeColor(color, percent) {
  const num = parseInt(color.replace('#',''), 16);
  const amt = Math.round(2.55 * percent);
  const R = Math.max(0, Math.min(255, (num >> 16) + amt));
  const G = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amt));
  const B = Math.max(0, Math.min(255, (num & 0x0000FF) + amt));
  return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1);
}

// ========== 鼠标交互 ==========
canvas.addEventListener('mousemove', (e) => {
  if (!currentPlan) return;
  const rect = canvas.getBoundingClientRect();
  const mx = (e.clientX - rect.left) * (canvas.width / rect.width);
  const my = (e.clientY - rect.top) * (canvas.height / rect.height);

  const room = currentPlan.rooms.find(r =>
    mx >= r.x && mx <= r.x + r.w && my >= r.y && my <= r.y + r.h);

  if (room) {
    tooltip.innerHTML = `<b>${room.type}</b><br>面积: ${room.area}㎡<br>尺寸: ${(room.gridW*0.8).toFixed(1)}×${(room.gridH*0.8).toFixed(1)}m`;
    tooltip.style.left = (e.clientX - canvas.getBoundingClientRect().left + 10) + 'px';
    tooltip.style.top = (e.clientY - canvas.getBoundingClientRect().top + 10) + 'px';
    tooltip.classList.add('show');
    canvas.style.cursor = 'pointer';
  } else {
    tooltip.classList.remove('show');
    canvas.style.cursor = 'crosshair';
  }
});

canvas.addEventListener('click', (e) => {
  if (!currentPlan) return;
  const rect = canvas.getBoundingClientRect();
  const mx = (e.clientX - rect.left) * (canvas.width / rect.width);
  const my = (e.clientY - rect.top) * (canvas.height / rect.height);
  const room = currentPlan.rooms.find(r =>
    mx >= r.x && mx <= r.x + r.w && my >= r.y && my <= r.y + r.h);
  selectedRoom = room || null;
  draw();
});

// ========== 主控函数 ==========
function regenerate() {
  seed = Math.floor(Math.random() * 1000000);
  rng = mulberry32(seed);
  document.getElementById('seedDisplay').textContent = '#' + seed.toString().padStart(6, '0');
  selectedRoom = null;
  generateLayout();
  draw();
}

function exportPNG() {
  const link = document.createElement('a');
  link.download = `户型图_${currentPlan ? currentPlan.totalArea : 'plan'}㎡_seed${seed}.png`;
  link.href = canvas.toDataURL('image/png');
  link.click();
}

function saveLayout() {
  if (!currentPlan) return;
  const data = {
    seed, houseType: document.getElementById('houseType').value,
    template: document.getElementById('template').value,
    variation: document.getElementById('variation').value,
    theme: document.getElementById('theme').value,
    timestamp: new Date().toISOString()
  };
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const link = document.createElement('a');
  link.download = `户型方案_seed${seed}.json`;
  link.href = URL.createObjectURL(blob);
  link.click();
}

function loadLayout() {
  const input = document.createElement('input');
  input.type = 'file';
  input.accept = '.json';
  input.onchange = (e) => {
    const file = e.target.files[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = (ev) => {
      try {
        const data = JSON.parse(ev.target.result);
        seed = data.seed;
        rng = mulberry32(seed);
        document.getElementById('houseType').value = data.houseType;
        document.getElementById('template').value = data.template;
        document.getElementById('variation').value = data.variation;
        document.getElementById('theme').value = data.theme;
        document.getElementById('seedDisplay').textContent = '#' + seed.toString().padStart(6, '0');
        generateLayout();
        draw();
      } catch (err) {
        alert('文件解析失败: ' + err.message);
      }
    };
    reader.readAsText(file);
  };
  input.click();
}

// 监听变化
document.querySelectorAll('select, input[type="checkbox"]').forEach(el => {
  el.addEventListener('change', () => {
    if (el.id === 'houseType' || el.id === 'template' || el.id === 'variation') {
      generateLayout();
    }
    draw();
  });
});

// 初始化
regenerate();
</script>
</body>
</html>
        
编辑器加载中
预览
控制台