import type { Operation } from './usePrintQueue' // Configuración de caracteres por línea según fuente y ancho export const CHAR_LIMITS = { font_a: { normal: 33, doubleWidth: 16 }, font_b: { normal: 40, doubleWidth: 20 } } as const export type FontType = 'font_a' | 'font_b' export type AlignType = 'left' | 'center' | 'right' // Segmento de texto: puede ser texto normal o una variable export interface TextSegment { type: 'text' | 'variable' content: string variableName?: string // Solo para type='variable' } // Una línea en el preview export interface PreviewLine { segments: TextSegment[] fullText: string // Texto completo para calcular longitud align: AlignType bold: boolean underline: boolean doubleWidth: boolean doubleHeight: boolean font: FontType charLimit: number exceedsLimit: boolean isFeed: boolean // true si es una línea vacía (feed) } // Estado del preview export interface PreviewState { lines: PreviewLine[] font: FontType warnings: string[] totalLines: number } // Regex para detectar variables: {{nombre}} o {{nombre:label:default}} const VARIABLE_REGEX = /\{\{(\w+)(?::([^:}]+))?(?::([^}]+))?\}\}/g // Parsear texto para extraer segmentos (texto normal y variables) export function parseTextSegments(text: string): TextSegment[] { const segments: TextSegment[] = [] let lastIndex = 0 let match: RegExpExecArray | null // Reset regex VARIABLE_REGEX.lastIndex = 0 while ((match = VARIABLE_REGEX.exec(text)) !== null) { // Agregar texto antes de la variable if (match.index > lastIndex) { segments.push({ type: 'text', content: text.slice(lastIndex, match.index) }) } // Agregar la variable const [fullMatch, name, _label, defaultValue] = match segments.push({ type: 'variable', content: defaultValue || name, // Mostrar default o nombre variableName: name }) lastIndex = match.index + fullMatch.length } // Agregar texto restante if (lastIndex < text.length) { segments.push({ type: 'text', content: text.slice(lastIndex) }) } // Si no hay segmentos, el texto completo es normal if (segments.length === 0 && text) { segments.push({ type: 'text', content: text }) } return segments } // Obtener el texto completo de los segmentos export function getFullText(segments: TextSegment[]): string { return segments.map(s => s.content).join('') } // Calcular límite de caracteres export function getCharLimit(font: FontType, doubleWidth: boolean): number { const limits = CHAR_LIMITS[font] return doubleWidth ? limits.doubleWidth : limits.normal } // Procesar operaciones en líneas de preview export function processOperations( operations: Operation[], variableValues: Record = {} ): PreviewState { const lines: PreviewLine[] = [] const warnings: string[] = [] // Estado actual (se modifica con cada operación de estilo) let currentState = { font: 'font_a' as FontType, align: 'left' as AlignType, bold: false, underline: false, doubleWidth: false, doubleHeight: false } for (const op of operations) { switch (op.op) { case 'text': { let text = op.value || '' // Reemplazar variables con valores proporcionados const segments = parseTextSegments(text) const resolvedSegments = segments.map(seg => { if (seg.type === 'variable' && seg.variableName && variableValues[seg.variableName]) { return { ...seg, content: variableValues[seg.variableName] } } return seg }) const fullText = getFullText(resolvedSegments) const charLimit = getCharLimit(currentState.font, currentState.doubleWidth) const exceedsLimit = fullText.length > charLimit if (exceedsLimit) { warnings.push(`Línea excede ${charLimit} chars: "${fullText.slice(0, 20)}..."`) } lines.push({ segments: resolvedSegments, fullText, ...currentState, charLimit, exceedsLimit, isFeed: false }) break } case 'textAlign': currentState.align = (op.align as AlignType) || 'left' break case 'textFont': if (op.font === 'font_a' || op.font === 'font_b') { currentState.font = op.font } break case 'textSize': currentState.doubleWidth = (op.width || 1) >= 2 currentState.doubleHeight = (op.height || 1) >= 2 break case 'textDouble': currentState.doubleWidth = op.dw === true currentState.doubleHeight = op.dh === true break case 'textStyle': if (op.em !== undefined) currentState.bold = op.em if (op.ul !== undefined) currentState.underline = op.ul break case 'feed': case 'feedLine': { const lineCount = op.line || op.lines || 1 for (let i = 0; i < lineCount; i++) { lines.push({ segments: [], fullText: '', ...currentState, charLimit: getCharLimit(currentState.font, currentState.doubleWidth), exceedsLimit: false, isFeed: true }) } break } case 'cut': // El corte no agrega líneas visuales, solo indica fin break // Ignorar operaciones que no afectan el preview visual case 'pulse': case 'textRotate': case 'textLineSpace': case 'feedUnit': break } } return { lines, font: currentState.font, warnings, totalLines: lines.length } } // Composable para el preview export function usePreview() { const previewState = ref(null) const variableValues = ref>({}) // Procesar operaciones y generar preview function generatePreview(operations: Operation[], initialValues: Record = {}) { variableValues.value = { ...initialValues } previewState.value = processOperations(operations, variableValues.value) } // Actualizar valor de una variable function updateVariable(name: string, value: string, operations: Operation[]) { variableValues.value[name] = value previewState.value = processOperations(operations, variableValues.value) } // Obtener todas las variables únicas de las operaciones function extractVariables(operations: Operation[]): Array<{ name: string; defaultValue: string }> { const variables = new Map() for (const op of operations) { if (op.op === 'text' && op.value) { let match: RegExpExecArray | null VARIABLE_REGEX.lastIndex = 0 while ((match = VARIABLE_REGEX.exec(op.value)) !== null) { const [, name, , defaultValue] = match if (!variables.has(name)) { variables.set(name, defaultValue || '') } } } } return Array.from(variables.entries()).map(([name, defaultValue]) => ({ name, defaultValue })) } return { previewState: readonly(previewState), variableValues: readonly(variableValues), generatePreview, updateVariable, extractVariables, CHAR_LIMITS } }