diff --git a/Cargo.lock b/Cargo.lock index 550b3bf..8510eed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -397,9 +397,9 @@ dependencies = [ [[package]] name = "dexpr" -version = "0.1.0" +version = "0.3.0" source = "sparse+https://gitea.duhanbalci.com/api/packages/duhanbalci/cargo/" -checksum = "37e0a98f2810bb770c76ef1e99d07066a15997086f9ead93917a82711274af25" +checksum = "e65e74adffaab8b52681e3e3e5006365f0f8c5e3e07870cbd58ca74769eb150a" dependencies = [ "bumpalo", "indexmap", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 71ae692..b4347cf 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -129,45 +129,24 @@ const sampleData: Record = { telefon: '+90 216 444 0018', }, kalemler: [ - { - siraNo: 1, - adi: 'Web Uygulama Gelistirme', - miktar: 1, - birim: 'Adet', - birimFiyat: 45000, - tutar: 45000, - }, - { - siraNo: 2, - adi: 'Mobil Uygulama Gelistirme', - miktar: 1, - birim: 'Adet', - birimFiyat: 35000, - tutar: 35000, - }, - { - siraNo: 3, - adi: 'UI/UX Tasarim Hizmeti', - miktar: 40, - birim: 'Saat', - birimFiyat: 750, - tutar: 30000, - }, - { - siraNo: 4, - adi: 'Sunucu Bakim Sozlesmesi (Yillik)', - miktar: 1, - birim: 'Adet', - birimFiyat: 12000, - tutar: 12000, - }, + { siraNo: 1, adi: 'Web Uygulama Gelistirme', miktar: 1, birim: 'Adet', birimFiyat: 45000, tutar: 45000 }, + { siraNo: 2, adi: 'Mobil Uygulama Gelistirme', miktar: 1, birim: 'Adet', birimFiyat: 35000, tutar: 35000 }, + { siraNo: 3, adi: 'UI/UX Tasarim Hizmeti', miktar: 40, birim: 'Saat', birimFiyat: 750, tutar: 30000 }, + { siraNo: 4, adi: 'Sunucu Bakim Sozlesmesi (Yillik)', miktar: 1, birim: 'Adet', birimFiyat: 12000, tutar: 12000 }, { siraNo: 5, adi: 'SSL Sertifikasi', miktar: 3, birim: 'Adet', birimFiyat: 500, tutar: 1500 }, + { siraNo: 6, adi: 'Veritabani Yonetimi', miktar: 12, birim: 'Ay', birimFiyat: 2000, tutar: 24000 }, + { siraNo: 7, adi: 'API Entegrasyon Hizmeti', miktar: 1, birim: 'Adet', birimFiyat: 18000, tutar: 18000 }, + { siraNo: 8, adi: 'Bulut Altyapi Kurulumu', miktar: 1, birim: 'Adet', birimFiyat: 8000, tutar: 8000 }, + { siraNo: 9, adi: 'Siber Guvenlik Danismanligi', miktar: 20, birim: 'Saat', birimFiyat: 900, tutar: 18000 }, + { siraNo: 10, adi: 'E-posta Sunucu Yapilandirmasi', miktar: 1, birim: 'Adet', birimFiyat: 3500, tutar: 3500 }, + { siraNo: 11, adi: 'Yedekleme Sistemi Kurulumu', miktar: 1, birim: 'Adet', birimFiyat: 5000, tutar: 5000 }, + { siraNo: 12, adi: 'SEO Optimizasyonu', miktar: 1, birim: 'Adet', birimFiyat: 7500, tutar: 7500 }, + { siraNo: 13, adi: 'Egitim ve Dokumantasyon', miktar: 8, birim: 'Saat', birimFiyat: 600, tutar: 4800 }, + { siraNo: 14, adi: 'Performans Testi ve Raporlama', miktar: 1, birim: 'Adet', birimFiyat: 6000, tutar: 6000 }, + { siraNo: 15, adi: 'Teknik Destek Paketi (6 Ay)', miktar: 1, birim: 'Adet', birimFiyat: 9000, tutar: 9000 }, ], toplamlar: { - araToplam: 123500, kdvOrani: 20, - kdv: 24700, - genelToplam: 148200, }, } @@ -480,22 +459,66 @@ const defaultInvoiceTemplate: Template = { style: { borderColor: '#e2e8f0', borderWidth: 0.5 }, children: [ { - id: 'el_ara_toplam', - type: 'text', + id: 'c_ara_toplam_row', + type: 'container', position: { type: 'flow' }, - size: { width: sz.auto(), height: sz.auto() }, - style: { fontSize: 10, color: '#333333', align: 'right' }, - content: 'Ara Toplam: ', - binding: { type: 'scalar', path: 'toplamlar.araToplam' }, + size: { width: sz.fr(), height: sz.auto() }, + direction: 'row', + gap: 2, + padding: { top: 0, right: 0, bottom: 0, left: 0 }, + align: 'center', + justify: 'space-between', + style: {}, + children: [ + { + id: 'el_ara_toplam_label', + type: 'static_text', + position: { type: 'flow' }, + size: { width: sz.auto(), height: sz.auto() }, + style: { fontSize: 10, color: '#333333' }, + content: 'Ara Toplam:', + }, + { + id: 'el_ara_toplam', + type: 'calculated_text', + position: { type: 'flow' }, + size: { width: sz.auto(), height: sz.auto() }, + style: { fontSize: 10, color: '#333333', align: 'right' }, + expression: 'kalemler.tutar.sum()', + format: 'currency', + }, + ], }, { - id: 'el_kdv', - type: 'text', + id: 'c_kdv_row', + type: 'container', position: { type: 'flow' }, - size: { width: sz.auto(), height: sz.auto() }, - style: { fontSize: 10, color: '#333333', align: 'right' }, - content: 'KDV (%20): ', - binding: { type: 'scalar', path: 'toplamlar.kdv' }, + size: { width: sz.fr(), height: sz.auto() }, + direction: 'row', + gap: 2, + padding: { top: 0, right: 0, bottom: 0, left: 0 }, + align: 'center', + justify: 'space-between', + style: {}, + children: [ + { + id: 'el_kdv_label', + type: 'static_text', + position: { type: 'flow' }, + size: { width: sz.auto(), height: sz.auto() }, + style: { fontSize: 10, color: '#333333' }, + content: 'KDV (%20):', + }, + { + id: 'el_kdv', + type: 'calculated_text', + position: { type: 'flow' }, + size: { width: sz.auto(), height: sz.auto() }, + style: { fontSize: 10, color: '#333333', align: 'right' }, + expression: 'kalemler.tutar.sum() * toplamlar.kdvOrani / 100', + format: 'currency', + }, + ], }, { id: 'el_cizgi_2', @@ -505,13 +528,35 @@ const defaultInvoiceTemplate: Template = { style: { strokeColor: '#1e293b', strokeWidth: 1 }, }, { - id: 'el_genel_toplam', - type: 'text', + id: 'c_genel_toplam_row', + type: 'container', position: { type: 'flow' }, - size: { width: sz.auto(), height: sz.auto() }, - style: { fontSize: 12, fontWeight: 'bold', color: '#1a1a1a', align: 'right' }, - content: 'GENEL TOPLAM: ', - binding: { type: 'scalar', path: 'toplamlar.genelToplam' }, + size: { width: sz.fr(), height: sz.auto() }, + direction: 'row', + gap: 2, + padding: { top: 0, right: 0, bottom: 0, left: 0 }, + align: 'center', + justify: 'space-between', + style: {}, + children: [ + { + id: 'el_genel_toplam_label', + type: 'static_text', + position: { type: 'flow' }, + size: { width: sz.auto(), height: sz.auto() }, + style: { fontSize: 12, fontWeight: 'bold', color: '#1a1a1a' }, + content: 'GENEL TOPLAM:', + }, + { + id: 'el_genel_toplam', + type: 'calculated_text', + position: { type: 'flow' }, + size: { width: sz.auto(), height: sz.auto() }, + style: { fontSize: 12, fontWeight: 'bold', color: '#1a1a1a', align: 'right' }, + expression: 'kalemler.tutar.sum() * (1 + toplamlar.kdvOrani / 100)', + format: 'currency', + }, + ], }, ], }, diff --git a/frontend/src/components/editor/EditorCanvas.vue b/frontend/src/components/editor/EditorCanvas.vue index 6fa298a..8b357f2 100644 --- a/frontend/src/components/editor/EditorCanvas.vue +++ b/frontend/src/components/editor/EditorCanvas.vue @@ -7,6 +7,7 @@ import { useLayoutEngine } from '../../composables/useLayoutEngine' import LayoutRenderer from './LayoutRenderer.vue' import InteractionOverlay from './InteractionOverlay.vue' import RulerBar from './RulerBar.vue' +import MinimapOverlay from './MinimapOverlay.vue' const props = withDefaults( defineProps<{ @@ -23,6 +24,7 @@ const { template, mockData, layoutVersion } = storeToRefs(templateStore) const containerRef = ref(null) const containerWidth = ref(800) +const containerHeight = ref(600) const emit = defineEmits<{ 'compile-error': [error: string | null] @@ -73,6 +75,29 @@ const pagesContainerStyle = computed(() => { } }) +// Pan sınırları +// pan=0 → sayfa yatayda viewport ortasında, dikeyde üstte. +// Kural: sayfanın en az yarısı viewport'ta görünsün. +function clampPan(x: number, y: number): [number, number] { + const pageW = templateStore.template.page.width * scale.value + const pageCount = Math.max(1, layoutPages.value.length) + const pageGap = 24 + const totalH = pageHeightPx.value * pageCount + pageGap * (pageCount - 1) + + 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 + // Dikey: pageTop = panY → sayfanın yarısı viewport'ta kalmalı + const maxY = viewH * 0.5 + const minY = viewH * 0.5 - totalH + + return [ + Math.max(-clampX, Math.min(clampX, x)), + Math.max(minY, Math.min(maxY, y)), + ] +} + // Pan transform — sayfa container'ına uygulanacak const panTransform = computed(() => { if (editorStore.panX === 0 && editorStore.panY === 0) return undefined @@ -98,7 +123,10 @@ onMounted(() => { if (containerRef.value) { resizeObserver = new ResizeObserver((entries) => { const entry = entries[0] - if (entry) containerWidth.value = entry.contentRect.width + if (entry) { + containerWidth.value = entry.contentRect.width + containerHeight.value = entry.contentRect.height + } }) resizeObserver.observe(containerRef.value) } @@ -142,7 +170,8 @@ function onWheel(e: WheelEvent) { } else { // İki parmak pan (touchpad) veya normal scroll e.preventDefault() - editorStore.setPan(editorStore.panX - e.deltaX, editorStore.panY - e.deltaY) + const [cx, cy] = clampPan(editorStore.panX - e.deltaX, editorStore.panY - e.deltaY) + editorStore.setPan(cx, cy) } } @@ -172,7 +201,8 @@ function applyZoom(delta: number, clientX: number, clientY: number) { const newPanY = editorStore.panY + mousePageMmY * (oldScale - newScale) editorStore.setZoom(newZoom) - editorStore.setPan(newPanX, newPanY) + const [cx, cy] = clampPan(newPanX, newPanY) + editorStore.setPan(cx, cy) } function onKeyDown(e: KeyboardEvent) { @@ -208,7 +238,8 @@ function onPointerDown(e: PointerEvent) { function onPointerMove(e: PointerEvent) { if (!isPanning.value) return - editorStore.setPan(e.clientX - panStart.value.x, e.clientY - panStart.value.y) + const [cx2, cy2] = clampPan(e.clientX - panStart.value.x, e.clientY - panStart.value.y) + editorStore.setPan(cx2, cy2) } function onPointerUp(e: PointerEvent) { @@ -217,6 +248,11 @@ function onPointerUp(e: PointerEvent) { ;(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId) } } + +function onMinimapNavigate(x: number, y: number) { + const [cx, cy] = clampPan(x, y) + editorStore.setPan(cx, cy) +} @@ -318,15 +374,22 @@ function onPointerUp(e: PointerEvent) { z-index: 100; } -.editor-canvas__zoom { +.editor-canvas__minimap-area { position: absolute; bottom: 12px; right: 16px; + z-index: 100; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 6px; +} + +.editor-canvas__zoom { background: rgba(0, 0, 0, 0.6); color: white; border-radius: 4px; padding: 2px 8px; font-size: 12px; - z-index: 100; } diff --git a/frontend/src/components/editor/MinimapOverlay.vue b/frontend/src/components/editor/MinimapOverlay.vue new file mode 100644 index 0000000..baf94ee --- /dev/null +++ b/frontend/src/components/editor/MinimapOverlay.vue @@ -0,0 +1,442 @@ + + + + + diff --git a/frontend/src/components/editor/RulerBar.vue b/frontend/src/components/editor/RulerBar.vue index 17e55f0..a6dde18 100644 --- a/frontend/src/components/editor/RulerBar.vue +++ b/frontend/src/components/editor/RulerBar.vue @@ -12,6 +12,12 @@ const props = defineProps<{ panX: number /** Pan offset Y (px) */ panY: number + /** editor-canvas content width (px) — ResizeObserver'dan */ + containerWidth: number + /** Sayfa sayısı */ + pageCount: number + /** Sayfalar arası boşluk (px) */ + pageGap?: number /** Cetvel kalınlığı px */ rulerSize?: number }>() @@ -69,19 +75,8 @@ function drawTicks( size: number, ) { const s = props.scale - const pageMm = direction === 'horizontal' ? props.pageWidth : props.pageHeight - const pan = direction === 'horizontal' ? props.panX : props.panY - - // Sayfa başlangıcı: ortaya hizalı + pan - // EditorCanvas sayfayı ortalar, ruler da buna uymalı - // Yatay: canvas ortası - sayfa genişliği/2 - // Sayfanın canvas üzerindeki orijin px'i - const canvasCenter = - direction === 'horizontal' - ? length / 2 // flex centering approximation - : 40 // EditorCanvas padding-top: 40px - - const pageStartPx = canvasCenter - (pageMm * s) / 2 + pan + const rulerSz = RULER_SIZE.value + const gap = props.pageGap ?? 24 // Tick aralığı belirleme (zoom'a göre) const mmPerPx = 1 / s @@ -98,11 +93,41 @@ function drawTicks( ctx.font = '9px system-ui, sans-serif' ctx.textBaseline = 'top' - // Sayfanın mm aralığını çiz - const startMm = 0 - const endMm = pageMm + if (direction === 'horizontal') { + // Yatay cetvel: tek sayfa genişliği, flex-center ile hizalı + // editor-canvas padding: left=60, right=40; ruler canvas left=rulerSize + // pageLeft_in_wrapper = 60 + (containerWidth - pageWidthPx) / 2 + // pageLeft_in_ruler = pageLeft_in_wrapper - rulerSz + panX + const pageWidthPx = props.pageWidth * s + const pageStartPx = (60 - rulerSz) + (props.containerWidth - pageWidthPx) / 2 + props.panX - for (let mm = startMm; mm <= endMm; mm += tickMm) { + drawPageTicks(ctx, direction, length, size, pageStartPx, props.pageWidth, tickMm) + } else { + // Dikey cetvel: her sayfa için ayrı tick çiz + // editor-canvas padding-top=60; ruler canvas top=rulerSize + // pageTop for page i = (60 - rulerSz) + panY + i * (pageHeightPx + gap) + const pageHeightPx = props.pageHeight * s + const pageCount = Math.max(1, props.pageCount) + + for (let i = 0; i < pageCount; i++) { + const pageStartPx = (60 - rulerSz) + props.panY + i * (pageHeightPx + gap) + drawPageTicks(ctx, direction, length, size, pageStartPx, props.pageHeight, tickMm) + } + } +} + +function drawPageTicks( + ctx: CanvasRenderingContext2D, + direction: 'horizontal' | 'vertical', + length: number, + size: number, + pageStartPx: number, + pageMm: number, + tickMm: number, +) { + const s = props.scale + + for (let mm = 0; mm <= pageMm; mm += tickMm) { const px = pageStartPx + mm * s if (px < -10 || px > length + 10) continue @@ -141,7 +166,7 @@ function drawTicks( } } - // Sayfa kenar çizgileri (margin göstergesi) + // Sayfa kenar çizgileri ctx.strokeStyle = 'rgba(59, 130, 246, 0.3)' ctx.lineWidth = 1 const startPx = pageStartPx @@ -159,6 +184,11 @@ function drawTicks( ctx.lineTo(size, endPx) } ctx.stroke() + + // Renkleri geri al (sonraki sayfa için) + ctx.fillStyle = '#94a3b8' + ctx.strokeStyle = '#94a3b8' + ctx.lineWidth = 0.5 } function redraw() { @@ -166,7 +196,7 @@ function redraw() { drawRuler(vCanvas.value, 'vertical') } -watch(() => [props.scale, props.panX, props.panY, props.pageWidth, props.pageHeight], redraw) +watch(() => [props.scale, props.panX, props.panY, props.pageWidth, props.pageHeight, props.containerWidth, props.pageCount], redraw) let resizeObserver: ResizeObserver | null = null @@ -205,7 +235,7 @@ onBeforeUnmount(() => { position: absolute; top: 0; left: 20px; - right: 0; + width: calc(100% - 20px); z-index: 50; pointer-events: none; } @@ -214,7 +244,7 @@ onBeforeUnmount(() => { position: absolute; top: 20px; left: 0; - bottom: 0; + height: calc(100% - 20px); z-index: 50; pointer-events: none; } diff --git a/layout-engine/Cargo.toml b/layout-engine/Cargo.toml index 788d161..98562ba 100644 --- a/layout-engine/Cargo.toml +++ b/layout-engine/Cargo.toml @@ -12,7 +12,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] dreport-core = { version = "0.2.0", path = "../core", registry = "gitea" } -dexpr = { version = "0.1.0", registry = "gitea" } +dexpr = { version = "0.3.0", registry = "gitea" } rust_decimal = "1.41" taffy = "0.9" cosmic-text = { version = "0.18", default-features = false, features = ["std", "swash"] } diff --git a/layout-engine/src/chart_layout.rs b/layout-engine/src/chart_layout.rs index 6955aa1..622d3f8 100644 --- a/layout-engine/src/chart_layout.rs +++ b/layout-engine/src/chart_layout.rs @@ -64,7 +64,9 @@ pub struct YTick { pub struct XLabelLayout { pub labels: Vec, - pub needs_rotate: bool, + /// Rotation angle in degrees (0 = horizontal, 90 = fully vertical). + /// Dynamically computed based on available space vs label length. + pub rotate_angle: f64, } pub struct XLabel { @@ -545,14 +547,14 @@ pub fn compute_chart_layout( } else { available_w }; - let max_chars_fit = (cat_width / 1.25).max(1.0) as usize; - let will_rotate = max_label_len > max_chars_fit; - if will_rotate { - let char_w_mm = 1.1; + let rotate_angle = compute_label_rotation(max_label_len, cat_width); + if rotate_angle > 0.0 { + let char_w_mm = 2.5 * 0.6; let max_text_w = max_label_len as f64 * char_w_mm; - let label_v = max_text_w * 0.707; + let angle_rad = rotate_angle.to_radians(); + let label_v = max_text_w * angle_rad.sin(); margin_bottom += label_v.clamp(6.0, 25.0); - let label_h = max_text_w * 0.707; + let label_h = max_text_w * angle_rad.cos(); let extra_left = (label_h - cat_width / 2.0).max(0.0); margin_left += extra_left.min(10.0); } else { @@ -622,6 +624,29 @@ pub fn compute_y_axis( } } +/// Compute dynamic label rotation angle (degrees) based on available space. +/// Uses Chart.js-style algorithm: rotate only when labels overflow their slot, +/// and use the minimum angle that prevents overlap. +fn compute_label_rotation(max_label_len: usize, slot_width: f64) -> f64 { + let label_font_size = 2.5_f64; + let char_w_mm = label_font_size * 0.6; + let max_label_w = max_label_len as f64 * char_w_mm; + let padding = label_font_size * 0.5; + + // Labels fit horizontally — no rotation needed + if (max_label_w + padding) <= slot_width { + return 0.0; + } + + // Chart.js Constraint A: sin(angle) = (label_height + padding) / slot_width + // This finds the minimum angle where the rotated label's projected height + // fits within the tick slot width, preventing horizontal overlap. + let label_h = label_font_size; + let sin_val = ((label_h + padding) / slot_width).clamp(0.0, 1.0); + let angle_deg = sin_val.asin().to_degrees(); + angle_deg.clamp(0.0, 50.0) +} + /// Compute X label positions for bar chart (slot-based spacing). pub fn compute_x_labels_bar( categories: &[String], @@ -633,12 +658,12 @@ pub fn compute_x_labels_bar( if n_cats == 0 { return XLabelLayout { labels: vec![], - needs_rotate: false, + rotate_angle: 0.0, }; } let cat_width = pw / n_cats as f64; - let max_chars = (cat_width / 1.25).max(1.0) as usize; - let needs_rotate = categories.iter().any(|c| c.len() > max_chars); + let max_label_len = categories.iter().map(|c| c.len()).max().unwrap_or(0); + let rotate_angle = compute_label_rotation(max_label_len, cat_width); let labels = categories .iter() .enumerate() @@ -650,7 +675,7 @@ pub fn compute_x_labels_bar( .collect(); XLabelLayout { labels, - needs_rotate, + rotate_angle, } } @@ -665,7 +690,7 @@ pub fn compute_x_labels_line( if n_cats == 0 { return XLabelLayout { labels: vec![], - needs_rotate: false, + rotate_angle: 0.0, }; } let spacing = if n_cats == 1 { @@ -673,8 +698,8 @@ pub fn compute_x_labels_line( } else { pw / (n_cats - 1) as f64 }; - let max_chars = (spacing / 1.25).max(1.0) as usize; - let needs_rotate = categories.iter().any(|c| c.len() > max_chars); + let max_label_len = categories.iter().map(|c| c.len()).max().unwrap_or(0); + let rotate_angle = compute_label_rotation(max_label_len, spacing); let labels = categories .iter() .enumerate() @@ -693,7 +718,7 @@ pub fn compute_x_labels_line( .collect(); XLabelLayout { labels, - needs_rotate, + rotate_angle, } } diff --git a/layout-engine/src/chart_render.rs b/layout-engine/src/chart_render.rs index a024c07..c3fb09c 100644 --- a/layout-engine/src/chart_render.rs +++ b/layout-engine/src/chart_render.rs @@ -337,12 +337,13 @@ fn render_y_axis_svg(svg: &mut String, y_axis: &chart_layout::YAxisLayout) { } fn render_x_labels_svg(svg: &mut String, x_labels: &chart_layout::XLabelLayout) { + let angle = x_labels.rotate_angle; for label in &x_labels.labels { - if x_labels.needs_rotate { + if angle > 0.0 { write!( svg, - r##"{}"##, - label.x, label.y, label.x, label.y, escape_xml(&label.text) + r##"{}"##, + label.x, label.y, angle, label.x, label.y, escape_xml(&label.text) ) .unwrap(); } else { diff --git a/layout-engine/src/expr_eval.rs b/layout-engine/src/expr_eval.rs index 38e2f04..a36e42e 100644 --- a/layout-engine/src/expr_eval.rs +++ b/layout-engine/src/expr_eval.rs @@ -65,6 +65,10 @@ fn dexpr_value_to_string(val: &DexprValue) -> String { .collect(); format!("{{{}}}", items.join(", ")) } + DexprValue::List(list) => { + let items: Vec = list.iter().map(|v| dexpr_value_to_string(v)).collect(); + format!("[{}]", items.join(", ")) + } } } @@ -358,4 +362,31 @@ mod tests { }; assert_eq!(format_currency("1500.25", &config), "$1,500.25"); } + + #[test] + fn test_array_field_sum() { + let data = json!({ + "kalemler": [ + {"adi": "A", "tutar": 100}, + {"adi": "B", "tutar": 200}, + {"adi": "C", "tutar": 50} + ] + }); + assert_eq!(evaluate_expression("kalemler.tutar.sum()", &data), "350"); + } + + #[test] + fn test_array_field_sum_in_arithmetic() { + let data = json!({ + "kalemler": [ + {"tutar": 1000}, + {"tutar": 2000} + ], + "toplamlar": {"kdvOrani": 20} + }); + assert_eq!( + evaluate_expression("kalemler.tutar.sum() * toplamlar.kdvOrani / 100", &data), + "600" + ); + } } diff --git a/layout-engine/src/pdf_render.rs b/layout-engine/src/pdf_render.rs index 19f5f7f..3aac829 100644 --- a/layout-engine/src/pdf_render.rs +++ b/layout-engine/src/pdf_render.rs @@ -1406,11 +1406,13 @@ fn render_chart_x_labels( fonts: &FontCollection, measurer: &mut TextMeasurer, ) { + let angle = x_labels.rotate_angle; for label in &x_labels.labels { - if x_labels.needs_rotate { + if angle > 0.0 { surface.push_transform(&Transform::from_translate(pt(label.x), pt(label.y))); - let c = std::f32::consts::FRAC_PI_4.cos(); - let s = std::f32::consts::FRAC_PI_4.sin(); + let angle_rad = (angle as f32).to_radians(); + let c = angle_rad.cos(); + let s = angle_rad.sin(); surface.push_transform(&Transform::from_row(c, -s, s, c, 0.0, 0.0)); chart_text_end( surface,