editor performans
Some checks failed
CI / rust (push) Failing after 42s
CI / frontend (push) Failing after 1m50s
CI / wasm (push) Successful in 1m45s
CI / publish-crates (push) Has been skipped
CI / publish-npm (push) Has been skipped

This commit is contained in:
2026-04-07 17:24:25 +03:00
parent 238e911875
commit e574889e5d
2 changed files with 196 additions and 40 deletions

View File

@@ -45,9 +45,23 @@ provide('generateBarcode', generateBarcode)
watch(error, (val) => emit('compile-error', val)) watch(error, (val) => emit('compile-error', val))
// mm → px dönüşüm katsayısı // ============================================================
// Zoom gesture: CSS transform ile anlık geri bildirim,
// debounce ile gerçek scale commit
// ============================================================
// committedZoom: son commit edilen zoom seviyesi (bu değer scale'i belirler)
const committedZoom = ref(editorStore.zoom)
// Gesture sırasında hedef zoom/pan (henüz commit edilmedi)
const gestureZoom = ref(editorStore.zoom)
const gesturePanX = ref(editorStore.panX)
const gesturePanY = ref(editorStore.panY)
const isZoomGesture = ref(false)
let zoomCommitTimer: ReturnType<typeof setTimeout> | null = null
// mm → px dönüşüm katsayısı (committed zoom'a bağlı)
const scale = computed(() => { const scale = computed(() => {
return (containerWidth.value / templateStore.template.page.width) * editorStore.zoom return (containerWidth.value / templateStore.template.page.width) * committedZoom.value
}) })
// Layout sayfaları // Layout sayfaları
@@ -56,7 +70,50 @@ const layoutPages = computed(() => layout.value?.pages ?? [])
// Sayfa yüksekliği px cinsinden // Sayfa yüksekliği px cinsinden
const pageHeightPx = computed(() => templateStore.template.page.height * scale.value) const pageHeightPx = computed(() => templateStore.template.page.height * scale.value)
// Sayfalar container stili — tüm sayfaları kapsayan dış kutu // Görünür sayfa indeksleri — viewport dışındaki sayfaların DOM elemanları render edilmez
// Stabil: sadece gerçek indeksler değiştiğinde yeni Set oluştur
const _lastVisibleKey = ref('')
const _lastVisibleSet = ref(new Set<number>([0]))
const visiblePageIndices = computed(() => {
// Gesture sırasında gesture değerlerini, yoksa store değerlerini kullan
const currentPanY = isZoomGesture.value ? gesturePanY.value : editorStore.panY
const currentZoom = isZoomGesture.value ? gestureZoom.value : editorStore.zoom
const baseScale = containerWidth.value / templateStore.template.page.width
const currentScale = baseScale * currentZoom
const pageH = templateStore.template.page.height * currentScale
const gap = 24
const count = layoutPages.value.length
if (count === 0) return _lastVisibleSet.value
const pagesTop = 60 + currentPanY
const viewH = containerHeight.value
const indices: number[] = []
for (let i = 0; i < count; i++) {
const pageTop = pagesTop + i * (pageH + gap)
const pageBottom = pageTop + pageH
const buffer = pageH
if (pageBottom > -buffer && pageTop < viewH + buffer) {
indices.push(i)
}
}
const key = indices.join(',')
if (key !== _lastVisibleKey.value) {
_lastVisibleKey.value = key
_lastVisibleSet.value = new Set(indices)
}
return _lastVisibleSet.value
})
// CSS transform zoom oranı — gesture sırasında visual feedback
const zoomCssRatio = computed(() => {
if (!isZoomGesture.value) return 1
return gestureZoom.value / committedZoom.value
})
// Sayfalar container stili — committed scale'e göre
const pagesContainerStyle = computed(() => { const pagesContainerStyle = computed(() => {
const w = templateStore.template.page.width * scale.value const w = templateStore.template.page.width * scale.value
const m = templateStore.template.root.padding const m = templateStore.template.root.padding
@@ -68,6 +125,7 @@ const pagesContainerStyle = computed(() => {
height: `${totalH}px`, height: `${totalH}px`,
position: 'relative' as const, position: 'relative' as const,
flexShrink: 0, flexShrink: 0,
willChange: 'transform' as const,
'--page-margin-top': `${m.top * scale.value}px`, '--page-margin-top': `${m.top * scale.value}px`,
'--page-margin-right': `${m.right * scale.value}px`, '--page-margin-right': `${m.right * scale.value}px`,
'--page-margin-bottom': `${m.bottom * scale.value}px`, '--page-margin-bottom': `${m.bottom * scale.value}px`,
@@ -76,19 +134,19 @@ const pagesContainerStyle = computed(() => {
}) })
// Pan sınırları // Pan sınırları
// pan=0 → sayfa yatayda viewport ortasında, dikeyde üstte. function clampPan(x: number, y: number, zoomOverride?: number): [number, number] {
// Kural: sayfanın en az yarısı viewport'ta görünsün. const z = zoomOverride ?? committedZoom.value
function clampPan(x: number, y: number): [number, number] { const baseScale = containerWidth.value / templateStore.template.page.width
const pageW = templateStore.template.page.width * scale.value const s = baseScale * z
const pageW = templateStore.template.page.width * s
const pageCount = Math.max(1, layoutPages.value.length) const pageCount = Math.max(1, layoutPages.value.length)
const pageGap = 24 const pageGap = 24
const totalH = pageHeightPx.value * pageCount + pageGap * (pageCount - 1) const phPx = templateStore.template.page.height * s
const totalH = phPx * pageCount + pageGap * (pageCount - 1)
const viewH = (containerRef.value?.clientHeight ?? 600) - 60 - 40 const viewH = (containerRef.value?.clientHeight ?? 600) - 60 - 40
// Yatay: pageLeft = (viewW - pageW)/2 + panX → sayfanın yarısı viewport'ta kalmalı
const clampX = pageW / 2 const clampX = pageW / 2
// Dikey: pageTop = panY → sayfanın yarısı viewport'ta kalmalı
const maxY = viewH * 0.5 const maxY = viewH * 0.5
const minY = viewH * 0.5 - totalH const minY = viewH * 0.5 - totalH
@@ -98,12 +156,71 @@ function clampPan(x: number, y: number): [number, number] {
] ]
} }
// Pan transform — sayfa container'ına uygulanacak // Pages container transform — pan + gesture zoom CSS scale
const panTransform = computed(() => { const pagesTransform = computed(() => {
if (editorStore.panX === 0 && editorStore.panY === 0) return undefined const ratio = zoomCssRatio.value
return `translate(${editorStore.panX}px, ${editorStore.panY}px)` const panX = isZoomGesture.value ? gesturePanX.value : editorStore.panX
const panY = isZoomGesture.value ? gesturePanY.value : editorStore.panY
if (ratio === 1) {
if (panX === 0 && panY === 0) return undefined
return `translate(${panX}px, ${panY}px)`
}
// Scale from top-left (0 0). Centering düzeltmesi:
// Flex container ortalar → naturalLeft = (containerW - w) / 2
// Scale sonrası visual width = w * ratio, visual center kayar
// Düzeltme: tx += w * (1 - ratio) / 2
const w = templateStore.template.page.width * scale.value
const centerCorrection = w * (1 - ratio) / 2
const tx = panX + centerCorrection
const ty = panY
return `translate(${tx}px, ${ty}px) scale(${ratio})`
}) })
const pagesTransformOrigin = computed(() => {
if (zoomCssRatio.value === 1) return undefined
return '0 0'
})
// Zoom commit: gesture sonunda gerçek scale'i güncelle
function commitZoom() {
const z = gestureZoom.value
const px = gesturePanX.value
let py = gesturePanY.value
const ratio = z / committedZoom.value
const pageCount = layoutPages.value.length
// Gap düzeltmesi: CSS scale sırasında 24px gap'ler de ratio ile ölçekleniyor.
// Commit sonrası gap'ler tekrar 24px'e dönüyor → dikey kayma.
// Viewport merkezindeki sayfanın üstündeki gap sayısı × 24 × (ratio - 1) kadar düzelt.
if (ratio !== 1 && pageCount > 1) {
const pageH_dom = templateStore.template.page.height * scale.value // committed scale'de
const strideVisual = (pageH_dom + 24) * ratio
// Viewport merkezinin container visual koordinatındaki Y pozisyonu
const viewCenterY = containerHeight.value / 2 - 60 - py
if (viewCenterY > 0 && strideVisual > 0) {
const gapsAbove = Math.min(pageCount - 1, Math.max(0, Math.floor(viewCenterY / strideVisual)))
py += gapsAbove * 24 * (ratio - 1)
}
}
committedZoom.value = z
editorStore.setZoom(z)
const [cx, cy] = clampPan(px, py, z)
editorStore.setPan(cx, cy)
isZoomGesture.value = false
zoomCommitTimer = null
}
function scheduleZoomCommit() {
if (zoomCommitTimer) clearTimeout(zoomCommitTimer)
zoomCommitTimer = setTimeout(commitZoom, 120)
}
// Pan: Space+drag veya orta fare tuşu // Pan: Space+drag veya orta fare tuşu
const isPanning = ref(false) const isPanning = ref(false)
const panStart = ref({ x: 0, y: 0 }) const panStart = ref({ x: 0, y: 0 })
@@ -137,10 +254,19 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
resizeObserver?.disconnect() resizeObserver?.disconnect()
dispose() dispose()
if (zoomCommitTimer) clearTimeout(zoomCommitTimer)
window.removeEventListener('keydown', onKeyDown) window.removeEventListener('keydown', onKeyDown)
window.removeEventListener('keyup', onKeyUp) window.removeEventListener('keyup', onKeyUp)
}) })
// Store'daki zoom değiştiğinde (dışarıdan, ör. zoom butonları) committed'ı da güncelle
watch(() => editorStore.zoom, (z) => {
if (!isZoomGesture.value) {
committedZoom.value = z
gestureZoom.value = z
}
})
// Zoom & Pan via wheel/trackpad // Zoom & Pan via wheel/trackpad
const pageRef = ref<HTMLElement | null>(null) const pageRef = ref<HTMLElement | null>(null)
@@ -170,8 +296,17 @@ function onWheel(e: WheelEvent) {
} else { } else {
// İki parmak pan (touchpad) veya normal scroll // İki parmak pan (touchpad) veya normal scroll
e.preventDefault() e.preventDefault()
const [cx, cy] = clampPan(editorStore.panX - e.deltaX, editorStore.panY - e.deltaY) const curPanX = isZoomGesture.value ? gesturePanX.value : editorStore.panX
editorStore.setPan(cx, cy) const curPanY = isZoomGesture.value ? gesturePanY.value : editorStore.panY
const curZoom = isZoomGesture.value ? gestureZoom.value : editorStore.zoom
const [cx, cy] = clampPan(curPanX - e.deltaX, curPanY - e.deltaY, curZoom)
if (isZoomGesture.value) {
gesturePanX.value = cx
gesturePanY.value = cy
} else {
editorStore.setPan(cx, cy)
}
} }
} }
@@ -179,30 +314,40 @@ function applyZoom(delta: number, clientX: number, clientY: number) {
const pageEl = pageRef.value const pageEl = pageRef.value
if (!pageEl) return if (!pageEl) return
const oldZoom = editorStore.zoom // Gesture başlat veya devam et
if (!isZoomGesture.value) {
isZoomGesture.value = true
gestureZoom.value = editorStore.zoom
gesturePanX.value = editorStore.panX
gesturePanY.value = editorStore.panY
}
const oldZoom = gestureZoom.value
const zoomFactor = Math.pow(0.99, delta) const zoomFactor = Math.pow(0.99, delta)
const newZoom = Math.max(0.25, Math.min(4, oldZoom * zoomFactor)) const newZoom = Math.max(0.25, Math.min(4, oldZoom * zoomFactor))
if (newZoom === oldZoom) return if (newZoom === oldZoom) return
// Sayfa elemanının şu anki ekran pozisyonunu al (centering + pan dahil)
const pageRect = pageEl.getBoundingClientRect()
// Mouse'un sayfa üzerindeki pozisyonu (mm cinsinden) // Mouse'un sayfa üzerindeki pozisyonu (mm cinsinden)
// pageRef'in ekran pozisyonunu al (CSS transform dahil)
const pageRect = pageEl.getBoundingClientRect()
const baseScale = containerWidth.value / templateStore.template.page.width const baseScale = containerWidth.value / templateStore.template.page.width
const oldScale = baseScale * oldZoom const oldGestureScale = baseScale * oldZoom
const newScale = baseScale * newZoom const newGestureScale = baseScale * newZoom
const mousePageMmX = (clientX - pageRect.left) / oldScale const mousePageMmX = (clientX - pageRect.left) / oldGestureScale
const mousePageMmY = (clientY - pageRect.top) / oldScale const mousePageMmY = (clientY - pageRect.top) / oldGestureScale
const pageW = templateStore.template.page.width const pageW = templateStore.template.page.width
// Yeni pan: mouse'un gösterdiği mm noktası aynı ekran pozisyonunda kalmalı // Yeni pan: mouse'un gösterdiği mm noktası aynı ekran pozisyonunda kalmalı
const newPanX = editorStore.panX + (mousePageMmX - pageW / 2) * (oldScale - newScale) const newPanX = gesturePanX.value + (mousePageMmX - pageW / 2) * (oldGestureScale - newGestureScale)
const newPanY = editorStore.panY + mousePageMmY * (oldScale - newScale) const newPanY = gesturePanY.value + mousePageMmY * (oldGestureScale - newGestureScale)
editorStore.setZoom(newZoom) gestureZoom.value = newZoom
const [cx, cy] = clampPan(newPanX, newPanY) const [cx, cy] = clampPan(newPanX, newPanY, newZoom)
editorStore.setPan(cx, cy) gesturePanX.value = cx
gesturePanY.value = cy
scheduleZoomCommit()
} }
function onKeyDown(e: KeyboardEvent) { function onKeyDown(e: KeyboardEvent) {
@@ -253,6 +398,12 @@ function onMinimapNavigate(x: number, y: number) {
const [cx, cy] = clampPan(x, y) const [cx, cy] = clampPan(x, y)
editorStore.setPan(cx, cy) editorStore.setPan(cx, cy)
} }
// Minimap'e gerçek scale'i geçir (gesture dahil)
const minimapScale = computed(() => {
const z = isZoomGesture.value ? gestureZoom.value : editorStore.zoom
return (containerWidth.value / templateStore.template.page.width) * z
})
</script> </script>
<template> <template>
@@ -261,9 +412,9 @@ function onMinimapNavigate(x: number, y: number) {
<RulerBar <RulerBar
:page-width="templateStore.template.page.width" :page-width="templateStore.template.page.width"
:page-height="templateStore.template.page.height" :page-height="templateStore.template.page.height"
:scale="scale" :scale="minimapScale"
:pan-x="editorStore.panX" :pan-x="isZoomGesture ? gesturePanX : editorStore.panX"
:pan-y="editorStore.panY" :pan-y="isZoomGesture ? gesturePanY : editorStore.panY"
:container-width="containerWidth" :container-width="containerWidth"
:page-count="layoutPages.length" :page-count="layoutPages.length"
:page-gap="24" :page-gap="24"
@@ -283,9 +434,13 @@ function onMinimapNavigate(x: number, y: number) {
<div <div
ref="pageRef" ref="pageRef"
class="editor-canvas__pages" class="editor-canvas__pages"
:style="[pagesContainerStyle, panTransform ? { transform: panTransform } : {}]" :style="[
pagesContainerStyle,
pagesTransform ? { transform: pagesTransform } : {},
pagesTransformOrigin ? { transformOrigin: pagesTransformOrigin } : {},
]"
> >
<LayoutRenderer :layout="layout" :scale="scale" /> <LayoutRenderer :layout="layout" :scale="scale" :visible-page-indices="visiblePageIndices" />
<InteractionOverlay <InteractionOverlay
:scale="scale" :scale="scale"
:layout-map="layoutMap" :layout-map="layoutMap"
@@ -307,16 +462,16 @@ function onMinimapNavigate(x: number, y: number) {
:layout="layout" :layout="layout"
:page-width="templateStore.template.page.width" :page-width="templateStore.template.page.width"
:page-height="templateStore.template.page.height" :page-height="templateStore.template.page.height"
:zoom="editorStore.zoom" :zoom="isZoomGesture ? gestureZoom : editorStore.zoom"
:pan-x="editorStore.panX" :pan-x="isZoomGesture ? gesturePanX : editorStore.panX"
:pan-y="editorStore.panY" :pan-y="isZoomGesture ? gesturePanY : editorStore.panY"
:container-width="containerWidth" :container-width="containerWidth"
:container-height="containerHeight" :container-height="containerHeight"
:scale="scale" :scale="minimapScale"
:page-gap="24" :page-gap="24"
@navigate="onMinimapNavigate" @navigate="onMinimapNavigate"
/> />
<div class="editor-canvas__zoom">%{{ editorStore.zoomPercent }}</div> <div class="editor-canvas__zoom">%{{ Math.round((isZoomGesture ? gestureZoom : editorStore.zoom) * 100) }}</div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -5,6 +5,7 @@ import type { ElementLayout, PageLayout, LayoutResult } from '../../core/layout-
const props = defineProps<{ const props = defineProps<{
layout: LayoutResult | null layout: LayoutResult | null
scale: number scale: number
visiblePageIndices?: Set<number>
}>() }>()
// WASM barcode üretme fonksiyonu (EditorCanvas'tan provide edilir) // WASM barcode üretme fonksiyonu (EditorCanvas'tan provide edilir)
@@ -196,7 +197,7 @@ watch(
class="layout-page" class="layout-page"
:style="pageContainerStyle(page)" :style="pageContainerStyle(page)"
> >
<template v-for="el in page.elements" :key="el.id"> <template v-if="!visiblePageIndices || visiblePageIndices.has(pageIdx)" v-for="el in page.elements" :key="el.id">
<!-- Page break: dashed horizontal line --> <!-- Page break: dashed horizontal line -->
<div <div
v-if="el.element_type === 'page_break'" v-if="el.element_type === 'page_break'"