refactor & improvements

This commit is contained in:
2026-03-29 22:35:57 +03:00
parent cdaf91927b
commit f0a1835fa2
63 changed files with 4803 additions and 7387 deletions

View File

@@ -0,0 +1,178 @@
import { ref } from 'vue'
import type { ElementLayout } from '../core/layout-types'
export interface SnapGuide {
type: 'vertical' | 'horizontal'
position_mm: number
}
export interface SnapResult {
snappedX_mm: number
snappedY_mm: number
guides: SnapGuide[]
}
interface EdgeSet {
verticals: number[] // x positions in mm (left, right, center of elements + page)
horizontals: number[] // y positions in mm (top, bottom, center of elements + page)
}
export function useSnapGuides() {
const SNAP_THRESHOLD_MM = 1.5
const activeGuides = ref<SnapGuide[]>([])
let cachedEdges: EdgeSet | null = null
/** Collect edges from all elements except the one being dragged. Call once on drag start. */
function collectEdges(
layoutMap: Record<string, ElementLayout>,
excludeId: string,
pageWidth: number,
pageHeight: number
) {
const verticals: number[] = [0, pageWidth / 2, pageWidth] // page edges + center
const horizontals: number[] = [0, pageHeight / 2, pageHeight]
for (const [id, el] of Object.entries(layoutMap)) {
if (id === excludeId) continue
// Left, center, right
verticals.push(el.x_mm, el.x_mm + el.width_mm / 2, el.x_mm + el.width_mm)
// Top, center, bottom
horizontals.push(el.y_mm, el.y_mm + el.height_mm / 2, el.y_mm + el.height_mm)
}
cachedEdges = { verticals, horizontals }
}
/** Calculate snap for a dragged element. Returns adjusted position + active guides. */
function calculateSnap(
proposedX_mm: number,
proposedY_mm: number,
width_mm: number,
height_mm: number
): SnapResult {
if (!cachedEdges) {
return { snappedX_mm: proposedX_mm, snappedY_mm: proposedY_mm, guides: [] }
}
const guides: SnapGuide[] = []
let snappedX = proposedX_mm
let snappedY = proposedY_mm
// Element edges to check
const myLeft = proposedX_mm
const myCenter = proposedX_mm + width_mm / 2
const myRight = proposedX_mm + width_mm
// Find closest vertical snap
let bestVDist = SNAP_THRESHOLD_MM
let bestVSnap: { edge: number; offset: number } | null = null
for (const v of cachedEdges.verticals) {
// Check left edge
const dLeft = Math.abs(myLeft - v)
if (dLeft < bestVDist) {
bestVDist = dLeft
bestVSnap = { edge: v, offset: 0 }
}
// Check center
const dCenter = Math.abs(myCenter - v)
if (dCenter < bestVDist) {
bestVDist = dCenter
bestVSnap = { edge: v, offset: width_mm / 2 }
}
// Check right edge
const dRight = Math.abs(myRight - v)
if (dRight < bestVDist) {
bestVDist = dRight
bestVSnap = { edge: v, offset: width_mm }
}
}
if (bestVSnap) {
snappedX = bestVSnap.edge - bestVSnap.offset
guides.push({ type: 'vertical', position_mm: bestVSnap.edge })
}
// Element edges to check (Y axis)
const myTop = proposedY_mm
const myMiddle = proposedY_mm + height_mm / 2
const myBottom = proposedY_mm + height_mm
// Find closest horizontal snap
let bestHDist = SNAP_THRESHOLD_MM
let bestHSnap: { edge: number; offset: number } | null = null
for (const h of cachedEdges.horizontals) {
const dTop = Math.abs(myTop - h)
if (dTop < bestHDist) {
bestHDist = dTop
bestHSnap = { edge: h, offset: 0 }
}
const dMiddle = Math.abs(myMiddle - h)
if (dMiddle < bestHDist) {
bestHDist = dMiddle
bestHSnap = { edge: h, offset: height_mm / 2 }
}
const dBottom = Math.abs(myBottom - h)
if (dBottom < bestHDist) {
bestHDist = dBottom
bestHSnap = { edge: h, offset: height_mm }
}
}
if (bestHSnap) {
snappedY = bestHSnap.edge - bestHSnap.offset
guides.push({ type: 'horizontal', position_mm: bestHSnap.edge })
}
activeGuides.value = guides
return { snappedX_mm: snappedX, snappedY_mm: snappedY, guides }
}
/** Calculate snap for resize edge */
function calculateResizeSnap(
edge: 'left' | 'right' | 'top' | 'bottom',
proposedValue_mm: number
): number {
if (!cachedEdges) return proposedValue_mm
const targets = (edge === 'left' || edge === 'right')
? cachedEdges.verticals
: cachedEdges.horizontals
const guides: SnapGuide[] = []
let snapped = proposedValue_mm
let bestDist = SNAP_THRESHOLD_MM
for (const t of targets) {
const d = Math.abs(proposedValue_mm - t)
if (d < bestDist) {
bestDist = d
snapped = t
}
}
if (snapped !== proposedValue_mm) {
guides.push({
type: (edge === 'left' || edge === 'right') ? 'vertical' : 'horizontal',
position_mm: snapped,
})
}
activeGuides.value = guides
return snapped
}
function clearGuides() {
activeGuides.value = []
cachedEdges = null
}
return {
activeGuides,
collectEdges,
calculateSnap,
calculateResizeSnap,
clearGuides,
}
}

View File

@@ -1,90 +0,0 @@
import { ref, watch, type Ref } from 'vue'
import type { ElementLayout } from '../core/template-to-typst'
import type { Template } from '../core/types'
export function useTypstCompiler(
template: Ref<Template>,
data: Ref<Record<string, unknown>>,
) {
const svg = ref<string | null>(null)
const error = ref<string | null>(null)
const compiling = ref(false)
const layout = ref<Record<string, ElementLayout>>({})
let worker: Worker | null = null
let requestId = 0
let debounceTimer: ReturnType<typeof setTimeout> | null = null
function initWorker() {
worker = new Worker(new URL('../workers/typst.worker.ts', import.meta.url), {
type: 'module',
})
worker.onmessage = (e: MessageEvent<{
type: string
svg?: string
layout?: Record<string, ElementLayout>
error?: string
id: number
}>) => {
const data = e.data
if (data.id !== requestId) return
compiling.value = false
if (data.type === 'result') {
svg.value = data.svg ?? null
layout.value = data.layout ?? {}
error.value = null
} else if (data.type === 'error') {
error.value = data.error ?? 'Bilinmeyen derleme hatası'
}
}
worker.onerror = () => {
compiling.value = false
error.value = 'Worker hatası — yeniden başlatılıyor'
worker?.terminate()
worker = null
setTimeout(initWorker, 500)
}
}
function compile() {
if (!worker) initWorker()
requestId++
compiling.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 derle
watch(
[template, data],
() => {
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
compile()
}, 200)
},
{ immediate: true, deep: true }
)
function dispose() {
worker?.terminate()
worker = null
if (debounceTimer) clearTimeout(debounceTimer)
}
return {
svg,
error,
compiling,
layout,
compile,
dispose,
}
}