Files
dreport/frontend/src/composables/useLayoutEngine.ts
Duhan BALCI 5ffc6d866c
Some checks failed
CI / frontend (push) Successful in 1m55s
CI / publish-crates (push) Successful in 23s
CI / rust (push) Successful in 49s
CI / wasm (push) Successful in 1m44s
CI / publish-npm (push) Failing after 1m50s
format
2026-04-07 01:56:40 +03:00

186 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
}
}