/**
 * Board Renderer - 白板渲染层
 *
 * 职责：
 * - 绘制各种形状元素
 * - 绘制连接线/箭头
 * - 绘制网格背景
 * - 处理视口变换
 * - 绘制选择框/手柄
 *
 * 设计原则：
 * - 纯渲染逻辑，不处理交互
 * - 高性能（脏矩形 + requestAnimationFrame）
 * - 支持 CSS 导出
 */

import type {
  BoardElement, BoardRendererConfig, Viewport, Point, Rect,
  RectangleElement, EllipseElement, DiamondElement,
  TextElement, StickyNoteElement, ArrowElement,
  LineElement, FreehandElement, ImageElement, GroupElement,
  Connection, ResizeHandle, ElementStyle, ConnectionStyle, ArrowEnd, LineStyle,
} from '../core/types'

import { DEFAULT_RENDERER_CONFIG } from '../core/types'
import type { BoardModel } from '../core/BoardModel'

/** 选中状态信息（由 InteractionManager 设置） */
interface SelectionInfo {
  selectedIds: Set<string>
  activeHandle: ResizeHandle | null
  selectionBox: Rect | null
}

/**
 * 白板渲染器
 */
export class BoardRenderer {
  private canvas: HTMLCanvasElement
  private ctx: CanvasRenderingContext2D
  private config: BoardRendererConfig
  private viewport: Viewport = { zoom: 1, panX: 0, panY: 0 }
  private needsRedraw = true

  // 选中状态
  private selectionInfo: SelectionInfo = {
    selectedIds: new Set(),
    activeHandle: null,
    selectionBox: null,
  }

  // 图片缓存
  private imageCache: Map<string, HTMLImageElement> = new Map()

  // 字体度量缓存
  private fontMetricsCache: Map<string, { ascent: number; descent: number; lineGap: number }> = new Map()

  // 当前正在文本编辑的元素 ID（编辑期间不在 canvas 上渲染其文本）
  private editingId: string | null = null

  constructor(canvas: HTMLCanvasElement, config: Partial<BoardRendererConfig> = {}) {
    this.canvas = canvas
    this.config = { ...DEFAULT_RENDERER_CONFIG, ...config }

    const ctx = canvas.getContext('2d')
    if (!ctx) throw new Error('Cannot get 2d context')
    this.ctx = ctx

    this.setupCanvas()
  }

  /** 设置 Canvas 尺寸 */
  private setupCanvas(): void {
    const { width, height, pixelRatio } = this.config
    this.canvas.width = width * pixelRatio
    this.canvas.height = height * pixelRatio
    this.canvas.style.width = `${width}px`
    this.canvas.style.height = `${height}px`
    this.ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0)
  }

  // ===== 坐标变换 =====

  /** 屏幕坐标转世界坐标 */
  screenToWorld(screenPoint: Point): Point {
    return {
      x: (screenPoint.x - this.viewport.panX) / this.viewport.zoom,
      y: (screenPoint.y - this.viewport.panY) / this.viewport.zoom,
    }
  }

  /** 世界坐标转屏幕坐标 */
  worldToScreen(worldPoint: Point): Point {
    return {
      x: worldPoint.x * this.viewport.zoom + this.viewport.panX,
      y: worldPoint.y * this.viewport.zoom + this.viewport.panY,
    }
  }

  /**
   * 返回元素内文字的可视区域边界（世界坐标），用于 textarea 精确定位
   * 用 canvas ctx 真实测量，与 BoardRenderer 渲染逻辑完全一致
   */
  getTextBoundsInElement(board: BoardModel, elementId: string): { startY: number; lineHeight: number; lines: string[] } | null {
    const el = board.getElement(elementId)
    if (!el || !('text' in el) || !(el as any).text) return null

    const text = (el as any).text as string
    const isShape = ['rectangle', 'ellipse', 'diamond', 'sticky-note'].includes(el.type)
    const style = (el as any).style || {}
    const fontSize = style.fontSize ?? (isShape ? 14 : 20)
    const fontWeight = style.fontWeight || '400'
    const align = style.textAlign || (isShape ? 'center' : 'left')
    const baseline = style.textBaseline ?? (isShape ? 'middle' : 'top')
    const padding = 8

    this.ctx.font = `${fontWeight} ${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`
    const maxWidth = el.width - padding * 2
    const lines = this.wrapText(text, maxWidth, fontSize)
    const { lineGap } = this.getFontMetrics(fontSize, fontWeight)
    const lineHeight = fontSize + lineGap

    let startY: number
    if (baseline === 'top') {
      startY = el.y + padding
    } else if (baseline === 'bottom') {
      startY = el.y + el.height - padding
    } else {
      startY = el.y + el.height / 2
    }

    return { startY, lineHeight, lines }
  }

  // ===== 视口 =====

  getViewport(): Viewport {
    return { ...this.viewport }
  }

  getCanvasSize(): { width: number; height: number } {
    return { width: this.config.width, height: this.config.height }
  }

  setViewport(viewport: Partial<Viewport>): void {
    this.viewport = { ...this.viewport, ...viewport }
    this.needsRedraw = true
  }

  // ===== 选择状态 =====

  setSelectionInfo(info: SelectionInfo): void {
    this.selectionInfo = info
    this.needsRedraw = true
  }

  getSelectedIds(): Set<string> {
    return this.selectionInfo.selectedIds
  }

  // ===== 文本编辑状态 =====

  setEditingId(id: string | null): void {
    if (this.editingId !== id) {
      this.editingId = id
      this.needsRedraw = true
    }
  }

  getEditingId(): string | null {
    return this.editingId
  }

  // ===== 主渲染 =====

  /** 主渲染方法 */
  render(board: BoardModel): void {
    if (!this.needsRedraw) return

    const { width, height } = this.config

    // 清空画布
    this.ctx.fillStyle = this.config.backgroundColor || '#f8fafc'
    this.ctx.fillRect(0, 0, width, height)

    // 绘制网格
    this.drawGrid()

    // 应用视口变换
    this.ctx.save()
    this.ctx.translate(this.viewport.panX, this.viewport.panY)
    this.ctx.scale(this.viewport.zoom, this.viewport.zoom)

    // 绘制连接
    board.connections.forEach(conn => {
      this.drawConnection(conn, board)
    })

    // 绘制元素（按 zIndex 排序）
    board.activePageElements.forEach(el => {
      this.drawElement(el, board)
    })

    // 绘制选中框/手柄（编辑状态不显示）
    if (!this.editingId) {
      this.selectionInfo.selectedIds.forEach(id => {
        const el = board.getElement(id)
        if (el) this.drawSelectionHandles(el)
      })
    }

    // 绘制正在拖拽的选择框
    if (this.selectionInfo.selectionBox) {
      this.drawSelectionBox(this.selectionInfo.selectionBox)
    }

    this.ctx.restore()
    this.needsRedraw = false
  }

  // ===== 网格 =====

  private drawGrid(): void {
    const { gridType, gridSize, gridColor } = this.config
    if (gridType === 'none' || !gridType) return

    const { width, height } = this.config
    const { zoom, panX, panY } = this.viewport
    const gs = (gridSize || 20) * zoom

    if (gs < 5) return // 缩放太小不显示网格

    const offsetX = panX % gs
    const offsetY = panY % gs

    this.ctx.strokeStyle = gridColor || '#cbd5e1'

    if (gridType === 'dot') {
      this.ctx.fillStyle = gridColor || '#cbd5e1'
      const dotSize = Math.max(1, 1.5 * zoom)
      for (let x = offsetX; x < width; x += gs) {
        for (let y = offsetY; y < height; y += gs) {
          this.ctx.beginPath()
          this.ctx.arc(x, y, dotSize, 0, Math.PI * 2)
          this.ctx.fill()
        }
      }
    } else if (gridType === 'line') {
      this.ctx.lineWidth = 0.5
      for (let x = offsetX; x < width; x += gs) {
        this.ctx.beginPath()
        this.ctx.moveTo(x, 0)
        this.ctx.lineTo(x, height)
        this.ctx.stroke()
      }
      for (let y = offsetY; y < height; y += gs) {
        this.ctx.beginPath()
        this.ctx.moveTo(0, y)
        this.ctx.lineTo(width, y)
        this.ctx.stroke()
      }
    } else if (gridType === 'cross') {
      this.ctx.lineWidth = 0.5
      const crossSize = 4 * zoom
      for (let x = offsetX; x < width; x += gs) {
        for (let y = offsetY; y < height; y += gs) {
          this.ctx.beginPath()
          this.ctx.moveTo(x - crossSize, y)
          this.ctx.lineTo(x + crossSize, y)
          this.ctx.moveTo(x, y - crossSize)
          this.ctx.lineTo(x, y + crossSize)
          this.ctx.stroke()
        }
      }
    }
  }

  private getLineDash(lineStyle?: LineStyle): number[] {
    switch (lineStyle) {
      case 'dashed':  return [10, 6]
      case 'dotted':  return [2, 4]
      case 'dash-dot': return [10, 4, 2, 4]
      default:         return []
    }
  }

  private applyStroke(style: ElementStyle): void {
    const dash = this.getLineDash(style.lineStyle)
    this.ctx.setLineDash(dash)
  }

  // ===== 元素绘制 =====

  private drawElement(el: BoardElement, board: BoardModel, skipText = false): void {
    // Skip rendering the element currently being edited (textarea takes over)
    // But keep drawing the shape so the textarea has a visible background
    if (el.id === this.editingId) skipText = true

    this.ctx.save()

    // 应用旋转
    if (el.rotation) {
      const cx = el.x + el.width / 2
      const cy = el.y + el.height / 2
      this.ctx.translate(cx, cy)
      this.ctx.rotate(el.rotation)
      this.ctx.translate(-cx, -cy)
    }

    // 应用透明度
    if (el.style.opacity !== undefined && el.style.opacity < 1) {
      this.ctx.globalAlpha = el.style.opacity
    }

    switch (el.type) {
      case 'rectangle': this.drawRectangle(el, skipText); break
      case 'ellipse': this.drawEllipse(el, skipText); break
      case 'diamond': this.drawDiamond(el, skipText); break
      case 'text': this.drawTextElement(el, skipText); break
      case 'sticky-note': this.drawStickyNote(el, skipText); break
      case 'arrow': this.drawArrowElement(el, board); break
      case 'line': this.drawLineElement(el); break
      case 'freehand': this.drawFreehand(el); break
      case 'image': this.drawImageElement(el); break
      case 'group': this.drawGroupElement(el, board); break
    }

    this.ctx.restore()
  }

  /** 绘制矩形 */
  private drawRectangle(el: RectangleElement, skipText = false): void {
    const { x, y, width, height, style } = el
    const radius = Math.min(style.borderRadius ?? 4, width / 2, height / 2)

    // 填充
    if (style.fill && style.fill !== 'transparent') {
      this.ctx.fillStyle = style.fill
      this.roundRect(x, y, width, height, radius)
      this.ctx.fill()
    }

    // 边框
    if (style.stroke && style.stroke !== 'transparent') {
      this.ctx.strokeStyle = style.stroke
      this.ctx.lineWidth = style.strokeWidth ?? 2
      this.applyStroke(style)
      this.roundRect(x, y, width, height, radius)
      this.ctx.stroke()
      this.ctx.setLineDash([])
    }

    // 文本（编辑时跳过，由 textarea 接管）
    if (!skipText && el.text) {
      this.drawElementText(el)
    }
  }

  /** 绘制椭圆 */
  private drawEllipse(el: EllipseElement, skipText = false): void {
    const cx = el.x + el.width / 2
    const cy = el.y + el.height / 2
    const rx = el.width / 2
    const ry = el.height / 2

    this.ctx.beginPath()
    this.ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2)

    if (el.style.fill && el.style.fill !== 'transparent') {
      this.ctx.fillStyle = el.style.fill
      this.ctx.fill()
    }

    if (el.style.stroke && el.style.stroke !== 'transparent') {
      this.ctx.strokeStyle = el.style.stroke
      this.ctx.lineWidth = el.style.strokeWidth ?? 2
      this.applyStroke(el.style)
      this.ctx.stroke()
      this.ctx.setLineDash([])
    }

    // 文本（编辑时跳过，由 textarea 接管）
    if (!skipText && el.text) {
      this.drawElementText(el)
    }
  }

  /** 绘制菱形 */
  private drawDiamond(el: DiamondElement, skipText = false): void {
    const cx = el.x + el.width / 2
    const cy = el.y + el.height / 2
    const hw = el.width / 2
    const hh = el.height / 2

    this.ctx.beginPath()
    this.ctx.moveTo(cx, cy - hh)
    this.ctx.lineTo(cx + hw, cy)
    this.ctx.lineTo(cx, cy + hh)
    this.ctx.lineTo(cx - hw, cy)
    this.ctx.closePath()

    if (el.style.fill && el.style.fill !== 'transparent') {
      this.ctx.fillStyle = el.style.fill
      this.ctx.fill()
    }

    if (el.style.stroke && el.style.stroke !== 'transparent') {
      this.ctx.strokeStyle = el.style.stroke
      this.ctx.lineWidth = el.style.strokeWidth ?? 2
      this.applyStroke(el.style)
      this.ctx.stroke()
      this.ctx.setLineDash([])
    }

    // 文本（编辑时跳过，由 textarea 接管）
    if (!skipText && el.text) {
      this.drawElementText(el)
    }
  }

  /** 绘制文本元素（Excalidraw 风格：透明背景，纯文字）
   *  skipText 控制文本是否跳过（编辑时由 textarea 接管，整个元素含背景都被跳过，
   *  保持 drawElement 中的统一逻辑）
   */
  private drawTextElement(el: TextElement, skipText = false): void {
    if (skipText) return
    const style = el.style
    const fontSize = style.fontSize || 20
    const fontWeight = style.fontWeight || '400'
    const align = style.textAlign || 'left'
    const baseline = style.textBaseline || 'top'

    this.ctx.fillStyle = style.color || '#1e293b'
    this.ctx.font = `${fontWeight} ${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`

    const padding = 8
    const maxWidth = el.width - padding * 2
    const lines = this.wrapText(el.text, maxWidth, fontSize)
    const { lineGap } = this.getFontMetrics(fontSize, fontWeight)
    const lineHeight = fontSize + lineGap

    const textX = align === 'left' ? el.x + padding
      : align === 'right' ? el.x + el.width - padding
      : el.x + el.width / 2

    let startY: number
    if (baseline === 'top') {
      this.ctx.textBaseline = 'top'
      startY = el.y + padding
    } else if (baseline === 'bottom') {
      this.ctx.textBaseline = 'bottom'
      startY = el.y + el.height - padding
    } else {
      this.ctx.textBaseline = 'middle'
      startY = el.y + el.height / 2
    }
    this.ctx.textAlign = align

    lines.forEach((line, i) => {
      this.ctx.fillText(line, textX, startY + i * lineHeight, maxWidth)
    })
  }

  /** 绘制便利贴 */
  private drawStickyNote(el: StickyNoteElement, skipText = false): void {
    const { x, y, width, height, style } = el

    // 阴影
    this.ctx.shadowColor = 'rgba(0,0,0,0.1)'
    this.ctx.shadowBlur = 8
    this.ctx.shadowOffsetX = 2
    this.ctx.shadowOffsetY = 2

    // 便利贴背景
    this.ctx.fillStyle = style.fill || '#fef08a'
    this.roundRect(x, y, width, height, 2)
    this.ctx.fill()

    this.ctx.shadowColor = 'transparent'
    this.ctx.shadowBlur = 0
    this.ctx.shadowOffsetX = 0
    this.ctx.shadowOffsetY = 0

    // 折角
    const foldSize = 16
    this.ctx.fillStyle = style.stroke || '#eab308'
    this.ctx.beginPath()
    this.ctx.moveTo(x + width - foldSize, y)
    this.ctx.lineTo(x + width, y + foldSize)
    this.ctx.lineTo(x + width - foldSize, y + foldSize)
    this.ctx.closePath()
    this.ctx.fill()

    // 文本（编辑时跳过，由 textarea 接管）
    if (!skipText && el.text) {
      this.drawElementText(el, 12)
    }
  }

  /** 绘制箭头元素 */
  private drawArrowElement(el: ArrowElement, board: BoardModel): void {
    let startPoint = el.startPoint
    let endPoint = el.endPoint

    // 如果连接了元素，从锚点计算
    if (el.sourceId) {
      startPoint = board.getElementAnchor(el.sourceId, el.sourceAnchor)
    }
    if (el.targetId) {
      endPoint = board.getElementAnchor(el.targetId, el.targetAnchor)
    }

    const style = el.connectionStyle
    this.drawArrowLine(startPoint, endPoint, el.waypoints, style, el.label, el.controlPoints)
  }

  /** 绘制直线元素 */
  private drawLineElement(el: LineElement): void {
    const style = el.connectionStyle
    this.drawArrowLine(el.startPoint, el.endPoint, undefined, { ...style, startArrow: 'none', endArrow: 'none' }, undefined, el.controlPoints)
  }

  /** 绘制自由画笔 */
  private drawFreehand(el: FreehandElement): void {
    if (el.points.length < 2) return

    this.ctx.strokeStyle = el.style.stroke || '#1e293b'
    this.ctx.lineWidth = el.style.strokeWidth || 2
    this.ctx.lineCap = 'round'
    this.ctx.lineJoin = 'round'

    this.ctx.beginPath()
    this.ctx.moveTo(el.x + el.points[0].x, el.y + el.points[0].y)

    if (el.points.length === 2) {
      this.ctx.lineTo(el.x + el.points[1].x, el.y + el.points[1].y)
    } else {
      // 平滑曲线
      for (let i = 1; i < el.points.length - 1; i++) {
        const xc = (el.x + el.points[i].x + el.x + el.points[i + 1].x) / 2
        const yc = (el.y + el.points[i].y + el.y + el.points[i + 1].y) / 2
        this.ctx.quadraticCurveTo(el.x + el.points[i].x, el.y + el.points[i].y, xc, yc)
      }
      const last = el.points[el.points.length - 1]
      this.ctx.lineTo(el.x + last.x, el.y + last.y)
    }

    this.ctx.stroke()
  }

  /** 绘制图片 */
  private drawImageElement(el: ImageElement): void {
    if (!el.src) return

    let img = this.imageCache.get(el.id)
    if (!img) {
      img = new Image()
      img.src = this.resolveImageSrc(el.src)
      img.onload = () => { this.needsRedraw = true }
      this.imageCache.set(el.id, img)
    }

    const radius = Math.max(0, el.style.borderRadius ?? 0)

    if (img.complete && img.naturalWidth > 0) {
      // 圆角裁剪图片
      if (radius > 0) {
        this.ctx.save()
        this.roundRect(el.x, el.y, el.width, el.height, radius)
        this.ctx.clip()
        this.ctx.drawImage(img, el.x, el.y, el.width, el.height)
        this.ctx.restore()
      } else {
        this.ctx.drawImage(img, el.x, el.y, el.width, el.height)
      }
      // 圆角边框
      if (el.style.stroke && el.style.stroke !== 'transparent') {
        this.ctx.strokeStyle = el.style.stroke
        this.ctx.lineWidth = el.style.strokeWidth ?? 2
        this.roundRect(el.x, el.y, el.width, el.height, radius)
        this.ctx.stroke()
      }
    } else {
      // 占位符
      this.ctx.fillStyle = el.style.fill || '#f1f5f9'
      if (radius > 0) {
        this.roundRect(el.x, el.y, el.width, el.height, radius)
        this.ctx.fill()
      } else {
        this.ctx.fillRect(el.x, el.y, el.width, el.height)
      }
      this.ctx.fillStyle = '#94a3b8'
      this.ctx.font = '12px sans-serif'
      this.ctx.textAlign = 'center'
      this.ctx.textBaseline = 'middle'
      this.ctx.fillText('Loading...', el.x + el.width / 2, el.y + el.height / 2)
    }
  }

  /** 绘制分组 */
  private drawGroupElement(el: GroupElement, board: BoardModel): void {
    // 分组边框（虚线）
    this.ctx.strokeStyle = '#6366f1'
    this.ctx.lineWidth = 1
    this.ctx.setLineDash([6, 4])
    this.ctx.strokeRect(el.x - 4, el.y - 4, el.width + 8, el.height + 8)
    this.ctx.setLineDash([])
  }

  // ===== 连接线绘制 =====

  /** 绘制连接 */
  private drawConnection(conn: Connection, board: BoardModel): void {
    const sourceEl = board.getElement(conn.sourceId)
    const targetEl = board.getElement(conn.targetId)
    if (!sourceEl || !targetEl) return

    const startPoint = board.getElementAnchor(conn.sourceId, conn.sourceAnchor)
    const endPoint = board.getElementAnchor(conn.targetId, conn.targetAnchor)

    this.drawArrowLine(startPoint, endPoint, conn.waypoints, conn.style, conn.label)
  }

  /**
   * 绘制带箭头的线。
   * 每种箭头类型按几何公式独立计算 pathEnd，线段画到 pathEnd，箭头头部直接盖在 end 点上。
   */
  private drawArrowLine(
    start: Point, end: Point,
    waypoints?: Point[], style?: ConnectionStyle,
    label?: string, controlPoints?: { cp1: Point; cp2: Point }
  ): void {
    const s = style || {} as ConnectionStyle
    const color = s.stroke || '#64748b'
    const sw = s.strokeWidth || 2
    const route = s.route || 'straight'
    const hasEndArrow = s.endArrow && s.endArrow !== 'none'
    const endArrowType = s.endArrow || 'none'

    // ===== 辅助函数 =====
    const dir = (from: Point, to: Point): Point => {
      const dx = to.x - from.x, dy = to.y - from.y
      const len = Math.hypot(dx, dy)
      return len > 0 ? { x: dx / len, y: dy / len } : { x: 1, y: 0 }
    }

    // ===== 箭头尾部起点（在 arrow tip 沿反方向 bodyBack 处）=====
    const getEndArrowDir = (): Point => {
      if (route === 'bezier') {
        const { cp2 } = this.computeBezierControlPoints(start, end, controlPoints)
        return dir(cp2, end)
      } else if (route === 'orthogonal') {
        const pts = this.computeOrthogonalPath(start, end)
        return dir(pts[pts.length - 2], pts[pts.length - 1])
      } else if (waypoints?.length) {
        return dir(waypoints[waypoints.length - 1], end)
      } else {
        return dir(start, end)
      }
    }

    const arrowSize = this.getArrowSize(sw)
    // bodyBack = 等腰三角 altitude，尖端到背部落点 = size * cos(30°)
    const bodyBack = arrowSize * Math.cos(Math.PI / 6)

    // ===== 每种箭头类型的 pathEnd =====
    // filled arrow:  body 实心，round cap 延伸 sw/2 藏在 body 里
    //   pathEnd = tip - bodyBack*dir，round cap 扩展到 body 背部 → 被 body 实心覆盖 ✓
    // open arrow:    画成单一 path，主线连入 wing 起点，lineJoin='round' 形成圆头
    //   pathEnd = tip - bodyBack*dir，无 cap 延伸 ✓
    // 其他类型:      pathEnd = tip
    let pathEnd = end
    if (hasEndArrow && endArrowType !== 'arrow-open') {
      const d = getEndArrowDir()
      pathEnd = { x: end.x - d.x * bodyBack, y: end.y - d.y * bodyBack }
    }

    this.ctx.strokeStyle = color
    this.ctx.fillStyle = color
    this.ctx.lineWidth = sw
    this.ctx.lineCap = 'round'
    this.ctx.lineJoin = 'round'

    // ===== 画线段 =====
    this.ctx.beginPath()
    if (route === 'bezier') {
      const { cp1, cp2 } = this.computeBezierControlPoints(start, end, controlPoints)
      this.ctx.moveTo(start.x, start.y)
      this.ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, pathEnd.x, pathEnd.y)
    } else if (route === 'orthogonal') {
      const pts = this.computeOrthogonalPath(start, end)
      this.ctx.moveTo(start.x, start.y)
      for (let i = 1; i < pts.length - 1; i++) this.ctx.lineTo(pts[i].x, pts[i].y)
      this.ctx.lineTo(pathEnd.x, pathEnd.y)
    } else if (waypoints?.length) {
      this.ctx.moveTo(start.x, start.y)
      waypoints.forEach(wp => this.ctx.lineTo(wp.x, wp.y))
      this.ctx.lineTo(pathEnd.x, pathEnd.y)
    } else {
      this.ctx.moveTo(start.x, start.y)
      this.ctx.lineTo(pathEnd.x, pathEnd.y)
    }

    // open arrow: 画成单一 path，lineJoin='round' 在尖端形成圆头
    if (hasEndArrow && endArrowType === 'arrow-open') {
      const d = getEndArrowDir()
      const angle = Math.atan2(d.y, d.x)
      // 从尖端沿反方向（-d）延伸 wing 到 wingBack，然后回 lineJoin='round' 到尖端
      this.ctx.moveTo(
        end.x - arrowSize * Math.cos(angle - Math.PI / 6),
        end.y - arrowSize * Math.sin(angle - Math.PI / 6)
      )
      this.ctx.lineTo(end.x, end.y)
      this.ctx.lineTo(
        end.x - arrowSize * Math.cos(angle + Math.PI / 6),
        end.y - arrowSize * Math.sin(angle + Math.PI / 6)
      )
    }

    this.applyStroke(s as ElementStyle)
    this.ctx.stroke()
    this.ctx.setLineDash([])

    // filled arrow / 其他：单独画头部（lineJoin 不影响线段）
    if (hasEndArrow && endArrowType !== 'arrow-open') {
      this.drawArrowHead(end, getEndArrowDir(), endArrowType, color, sw)
    }

    // ===== 标签 =====
    if (label) {
      const pathSegments: Point[] = waypoints?.length
        ? [start, ...waypoints, end]
        : route === 'orthogonal' ? this.computeOrthogonalPath(start, end)
        : [start, end]
      const mid = this.getPathMidpoint(pathSegments, route, start, end, waypoints, controlPoints)
      this.ctx.fillStyle = '#ffffff'
      this.ctx.font = `${s.fontSize || 12}px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`
      this.ctx.textAlign = 'center'
      this.ctx.textBaseline = 'middle'
      const metrics = this.ctx.measureText(label)
      const padding = 4
      this.ctx.fillRect(mid.x - metrics.width / 2 - padding, mid.y - 8 - padding, metrics.width + padding * 2, 16 + padding * 2)
      this.ctx.fillStyle = s.color || color
      this.ctx.fillText(label, mid.x, mid.y)
    }
  }

  /**
   * 计算正交路由路径（不含端点裁剪）
   * 返回: [start, corner1, ..., cornerN, end]
   */
  private computeOrthogonalPath(start: Point, end: Point): Point[] {
    const horizontal = Math.abs(end.y - start.y) > Math.abs(end.x - start.x)
    if (horizontal) {
      const midY = (start.y + end.y) / 2
      return [start, { x: start.x, y: midY }, { x: end.x, y: midY }, end]
    } else {
      const midX = (start.x + end.x) / 2
      return [start, { x: midX, y: start.y }, { x: midX, y: end.y }, end]
    }
  }

  /**
   * 获取路径实际中点（用于 orthogonal/bezier 模式下的标签定位）
   */
  private getPathMidpoint(points: Point[], route: string, start: Point, end: Point, _waypoints?: Point[], controlPoints?: { cp1: Point; cp2: Point }): Point {
    if (route === 'bezier') {
      const { cp1, cp2 } = this.computeBezierControlPoints(start, end, controlPoints)
      const t = 0.5
      const u = 1 - t
      const b0 = u * u * u, b1 = 3 * u * u * t, b2 = 3 * u * t * t, b3 = t * t * t
      return {
        x: b0 * start.x + b1 * cp1.x + b2 * cp2.x + b3 * end.x,
        y: b0 * start.y + b1 * cp1.y + b2 * cp2.y + b3 * end.y,
      }
    }
    if (route === 'orthogonal' && points.length >= 2) {
      let total = 0
      const segs: number[] = []
      for (let i = 0; i < points.length - 1; i++) {
        const len = Math.hypot(points[i + 1].x - points[i].x, points[i + 1].y - points[i].y)
        segs.push(len)
        total += len
      }
      const half = total / 2
      let acc = 0
      for (let i = 0; i < segs.length; i++) {
        if (acc + segs[i] >= half) {
          const ratio = (half - acc) / segs[i]
          return {
            x: points[i].x + (points[i + 1].x - points[i].x) * ratio,
            y: points[i].y + (points[i + 1].y - points[i].y) * ratio,
          }
        }
        acc += segs[i]
      }
    }
    return { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 }
  }

  /**
   * 固定像素尺寸（Excalidraw 2024+ 风格），strokeWidth 仅影响 lineJoin 视觉粗细
   */
  private getArrowSize(_sw: number): number {
    return 25
  }

  /** 绘制箭头端点：尖端在 point，朝向 direction 向量 */
  private drawArrowHead(point: Point, direction: Point, type: ArrowEnd, color: string, sw: number): void {
    const size = this.getArrowSize(sw)
    const angle = Math.atan2(direction.y, direction.x)
    this.ctx.fillStyle = color
    this.ctx.strokeStyle = color

    switch (type) {
      case 'arrow': {
        this.ctx.lineWidth = Math.max(1, sw * 0.5)
        this.ctx.lineJoin = 'round'
        this.ctx.beginPath()
        this.ctx.moveTo(point.x, point.y)
        this.ctx.lineTo(
          point.x - size * Math.cos(angle - Math.PI / 6),
          point.y - size * Math.sin(angle - Math.PI / 6)
        )
        this.ctx.lineTo(
          point.x - size * Math.cos(angle + Math.PI / 6),
          point.y - size * Math.sin(angle + Math.PI / 6)
        )
        this.ctx.closePath()
        this.ctx.fill()
        break
      }
      case 'arrow-open': {
        this.ctx.lineWidth = sw
        this.ctx.lineJoin = 'round'
        this.ctx.beginPath()
        this.ctx.moveTo(point.x - size * Math.cos(angle - Math.PI / 6), point.y - size * Math.sin(angle - Math.PI / 6))
        this.ctx.lineTo(point.x, point.y)
        this.ctx.lineTo(point.x - size * Math.cos(angle + Math.PI / 6), point.y - size * Math.sin(angle + Math.PI / 6))
        this.ctx.stroke()
        break
      }
      case 'circle': {
        this.ctx.lineWidth = Math.max(1, sw * 0.5)
        const r = (size * 0.5 + sw) / 2
        this.ctx.beginPath()
        this.ctx.arc(point.x, point.y, r, 0, Math.PI * 2)
        this.ctx.stroke()
        break
      }
      case 'dot': {
        const r = (size * 0.5 + sw) / 2
        this.ctx.beginPath()
        this.ctx.arc(point.x, point.y, r, 0, Math.PI * 2)
        this.ctx.fill()
        break
      }
      case 'diamond': {
        this.ctx.lineWidth = Math.max(1, sw * 0.5)
        const d = (size * 0.5) / 2
        this.ctx.beginPath()
        this.ctx.moveTo(point.x, point.y)
        this.ctx.lineTo(point.x - d * Math.cos(angle) - d * Math.sin(angle), point.y - d * Math.sin(angle) + d * Math.cos(angle))
        this.ctx.lineTo(point.x - 2 * d * Math.cos(angle), point.y - 2 * d * Math.sin(angle))
        this.ctx.lineTo(point.x - d * Math.cos(angle) + d * Math.sin(angle), point.y - d * Math.sin(angle) - d * Math.cos(angle))
        this.ctx.closePath()
        this.ctx.fill()
        break
      }
      case 'diamond-open': {
        this.ctx.lineWidth = Math.max(1, sw * 0.5)
        const d = (size * 0.5) / 2
        this.ctx.beginPath()
        this.ctx.moveTo(point.x - d * Math.cos(angle) - d * Math.sin(angle), point.y - d * Math.sin(angle) + d * Math.cos(angle))
        this.ctx.lineTo(point.x - 2 * d * Math.cos(angle), point.y - 2 * d * Math.sin(angle))
        this.ctx.lineTo(point.x - d * Math.cos(angle) + d * Math.sin(angle), point.y - d * Math.sin(angle) - d * Math.cos(angle))
        this.ctx.closePath()
        this.ctx.stroke()
        break
      }
      case 'square': {
        this.ctx.lineWidth = Math.max(1, sw * 0.5)
        const s = size * 0.3
        this.ctx.save()
        this.ctx.translate(point.x, point.y)
        this.ctx.rotate(angle - Math.PI / 4)
        this.ctx.fillRect(-s, -s, s * 2, s * 2)
        this.ctx.restore()
        break
      }
      case 'bar': {
        this.ctx.lineWidth = Math.max(1, sw * 0.5)
        const barLen = size * 0.6
        const barWidth = sw
        this.ctx.save()
        this.ctx.translate(point.x, point.y)
        this.ctx.rotate(angle - Math.PI / 2)
        this.ctx.fillRect(-barLen / 2, -barWidth / 2, barLen, barWidth)
        this.ctx.restore()
        break
      }
    }
  }

  /** 计算 bezier 控制点（S 形曲线） */
  computeBezierControlPoints(start: Point, end: Point, override?: { cp1: Point; cp2: Point }): { cp1: Point; cp2: Point } {
    if (override) return override
    const dx = end.x - start.x
    const dy = end.y - start.y
    const len = Math.hypot(dx, dy)
    if (len === 0) return { cp1: start, cp2: end }
    // perp = (-dy/L, dx/L)，曲线向"左上"侧弯曲
    const perpX = -dy / len
    const perpY = dx / len
    const offset = len * 0.25
    return {
      cp1: { x: start.x + dx * 0.25 + perpX * offset, y: start.y + dy * 0.25 + perpY * offset },
      cp2: { x: end.x - dx * 0.25 + perpX * offset, y: end.y - dy * 0.25 + perpY * offset },
    }
  }

  // ===== 选择手柄 =====

  private drawSelectionHandles(el: BoardElement): void {
    const handleSize = 7 / this.viewport.zoom

    // 箭头/线条：绘制线段选中框 + 端点圆形手柄 + 中点手柄
    if (el.type === 'arrow' || el.type === 'line') {
      const arrow = el as ArrowElement | LineElement
      const sp = 'startPoint' in arrow ? arrow.startPoint : { x: el.x, y: el.y }
      const ep = 'endPoint' in arrow ? arrow.endPoint : { x: el.x + el.width, y: el.y }
      const route = arrow.connectionStyle?.route || 'straight'
      let midX = (sp.x + ep.x) / 2
      let midY = (sp.y + ep.y) / 2
      let cp1: Point | null = null
      let cp2: Point | null = null
      if (route === 'bezier') {
        const { cp1: c1, cp2: c2 } = this.computeBezierControlPoints(sp, ep, arrow.controlPoints)
        cp1 = c1
        cp2 = c2
        const mid = this.getPathMidpoint([], route, sp, ep, undefined, arrow.controlPoints)
        midX = mid.x
        midY = mid.y
      }

      // 线段选中框（蓝色实线）
      this.ctx.strokeStyle = '#3b82f6'
      this.ctx.lineWidth = 1.5 / this.viewport.zoom
      this.ctx.setLineDash([])
      this.ctx.beginPath()
      this.ctx.moveTo(sp.x, sp.y)
      if (route === 'bezier' && cp1 && cp2) {
        this.ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, ep.x, ep.y)
      } else {
        this.ctx.lineTo(ep.x, ep.y)
      }
      this.ctx.stroke()

      // 贝塞尔控制线（从端点到控制点，虚线，半透明）
      if (route === 'bezier' && cp1 && cp2) {
        this.ctx.strokeStyle = 'rgba(59, 130, 246, 0.35)'
        this.ctx.lineWidth = 1 / this.viewport.zoom
        this.ctx.setLineDash([4 / this.viewport.zoom, 4 / this.viewport.zoom])
        ;[
          [sp, cp1], [ep, cp2]
        ].forEach(([a, b]) => {
          this.ctx.beginPath()
          this.ctx.moveTo(a.x, a.y)
          this.ctx.lineTo(b.x, b.y)
          this.ctx.stroke()
        })
        this.ctx.setLineDash([])
      }

      // 端点手柄：白色填充 + 蓝色描边 + 阴影（Excalidraw 风格）
      const r = (handleSize / 2) * 1.2
      ;[sp, ep].forEach(pt => {
        this.ctx.save()
        this.ctx.shadowColor = 'rgba(59, 130, 246, 0.4)'
        this.ctx.shadowBlur = 4 / this.viewport.zoom
        this.ctx.shadowOffsetX = 0
        this.ctx.shadowOffsetY = 1 / this.viewport.zoom
        this.ctx.fillStyle = '#ffffff'
        this.ctx.strokeStyle = '#3b82f6'
        this.ctx.lineWidth = 1.5 / this.viewport.zoom
        this.ctx.beginPath()
        this.ctx.arc(pt.x, pt.y, r, 0, Math.PI * 2)
        this.ctx.fill()
        this.ctx.stroke()
        this.ctx.restore()
      })

      // 贝塞尔控制点手柄：蓝色小方块（Excalidraw 风格）
      if (route === 'bezier' && cp1 && cp2) {
        const cpSize = handleSize * 0.9
        ;[cp1, cp2].forEach(cp => {
          this.ctx.save()
          this.ctx.fillStyle = '#ffffff'
          this.ctx.strokeStyle = '#3b82f6'
          this.ctx.lineWidth = 1.5 / this.viewport.zoom
          this.ctx.fillRect(cp.x - cpSize / 2, cp.y - cpSize / 2, cpSize, cpSize)
          this.ctx.strokeRect(cp.x - cpSize / 2, cp.y - cpSize / 2, cpSize, cpSize)
          this.ctx.restore()
        })
      }

      // 中点手柄：蓝色实心圆（Excalidraw 风格，方便拖拽整条线）
      const midR = (handleSize / 2) * 0.8
      this.ctx.save()
      this.ctx.fillStyle = '#3b82f6'
      this.ctx.strokeStyle = '#ffffff'
      this.ctx.lineWidth = 1.5 / this.viewport.zoom
      this.ctx.beginPath()
      this.ctx.arc(midX, midY, midR, 0, Math.PI * 2)
      this.ctx.fill()
      this.ctx.stroke()
      this.ctx.restore()
      return
    }

    // 普通形状：绘制包围盒 + 8 个方形手柄 + 旋转手柄
    // 获取手柄位置（已考虑旋转）
    const handles = this.getResizeHandles(el)
    const rotateHandlePos = handles.get('rotate')!
    const nw = handles.get('nw')!
    const ne = handles.get('ne')!
    const se = handles.get('se')!
    const sw = handles.get('sw')!

    // 绘制旋转后的包围盒边框
    this.ctx.strokeStyle = '#3b82f6'
    this.ctx.lineWidth = 1.5 / this.viewport.zoom
    this.ctx.setLineDash([])
    this.ctx.beginPath()
    this.ctx.moveTo(nw.x, nw.y)
    this.ctx.lineTo(ne.x, ne.y)
    this.ctx.lineTo(se.x, se.y)
    this.ctx.lineTo(sw.x, sw.y)
    this.ctx.closePath()
    this.ctx.stroke()

    // 绘制旋转手柄连接线（从顶边中点到旋转手柄）
    const topCenter = handles.get('n')!
    this.ctx.beginPath()
    this.ctx.moveTo(topCenter.x, topCenter.y)
    this.ctx.lineTo(rotateHandlePos.x, rotateHandlePos.y)
    this.ctx.stroke()

    // 绘制旋转手柄（圆形）
    const rotateHandleSize = handleSize * 1.2
    this.ctx.fillStyle = '#ffffff'
    this.ctx.strokeStyle = '#3b82f6'
    this.ctx.lineWidth = 1.5 / this.viewport.zoom
    this.ctx.beginPath()
    this.ctx.arc(rotateHandlePos.x, rotateHandlePos.y, rotateHandleSize / 2, 0, Math.PI * 2)
    this.ctx.fill()
    this.ctx.stroke()

    // 绘制 8 个缩放手柄（排除旋转手柄）
    handles.forEach((pos: Point, handleName: ResizeHandle) => {
      if (handleName === 'rotate') return
      this.ctx.fillStyle = '#ffffff'
      this.ctx.strokeStyle = '#3b82f6'
      this.ctx.lineWidth = 1.5 / this.viewport.zoom
      this.ctx.fillRect(pos.x - handleSize / 2, pos.y - handleSize / 2, handleSize, handleSize)
      this.ctx.strokeRect(pos.x - handleSize / 2, pos.y - handleSize / 2, handleSize, handleSize)
    })
  }

  /** 绘制正在拖拽的选择框 */
  private drawSelectionBox(box: Rect): void {
    // 绘制半透明填充
    this.ctx.fillStyle = 'rgba(59, 130, 246, 0.1)'
    this.ctx.fillRect(box.x, box.y, box.width, box.height)

    // 绘制边框
    this.ctx.strokeStyle = '#3b82f6'
    this.ctx.lineWidth = 1.5 / this.viewport.zoom
    this.ctx.setLineDash([5 / this.viewport.zoom, 3 / this.viewport.zoom])
    this.ctx.strokeRect(box.x, box.y, box.width, box.height)
    this.ctx.setLineDash([])
  }

  /** 获取 Resize 手柄位置 */
  getResizeHandles(el: BoardElement): Map<ResizeHandle, Point> {
    const handles = new Map<ResizeHandle, Point>()

    // 箭头/线条：使用线段端点 + 中点作为手柄
    if (el.type === 'arrow' || el.type === 'line') {
      const arrow = el as ArrowElement | LineElement
      const sp = 'startPoint' in arrow ? arrow.startPoint : { x: el.x, y: el.y }
      const ep = 'endPoint' in arrow ? arrow.endPoint : { x: el.x + el.width, y: el.y }
      handles.set('start', { ...sp })
      handles.set('end', { ...ep })
      // 中点（方便拖拽中间区域）：Bezier 模式下使用曲线实际中点
      const route = arrow.connectionStyle?.route || 'straight'
      if (route === 'bezier') {
        const controlPoints = arrow.controlPoints
        const { cp1, cp2 } = this.computeBezierControlPoints(sp, ep, controlPoints)
        const mid = this.getBezierMidpoint(sp, cp1, cp2, ep)
        handles.set('n', mid)
        // 贝塞尔控制点手柄（可拖拽调整曲度）
        handles.set('cp1', { ...cp1 })
        handles.set('cp2', { ...cp2 })
      } else {
        handles.set('n', { x: (sp.x + ep.x) / 2, y: (sp.y + ep.y) / 2 })
      }
      return handles
    }

    // 普通形状：使用包围盒角点 + 旋转手柄（考虑元素旋转）
    const { x, y, width, height } = el
    const cx = x + width / 2
    const cy = y + height / 2
    const rotation = el.rotation ?? 0

    const corners = [
      { key: 'nw' as ResizeHandle, x, y },
      { key: 'n' as ResizeHandle, x: x + width / 2, y },
      { key: 'ne' as ResizeHandle, x: x + width, y },
      { key: 'e' as ResizeHandle, x: x + width, y: y + height / 2 },
      { key: 'se' as ResizeHandle, x: x + width, y: y + height },
      { key: 's' as ResizeHandle, x: x + width / 2, y: y + height },
      { key: 'sw' as ResizeHandle, x, y: y + height },
      { key: 'w' as ResizeHandle, x, y: y + height / 2 },
    ]

    corners.forEach(({ key, x: px, y: py }) => {
      handles.set(key, rotation ? this.rotatePoint({ x: px, y: py }, { x: cx, y: cy }, rotation) : { x: px, y: py })
    })

    // 旋转手柄：位于顶边中点上方（同样需要旋转）
    const rotateHandleLocal = { x: cx, y: y - 20 / this.viewport.zoom }
    handles.set('rotate', rotation ? this.rotatePoint(rotateHandleLocal, { x: cx, y: cy }, rotation) : rotateHandleLocal)
    return handles
  }

  /** 检测点击了哪个 Resize 手柄 */
  hitTestResizeHandle(el: BoardElement, worldPos: Point): ResizeHandle | 'drag' | null {
    const handles = this.getResizeHandles(el)
    const hitRadius = 8 / this.viewport.zoom

    // 箭头/线条：先检测是否点击在端点或中点或控制点手柄
    if (el.type === 'arrow' || el.type === 'line') {
      for (const [handle, pos] of handles) {
        const dx = worldPos.x - pos.x
        const dy = worldPos.y - pos.y
        if (dx * dx + dy * dy <= hitRadius * hitRadius) {
          return handle // 'start' | 'end' | 'n' | 'cp1' | 'cp2'
        }
      }
      // 手柄都没命中，检测是否点击在线条本身上
      const arrow = el as ArrowElement | LineElement
      const sp = (arrow as any).startPoint ?? { x: el.x, y: el.y }
      const ep = (arrow as any).endPoint ?? { x: el.x + el.width, y: el.y }
      const route = arrow.connectionStyle?.route || 'straight'
      const lineHitRadius = 8 / this.viewport.zoom

      if (route === 'bezier') {
        const { cp1, cp2 } = this.computeBezierControlPoints(sp, ep, arrow.controlPoints)
        if (this.distToBezier(worldPos, sp, cp1, cp2, ep) <= lineHitRadius) {
          return 'drag'
        }
      } else {
        if (this.distToSegment(worldPos, sp, ep) <= lineHitRadius) {
          return 'drag'
        }
      }
      return null
    }

    // 普通形状：检测 8 个角点手柄
    for (const [handle, pos] of handles) {
      const dx = worldPos.x - pos.x
      const dy = worldPos.y - pos.y
      if (dx * dx + dy * dy <= hitRadius * hitRadius) {
        return handle
      }
    }
    return null
  }

  /** 点到线段的距离 */
  private distToSegment(p: Point, a: Point, b: Point): number {
    const dx = b.x - a.x
    const dy = b.y - a.y
    const lenSq = dx * dx + dy * dy
    if (lenSq === 0) return Math.hypot(p.x - a.x, p.y - a.y)
    let t = Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / lenSq))
    return Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy))
  }

  /**
   * 计算 Bezier 曲线上任意参数 t (0-1) 对应的点
   */
  private getBezierPointAtT(t: number, p0: Point, p1: Point, p2: Point, p3: Point): Point {
    const u = 1 - t
    const uu = u * u
    const tt = t * t
    return {
      x: uu * u * p0.x + 3 * uu * t * p1.x + 3 * u * tt * p2.x + tt * t * p3.x,
      y: uu * u * p0.y + 3 * uu * t * p1.y + 3 * u * tt * p2.y + tt * t * p3.y,
    }
  }

  /**
   * 计算点到 Bezier 曲线的最小距离（使用牛顿迭代法采样）
   */
  private distToBezier(p: Point, p0: Point, p1: Point, p2: Point, p3: Point, samples: number = 50): number {
    let minDist = Infinity
    for (let i = 0; i <= samples; i++) {
      const t = i / samples
      const pt = this.getBezierPointAtT(t, p0, p1, p2, p3)
      const d = Math.hypot(p.x - pt.x, p.y - pt.y)
      if (d < minDist) minDist = d
    }
    return minDist
  }

  /**
   * 计算 Bezier 曲线的中点（t=0.5）
   */
  private getBezierMidpoint(p0: Point, p1: Point, p2: Point, p3: Point): Point {
    return this.getBezierPointAtT(0.5, p0, p1, p2, p3)
  }

  /**
   * 绕指定中心点旋转点
   */
  private rotatePoint(point: Point, center: Point, angle: number): Point {
    const cos = Math.cos(angle)
    const sin = Math.sin(angle)
    const dx = point.x - center.x
    const dy = point.y - center.y
    return {
      x: center.x + dx * cos - dy * sin,
      y: center.y + dx * sin + dy * cos,
    }
  }

  // ===== 辅助绘制 =====

  /** 圆角矩形 */
  private roundRect(x: number, y: number, w: number, h: number, r: number): void {
    // Fully rounded sentinel (9999) and clamp to available space
    const radius = Math.min(r, w / 2, h / 2)
    this.ctx.beginPath()
    this.ctx.moveTo(x + radius, y)
    this.ctx.lineTo(x + w - radius, y)
    this.ctx.arcTo(x + w, y, x + w, y + radius, radius)
    this.ctx.lineTo(x + w, y + h - radius)
    this.ctx.arcTo(x + w, y + h, x + w - radius, y + h, radius)
    this.ctx.lineTo(x + radius, y + h)
    this.ctx.arcTo(x, y + h, x, y + h - radius, radius)
    this.ctx.lineTo(x, y + radius)
    this.ctx.arcTo(x, y, x + radius, y, radius)
    this.ctx.closePath()
  }

  /** 绘制元素内文本（矩形/椭圆/菱形/便利贴的内嵌文本） */
  private drawElementText(el: BoardElement & { text?: string }, padding: number = 8): void {
    if (!('text' in el) || !el.text) return
    const text = el.text as string
    const style = el.style

    const fontSize = style.fontSize || 14
    const fontWeight = style.fontWeight || '400'
    const align = style.textAlign || 'center'
    const baseline = style.textBaseline || 'middle'

    this.ctx.fillStyle = style.color || '#1e293b'
    this.ctx.font = `${fontWeight} ${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`

    const maxWidth = el.width - padding * 2
    const lines = this.wrapText(text, maxWidth, fontSize)
    const { lineGap } = this.getFontMetrics(fontSize, fontWeight)
    const lineHeight = fontSize + lineGap

    const textX = align === 'left' ? el.x + padding
      : align === 'right' ? el.x + el.width - padding
      : el.x + el.width / 2

    let startY: number
    if (baseline === 'top') {
      this.ctx.textBaseline = 'top'
      startY = el.y + padding
    } else if (baseline === 'bottom') {
      this.ctx.textBaseline = 'bottom'
      startY = el.y + el.height - padding
    } else {
      this.ctx.textBaseline = 'middle'
      startY = el.y + el.height / 2
    }
    this.ctx.textAlign = align

    lines.forEach((line, i) => {
      this.ctx.fillText(line, textX, startY + i * lineHeight, maxWidth)
    })
  }

  /** 文本自动换行 */
  private wrapText(text: string, maxWidth: number, fontSize: number = 14): string[] {
    if (!text) return []
    const lines: string[] = []
    let currentLine = ''

    for (let i = 0; i < text.length; i++) {
      const char = text[i]
      if (char === '\n') {
        lines.push(currentLine)
        currentLine = ''
        continue
      }
      const testLine = currentLine + char
      if (this.ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
        lines.push(currentLine)
        currentLine = char
      } else {
        currentLine = testLine
      }
    }
    if (currentLine) lines.push(currentLine)
    return lines
  }
  // ===== 字体度量 =====

  /**
   * 实测字体度量（缓存），避免依赖字体比例估算
   */
  private getFontMetrics(fontSize: number, fontWeight: string = '400'): { ascent: number; descent: number; lineGap: number } {
    const key = `${fontWeight}_${fontSize}`
    if (this.fontMetricsCache.has(key)) {
      return this.fontMetricsCache.get(key)!
    }
    const ascent = this.ctx.measureText('Hg').actualBoundingBoxAscent
    const descent = this.ctx.measureText('Hg').actualBoundingBoxDescent
    const lineGap = Math.round(fontSize * 0.4) // lineHeight = fontSize * 1.4，对应 0.4*fontSize 的行间距
    const result = { ascent, descent, lineGap }
    this.fontMetricsCache.set(key, result)
    return result
  }

  /** 将相对路径转为完整 URL，相对路径指向前缀 /files/ */
  private resolveImageSrc(src: string): string {
    if (!src) return ''
    if (src.startsWith('data:') || src.startsWith('http://') || src.startsWith('https://') || src.startsWith('blob:') || src.startsWith('file://')) {
      return src
    }
    return window.location.origin + src
  }

  /** 使图片缓存失效（元素 src 变更时调用，如上传完成后替换占位图） */
  invalidateImageCache(elementId: string): void {
    this.imageCache.delete(elementId)
    this.needsRedraw = true
  }

  /**
   * 测量文本宽度（供交互层在创建元素前估算初始尺寸）
   */
  measureTextWidth(text: string, fontSize: number, fontWeight: string = '400'): number {
    this.ctx.font = `${fontWeight} ${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`
    return this.ctx.measureText(text).width
  }

  // ===== 公共方法 =====

  forceRedraw(): void {
    this.needsRedraw = true
  }

  resize(width: number, height: number): void {
    this.config.width = width
    this.config.height = height
    this.setupCanvas()
    this.needsRedraw = true
  }

  toDataURL(type: string = 'image/png'): string {
    return this.canvas.toDataURL(type)
  }
}

export default BoardRenderer
