import type { DetailScreen } from './types' const combiningMark = new RegExp('\\p{Mark}', 'u') /** Conservative grapheme wrapping matches the fixed font and Fabric splitByGrapheme layout. */ export function estimateDetailTextHeight(text: string, width: number, fontSize: number, lineHeight: number): number { if (!text) return Math.ceil(fontSize * 1.13) let lines = 0 for (const paragraph of text.split('\n')) { let occupied = 0; let paragraphLines = 1 for (const character of Array.from(paragraph)) { if (combiningMark.test(character)) continue const factor = /[MW@%]/.test(character) ? 1 : /[\x20-\x7e]/.test(character) ? 0.65 : 1.05 const next = fontSize * factor if (occupied > 0 && occupied + next > width) { paragraphLines++; occupied = 0 } occupied += next } lines += paragraphLines } return Math.ceil(lines * fontSize * 1.13 * lineHeight) } /** v1 preserves the template while letting copy grow without covering the product image. */ export function reflowDetailScreen(screen: DetailScreen): DetailScreen { const [title, body] = screen.texts if (!title || !body || !title.id.endsWith('-title') || !body.id.endsWith('-body')) return screen const titleY = screen.logo ? Math.max(58, screen.logo.y + screen.logo.height + 24) : 58 const titleHeight = estimateDetailTextHeight(screen.title, title.width, title.fontSize, title.lineHeight) const bodyY = titleY + titleHeight + 24 const bodyHeight = estimateDetailTextHeight(screen.body, body.width, body.fontSize, body.lineHeight) const imageArea = { x: 0, y: bodyY + bodyHeight + 32, width: 750, height: 750 } const height = Math.max(1200, imageArea.y + imageArea.height + 48) if (height > 2400) throw new Error('文案过长,请精简后确认;每屏高度最多 2400px') return { ...screen, height, imageArea, ...(screen.image ? { image: { assetId: screen.image.assetId, ...imageArea } } : {}), texts: [{ ...title, text: screen.title, y: titleY }, { ...body, text: screen.body, y: bodyY }, ...screen.texts.slice(2)], } }