textElementLayout.ts 3.65 KB
/** 文本控件在元素框内的垂直对齐(config.verticalAlign) */
export type TextVerticalAlign = 'top' | 'center' | 'bottom'

export function readVerticalAlign(
  config: Record<string, unknown> | undefined | null,
): TextVerticalAlign {
  const raw = config?.verticalAlign ?? config?.VerticalAlign
  const v = String(raw ?? 'top').trim().toLowerCase()
  if (v === 'center' || v === 'middle') return 'center'
  if (v === 'bottom') return 'bottom'
  return 'top'
}

export function computeVerticalTextBlockOffset(
  innerHeight: number,
  blockHeight: number,
  verticalAlign: TextVerticalAlign,
): number {
  const extra = Math.max(0, innerHeight - blockHeight)
  if (verticalAlign === 'center') return Math.floor(extra / 2)
  if (verticalAlign === 'bottom') return extra
  return 0
}

export function readFontWeight(
  config: Record<string, unknown> | undefined | null,
): 'normal' | 'bold' {
  const raw = config?.fontWeight ?? config?.FontWeight
  const v = String(raw ?? 'normal').trim().toLowerCase()
  return v === 'bold' || v === '700' || v === 'bolder' ? 'bold' : 'normal'
}

export function readFontStyle(
  config: Record<string, unknown> | undefined | null,
): 'normal' | 'italic' {
  const raw = config?.fontStyle ?? config?.FontStyle
  const v = String(raw ?? 'normal').trim().toLowerCase()
  return v === 'italic' || v === 'oblique' ? 'italic' : 'normal'
}

export function readTextDecoration(
  config: Record<string, unknown> | undefined | null,
): 'none' | 'underline' {
  const raw = config?.textDecoration ?? config?.TextDecoration
  const v = String(raw ?? 'none').trim().toLowerCase()
  return v.includes('underline') ? 'underline' : 'none'
}

export function readElementBorder(
  el: {
    border?: string | null
    Border?: string | null
    BorderType?: string | null
    borderType?: string | null
  },
): string {
  const raw = el.border ?? el.Border ?? el.BorderType ?? el.borderType ?? 'none'
  return String(raw).trim().toLowerCase() || 'none'
}

/** 元素 rotation(兼容大小写) */
export function readElementRotation(
  el: { rotation?: string | null; Rotation?: string | null },
): 'horizontal' | 'vertical' | 'right' | 'rotate180' {
  const v = String(el.rotation ?? el.Rotation ?? 'horizontal').trim().toLowerCase()
  if (v === 'vertical' || v === 'left' || v === 'rotate-left' || v === 'left90') return 'vertical'
  if (v === 'right' || v === 'rotate-right' || v === 'right90') return 'right'
  if (v === 'rotate180' || v === '180' || v === 'down' || v === 'inverted') return 'rotate180'
  return 'horizontal'
}

export function elementRotationDegrees(
  el: { rotation?: string | null; Rotation?: string | null },
): 0 | -90 | 90 | 180 {
  const rotation = readElementRotation(el)
  if (rotation === 'vertical') return -90
  if (rotation === 'right') return 90
  if (rotation === 'rotate180') return 180
  return 0
}

/** 与 renderLabelPreviewCanvas lineHeightForTextElement 一致 */
export function textElementLineHeightPx(fontSize: number, isDateTimeType = false): number {
  return isDateTimeType
    ? Math.max(fontSize + 2, Math.round(fontSize * 1.2))
    : Math.max(fontSize + 2, Math.round(fontSize * 1.25))
}

/** 矮框内固定文案:强制单行,避免折行后仅显示第一行 */
export function isShortSingleLineTextBox(
  boxHeightPx: number,
  fontSize: number,
  text: string,
  opts?: { breakAll?: boolean },
): boolean {
  if (opts?.breakAll) return false
  if (String(text ?? '').includes('\n')) return false
  const lineH = textElementLineHeightPx(fontSize, false)
  const descentPad = Math.max(2, Math.round(fontSize * 0.14))
  const maxLines = Math.max(
    1,
    Math.floor((Math.max(0, boxHeightPx) + descentPad) / lineH),
  )
  return maxLines <= 1
}