mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-01 18:39:16 +00:00
186 lines
4.9 KiB
TypeScript
186 lines
4.9 KiB
TypeScript
import { ref, watch, type Ref } from 'vue'
|
||
import type { Template } from '../core/types'
|
||
import type { LayoutResult, LayoutMapEntry } from '../core/layout-types'
|
||
|
||
export type { LayoutMapEntry }
|
||
|
||
/** Discriminated union for all messages the layout worker can send back */
|
||
type WorkerResponse =
|
||
| { type: 'result'; layout: LayoutResult; id: number }
|
||
| { type: 'error'; error: string; id: number }
|
||
| { type: 'barcode-result'; width: number; height: number; rgba: ArrayBuffer; id: number }
|
||
| { type: 'barcode-error'; error: string; id: number }
|
||
|
||
export interface LayoutEngineOptions {
|
||
/** Font API base URL. Default: '/api/fonts' */
|
||
fontApiBase?: string
|
||
}
|
||
|
||
export function useLayoutEngine(
|
||
template: Ref<Template>,
|
||
data: Ref<Record<string, unknown>>,
|
||
layoutVersion?: Ref<number>,
|
||
options?: LayoutEngineOptions,
|
||
) {
|
||
const layout = ref<LayoutResult | null>(null)
|
||
const error = ref<string | null>(null)
|
||
const computing = ref(false)
|
||
|
||
// Uyumluluk: InteractionOverlay'ın beklediği flat layout map (id → ElementLayout)
|
||
const layoutMap = ref<Record<string, LayoutMapEntry>>({})
|
||
|
||
let worker: Worker | null = null
|
||
let requestId = 0
|
||
|
||
function initWorker() {
|
||
worker = new Worker(new URL('../workers/layout.worker.ts', import.meta.url), {
|
||
type: 'module',
|
||
})
|
||
|
||
// Configure font API base if provided
|
||
if (options?.fontApiBase) {
|
||
worker.postMessage({ type: 'configure', fontApiBase: options.fontApiBase })
|
||
}
|
||
|
||
worker.onmessage = (e: MessageEvent<WorkerResponse>) => {
|
||
const msg = e.data
|
||
|
||
// Barcode yanıtları
|
||
if (msg.type === 'barcode-result' || msg.type === 'barcode-error') {
|
||
handleBarcodeResponse(msg)
|
||
return
|
||
}
|
||
|
||
if (msg.id !== requestId) return
|
||
|
||
computing.value = false
|
||
switch (msg.type) {
|
||
case 'result': {
|
||
layout.value = msg.layout
|
||
error.value = null
|
||
|
||
// Flat map oluştur: id → LayoutMapEntry (pageIndex dahil)
|
||
const map: Record<string, LayoutMapEntry> = {}
|
||
for (const page of msg.layout.pages) {
|
||
for (const el of page.elements) {
|
||
if (!map[el.id]) {
|
||
map[el.id] = { ...el, pageIndex: page.page_index }
|
||
}
|
||
}
|
||
}
|
||
layoutMap.value = map
|
||
break
|
||
}
|
||
case 'error':
|
||
error.value = msg.error ?? 'Bilinmeyen layout hatası'
|
||
break
|
||
}
|
||
}
|
||
|
||
worker.onerror = () => {
|
||
computing.value = false
|
||
error.value = 'Worker hatası — yeniden başlatılıyor'
|
||
worker?.terminate()
|
||
worker = null
|
||
setTimeout(initWorker, 500)
|
||
}
|
||
}
|
||
|
||
function compute() {
|
||
if (!worker) initWorker()
|
||
requestId++
|
||
computing.value = true
|
||
worker!.postMessage({
|
||
type: 'compile',
|
||
templateJson: JSON.stringify(template.value),
|
||
dataJson: JSON.stringify(data.value),
|
||
id: requestId,
|
||
})
|
||
}
|
||
|
||
// template veya data değiştiğinde yeniden hesapla.
|
||
// layoutVersion verilmişse sadece onu izle (cheap integer comparison).
|
||
// Verilmemişse eski davranış: deep watch (geriye uyumluluk).
|
||
if (layoutVersion) {
|
||
watch(
|
||
layoutVersion,
|
||
() => {
|
||
compute()
|
||
},
|
||
{ immediate: true },
|
||
)
|
||
} else {
|
||
watch(
|
||
[template, data],
|
||
() => {
|
||
compute()
|
||
},
|
||
{ immediate: true, deep: true },
|
||
)
|
||
}
|
||
|
||
// --- Barcode üretimi (WASM üzerinden) ---
|
||
let barcodeReqId = 0
|
||
const barcodeCallbacks = new Map<
|
||
number,
|
||
(result: { width: number; height: number; rgba: ArrayBuffer } | null) => void
|
||
>()
|
||
|
||
function generateBarcode(
|
||
format: string,
|
||
value: string,
|
||
width: number,
|
||
height: number,
|
||
includeText: boolean = false,
|
||
): Promise<{ width: number; height: number; rgba: ArrayBuffer } | null> {
|
||
if (!worker) initWorker()
|
||
return new Promise((resolve) => {
|
||
barcodeReqId++
|
||
const id = barcodeReqId
|
||
const timeout = setTimeout(() => {
|
||
barcodeCallbacks.delete(id)
|
||
resolve(null)
|
||
}, 5000)
|
||
barcodeCallbacks.set(id, (result) => {
|
||
clearTimeout(timeout)
|
||
resolve(result)
|
||
})
|
||
worker!.postMessage({ type: 'barcode', format, value, width, height, includeText, id })
|
||
})
|
||
}
|
||
|
||
function handleBarcodeResponse(
|
||
msg: Extract<WorkerResponse, { type: 'barcode-result' } | { type: 'barcode-error' }>,
|
||
) {
|
||
const cb = barcodeCallbacks.get(msg.id)
|
||
if (cb) {
|
||
barcodeCallbacks.delete(msg.id)
|
||
cb(
|
||
msg.type === 'barcode-result'
|
||
? { width: msg.width, height: msg.height, rgba: msg.rgba }
|
||
: null,
|
||
)
|
||
}
|
||
}
|
||
|
||
function dispose() {
|
||
worker?.terminate()
|
||
worker = null
|
||
// Bekleyen barcode promise'lerini null ile resolve et
|
||
for (const cb of barcodeCallbacks.values()) {
|
||
cb(null)
|
||
}
|
||
barcodeCallbacks.clear()
|
||
}
|
||
|
||
return {
|
||
layout,
|
||
layoutMap,
|
||
error,
|
||
computing,
|
||
compute,
|
||
generateBarcode,
|
||
dispose,
|
||
}
|
||
}
|