diff --git a/ELEMENTS.md b/ELEMENTS.md deleted file mode 100644 index 256b3cd..0000000 --- a/ELEMENTS.md +++ /dev/null @@ -1,271 +0,0 @@ -# Eleman Tipleri — dreport - -Bu belge, dreport toolbar'inda bulunan ve planlanmis tum eleman tiplerini aciklar. - ---- - -## Mevcut Elemanlar - -### `container` — Duzen Kutusu - -CSS Flexbox mantiginda calisan layout container'i. Cocuk elemanlari `direction` (row/column) dogrultusunda dizer. Ic ice gecebilir. Tum diger elemanlar bir container icinde yer alir. - -- **Binding:** Yok -- **Ozellikler:** `direction`, `gap`, `padding`, `align`, `justify`, `style` - ---- - -### `static_text` — Sabit Metin - -Veri baglantisi olmayan, kullanicinin dogrudan yazdigi metin. Fatura basliklari, etiketler, aciklama satirlari icin kullanilir. - -- **Binding:** Yok -- **Ozellikler:** `content`, `style` (fontSize, fontWeight, color, align) - ---- - -### `text` — Dinamik Metin - -JSON schema'dan veri ceken metin elemani. Kullanici schema agacindan bir alani surukleyip bu elemana baglar. - -- **Binding:** Scalar (`"binding": { "type": "scalar", "path": "firma.unvan" }`) -- **Ozellikler:** `binding`, `style`, `format` (currency, date, percentage) - ---- - -### `repeating_table` — Tekrarlayan Tablo - -Array verisinden tekrarlayan satirlar ureten tablo bileseni. Fatura kalemleri, stok listeleri gibi tekrarlayan veri icin kullanilir. - -- **Binding:** Array (`"dataSource": "kalemler"`) -- **Ozellikler:** `columns` (alan, genislik, hizalama), `headerStyle`, `rowStyle`, `zebraStyle` - ---- - -### `line` — Cizgi - -Yatay veya dikey ayirici cizgi. Bolum ayirma, dekoratif amaclarla kullanilir. - -- **Binding:** Yok -- **Ozellikler:** `style` (strokeColor, strokeWidth) - ---- - -### `image` — Gorsel - -Statik (base64/URL) veya dinamik (schema'dan) gorsel. Logo, imza, urun gorseli gibi kullanim alanlari. - -- **Binding:** Opsiyonel scalar (dinamik gorsel icin) -- **Ozellikler:** `src` (statik), `binding`, `style` (objectFit) - ---- - -### `page_number` — Sayfa Numarasi - -Cok sayfali belgelerde otomatik sayfa numarasi. Format sablonu destekler (or: "Sayfa {current} / {total}"). - -- **Binding:** Otomatik -- **Ozellikler:** `format`, `style` - ---- - -### `barcode` — Barkod / QR Kod - -1D ve 2D barkod ureteci. e-Fatura, e-Arsiv, urun etiketleri icin kullanilir. - -- **Binding:** Scalar (barkod verisi icin) -- **Desteklenen formatlar:** QR, EAN-13, EAN-8, CODE128, CODE39 -- **Ozellikler:** `barcodeType`, `binding`, `style` - ---- - -## Planlanmis Elemanlar - -### `rich_text` — Zengin Metin [Yapildi] - -Tek bir metin blogu icinde karisik formatlama destekleyen eleman. Kalin, italik, farkli font boyutu, renk gibi stilleri ayni paragraf icinde kullanmayi saglar. - -- **Kullanim alanlari:** Fatura aciklama alanlari, sozlesme maddeleri, rapor notlari, uzun formlu metin icerikleri -- **Binding:** Opsiyonel scalar (dinamik icerik icin) -- **Yaklasim:** Inline span'lar ile zengin metin. cosmic-text attributed text destekledigi icin layout engine tarafinda uyumlu. - -```jsonc -{ - "type": "rich_text", - "content": [ - { "text": "Odeme vadesi: ", "style": {} }, - { "text": "30 gun", "style": { "fontWeight": "bold", "color": "#e00" } } - ] -} -``` - -**Referans:** Telerik (HtmlTextBox), DevExpress (Rich Text), Stimulsoft, FastReport, CraftMyPDF — hepsinde mevcut. Belge tasarim araclarinda standart bir beklenti. - ---- - -### `shape` — Sekil (Dikdortgen / Elips) [Yapildi] - -Cocuk eleman barindirmayan sade gorsel element. Vurgu kutulari, dekoratif cerceveler, arka plan alanlari icin kullanilir. Container'dan farki: layout'a katilmaz, sadece gorsel amaclidir. - -- **Kullanim alanlari:** Toplam kutusunun arka plani, raporlarda highlight alanlari, dekoratif cerceveler -- **Binding:** Yok -- **Sekil tipleri:** `rectangle`, `ellipse`, `rounded_rectangle` - -```jsonc -{ - "type": "shape", - "shapeType": "rectangle", - "style": { - "backgroundColor": "#f0f0f0", - "borderColor": "#333", - "borderWidth": 0.5, - "borderRadius": 2 - } -} -``` - -**Referans:** JasperReports, Telerik, DevExpress, Stimulsoft, FastReport, CraftMyPDF — neredeyse tum araclarda var. - ---- - -### `checkbox` — Onay Kutusu [Yapildi] - -Boolean deger gosteren isaret kutusu. Isaretsiz kare veya isaretli (checkmark) kare olarak render edilir. Veri baglantisi ile dinamik calisan veya statik olarak kullanilabilen basit bir element. - -- **Kullanim alanlari:** Irsaliyelerde "teslim edildi / edilmedi", faturalarda odeme durumu, raporlarda checklist, form benzeri belgeler -- **Binding:** Scalar (boolean alan) - -```jsonc -{ - "type": "checkbox", - "binding": { "type": "scalar", "path": "fatura.odpiendi" }, - "style": { "size": 4, "checkColor": "#000", "borderColor": "#333" } -} -``` - -**Referans:** DevExpress, Telerik, Stimulsoft, FastReport, CraftMyPDF. - ---- - -### `calculated_text` — Hesaplanmis Alan [Yapildi] - -Basit ifadeler (expression) ile hesaplanmis deger gosteren metin elemani. Aritmetik islemler, string birlestirme ve kosullu metin destekler. - -- **Kullanim alanlari:** Ara toplam hesaplari (`araToplam * 0.20`), string birlestirme (`"Fatura No: " + fatura.no`), kosullu metin, rapor ozetleri -- **Binding:** Expression-based (birden fazla alana referans verebilir) -- **Format:** currency, date, percentage, number destegi - -```jsonc -{ - "type": "calculated_text", - "expression": "toplamlar.araToplam * 0.20", - "format": "currency", - "style": { "fontSize": 10 } -} -``` - -**Referans:** Crystal Reports (Formula Field), JasperReports (Variable), Stimulsoft (Expression). - ---- - -### `current_date` — Tarih / Zaman [Yapildi] - -Belgenin basilma/render anindaki tarihi otomatik gosteren element. `page_number` gibi otomatik deger uretir, veri baglantisi gerektirmez. - -- **Kullanim alanlari:** Fatura basim tarihi, rapor olusturma zamani, belge altbilgisi -- **Binding:** Otomatik -- **Format:** Konfigurasyon ile (or: `DD.MM.YYYY`, `DD MMMM YYYY`, `DD.MM.YYYY HH:mm`) - -```jsonc -{ - "type": "current_date", - "format": "DD.MM.YYYY", - "style": { "fontSize": 8, "color": "#666" } -} -``` - -**Referans:** Crystal Reports (Print Date), JasperReports (Current Date), BIRT (AutoText). - ---- - -### `page_break` — Sayfa Sonu [Yapildi] - -Kullanicinin belirli bir noktada yeni sayfaya gecmesini saglayan kontrol elemani. Otomatik sayfa sonu (page_break.rs) zaten mevcut, bu element manuel kontrol saglar. - -- **Kullanim alanlari:** Rapor ozet sayfasi + detay sayfasi ayrimi, faturada ek bilgi sayfasi, belirli bolumlerin ayri sayfada baslamasi -- **Binding:** Yok -- **Gorsel:** Editorde kesikli cizgi olarak gosterilir, PDF'te sayfa gecisi uretir. - -```jsonc -{ - "type": "page_break" -} -``` - -**Referans:** DevExpress (Page Break kontrol), Stimulsoft. - ---- - -### `chart` — Grafik [Henuz implemente edilmedi] - -Veri gorselIestirme icin basit grafik elemani. Rapor ciktilari icin degerli, fatura/irsaliye icin genellikle gereksiz. - -- **Kullanim alanlari:** Satis raporlari, performans ozetleri, karsilastirmali veriler -- **Binding:** Array veya multiple scalar -- **Grafik tipleri:** `bar`, `pie`, `line` (baslangic seti) -- **Yaklasim:** Backend'de SVG olarak render edilip PDF'e image olarak gomulur. - -```jsonc -{ - "type": "chart", - "chartType": "bar", - "dataSource": "aylik_satislar", - "labelField": "ay", - "valueField": "tutar", - "style": { "width": 120, "height": 80 } -} -``` - -**Referans:** JasperReports, Crystal Reports, Telerik, DevExpress, Stimulsoft, CraftMyPDF — enterprise araclarin tamami destekler. - ---- - -## Toolbar Organizasyonu - -``` -Toolbar -├── Duzen -│ ├── Container (mevcut) -│ └── Page Break (mevcut) -├── Metin -│ ├── Statik Metin (mevcut) -│ ├── Rich Text (mevcut) -│ └── Hesaplanmis Alan (mevcut) -├── Veri -│ ├── Tekrarlayan Tablo (mevcut) -│ └── Checkbox (mevcut) -├── Gorsel -│ ├── Gorsel (mevcut) -│ ├── Cizgi (mevcut) -│ ├── Sekil (mevcut) -│ └── Barkod / QR (mevcut) -├── Otomatik -│ ├── Sayfa No (mevcut) -│ └── Tarih (mevcut) -└── Rapor - └── Grafik (planlanmis) -``` - ---- - -## Oncelik Sirasi - -| Oncelik | Element | Gerekce | Durum | -|---------|---------|---------|-------| -| 1 | `rich_text` | Karisik formatlama en cok talep edilen ozellik, cosmic-text uyumlu | Yapildi | -| 2 | `shape` | Basit implementasyon, gorsel zenginlik katiyor | Yapildi | -| 3 | `checkbox` | Boolean gosterim, form/irsaliye icin onemli | Yapildi | -| 4 | `calculated_text` | Hesaplama ihtiyaci fatura/rapor icin kritik | Yapildi | -| 5 | `current_date` | Kucuk ama kullanisli, hizli implemente edilir | Yapildi | -| 6 | `page_break` | Manuel sayfa kontrolu, rapor senaryolari icin | Yapildi | -| 7 | `chart` | En karmasik, rapor fazinda ele alinabilir | | diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md index 7f13e01..492878d 100644 --- a/IMPROVEMENTS.md +++ b/IMPROVEMENTS.md @@ -7,7 +7,7 @@ ## 1. Kritik Buglar -### 1.1 Undo/Redo `Object.assign` Hatasi `[IMPLEMENTE EDILMEDI]` +### 1.1 Undo/Redo `Object.assign` Hatasi `[IMPLEMENTE EDILDI]` **Dosya:** `frontend/src/composables/useUndoRedo.ts` (satir 52) @@ -35,7 +35,7 @@ Undo/redo watcher'da 300ms debounce var. Kullanici hizli bir edit yapip 300ms ic --- -### 1.2 PDF'te Text Wrapping Yok `[IMPLEMENTE EDILMEDI]` +### 1.2 PDF'te Text Wrapping Yok `[IMPLEMENTE EDILDI]` **Dosya:** `layout-engine/src/pdf_render.rs` (satir ~487) @@ -52,7 +52,7 @@ Bu, projenin temel vaadi olan "editorde gordugum = PDF'te aldigim" WYSIWYG garan --- -### 1.3 Image objectFit Hardcoded `[IMPLEMENTE EDILMEDI]` +### 1.3 Image objectFit Hardcoded `[IMPLEMENTE EDILDI]` **Dosya:** `frontend/src/components/editor/LayoutRenderer.vue` (satir ~229) @@ -74,7 +74,7 @@ objectFit: el.style.objectFit || 'fill', --- -### 1.4 PDF'te Italic Font Secilmiyor `[IMPLEMENTE EDILMEDI]` +### 1.4 PDF'te Italic Font Secilmiyor `[IMPLEMENTE EDILDI]` **Dosya:** `layout-engine/src/pdf_render.rs` (satir ~104) @@ -286,7 +286,7 @@ Ayri bir message type namespace kullanmak — `msg.type` alani ile ayristirma za ## 3. Eksik Ozellikler (CLAUDE.md'de Tanimli) -### 3.1 Coklu Secim (Multi-Selection) `[IMPLEMENTE EDILMEDI]` +### 3.1 Coklu Secim (Multi-Selection) `[IMPLEMENTE EDILDI]` **Referans:** CLAUDE.md — "Shift+tiklama ile coklu secim" @@ -302,7 +302,7 @@ Ayri bir message type namespace kullanmak — `msg.type` alani ile ayristirma za --- -### 3.2 Z-Order Kontrolleri `[IMPLEMENTE EDILMEDI]` +### 3.2 Z-Order Kontrolleri `[IMPLEMENTE EDILDI]` **Referans:** CLAUDE.md — "One Getir / Arkaya Gonder" @@ -316,7 +316,7 @@ Ayri bir message type namespace kullanmak — `msg.type` alani ile ayristirma za --- -### 3.3 Dinamik Image Binding UI `[IMPLEMENTE EDILMEDI]` +### 3.3 Dinamik Image Binding UI `[IMPLEMENTE EDILDI]` **Referans:** CLAUDE.md — "image: Statik veya dinamik gorsel, Opsiyonel scalar binding" @@ -330,7 +330,7 @@ Ayri bir message type namespace kullanmak — `msg.type` alani ile ayristirma za --- -### 3.4 RulerBar (Cetvel) `[IMPLEMENTE EDILMEDI]` +### 3.4 RulerBar (Cetvel) `[IMPLEMENTE EDILDI]` **Referans:** CLAUDE.md proje yapisi — `components/editor/RulerBar.vue` @@ -346,7 +346,7 @@ Component dosyasi olusturulmamis, hicbir yerde import edilmiyor. --- -### 3.5 Format Fonksiyonlari (Tablo Sutunlari) `[IMPLEMENTE EDILMEDI]` +### 3.5 Format Fonksiyonlari (Tablo Sutunlari) `[IMPLEMENTE EDILDI]` **Referans:** CLAUDE.md roadmap — "Format fonksiyonlari (currency, date)" @@ -421,7 +421,7 @@ Buyuk tablolarda layout hesaplama suresi ve bellek kullanimi artar. Editorde her --- -### 4.4 Font Loader Iyilestirmesi (Backend) `[IMPLEMENTE EDILMEDI]` +### 4.4 Font Loader Iyilestirmesi (Backend) `[font loader implementasyonu yapıldı. check edilmesi gerek bu sorunun tekrar]` **Dosya:** `backend/src/main.rs` (satirlar 44-53) @@ -444,7 +444,7 @@ TTF/OTF `name` tablosunu okuyarak font ailesini (family name) metadata'dan almak `1.005` gibi degerler icin floating-point representation kaybi nedeniyle kusurat 0 veya 1 olarak yanlis yuvarlanabilir. **Cozum:** -`Decimal` arithmetic kullanmak veya en azindan `format!("{:.2}", value)` ile string uzerinden islem yapmak. +`Decimal` arithmetic kullanmak. --- diff --git a/backend/src/font_registry.rs b/backend/src/font_registry.rs new file mode 100644 index 0000000..58174df --- /dev/null +++ b/backend/src/font_registry.rs @@ -0,0 +1,164 @@ +use std::collections::HashMap; +use dreport_layout::FontData; +use dreport_layout::font_meta::{self, FontFamilyInfo, FontVariantKey}; +use dreport_layout::font_provider::FontProvider; + +/// Font registry — manages all available fonts from embedded defaults + external directory. +pub struct FontRegistry { + /// family_lower -> variant_key -> FontData + families: HashMap>, + /// Original-case family names + family_names: HashMap, +} + +impl FontRegistry { + pub fn new() -> Self { + let mut registry = Self { + families: HashMap::new(), + family_names: HashMap::new(), + }; + + // Load embedded default fonts + registry.load_embedded_defaults(); + + // Load fonts from DREPORT_FONTS_DIR if set + if let Ok(dir) = std::env::var("DREPORT_FONTS_DIR") { + registry.load_from_directory(&dir); + } + + registry + } + + fn load_embedded_defaults(&mut self) { + let embedded: &[(&str, &[u8])] = &[ + ("NotoSans-Regular", include_bytes!("../fonts/NotoSans-Regular.ttf")), + ("NotoSans-Bold", include_bytes!("../fonts/NotoSans-Bold.ttf")), + ("NotoSans-Italic", include_bytes!("../fonts/NotoSans-Italic.ttf")), + ("NotoSans-BoldItalic", include_bytes!("../fonts/NotoSans-BoldItalic.ttf")), + ("NotoSansMono-Regular", include_bytes!("../fonts/NotoSansMono-Regular.ttf")), + ]; + + for (_name, data) in embedded { + self.register_font(data.to_vec()); + } + } + + fn load_from_directory(&mut self, dir: &str) { + let path = std::path::Path::new(dir); + if !path.is_dir() { + eprintln!("DREPORT_FONTS_DIR dizini bulunamadı: {}", dir); + return; + } + + let entries = match std::fs::read_dir(path) { + Ok(e) => e, + Err(e) => { + eprintln!("DREPORT_FONTS_DIR okunamadı: {}", e); + return; + } + }; + + for entry in entries.flatten() { + let p = entry.path(); + if p.extension().is_some_and(|e| e == "ttf" || e == "otf") { + if let Ok(data) = std::fs::read(&p) { + if self.register_font(data) { + println!(" Font yüklendi: {}", p.display()); + } else { + eprintln!(" Font parse edilemedi: {}", p.display()); + } + } + } + } + } + + /// Register a font from raw bytes. Returns true if successful. + fn register_font(&mut self, data: Vec) -> bool { + let Some(meta) = font_meta::parse_font_meta(&data) else { + return false; + }; + + let family_lower = meta.family.to_lowercase(); + let variant_key = meta.variant_key(); + + self.family_names + .entry(family_lower.clone()) + .or_insert_with(|| meta.family.clone()); + + let font_data = FontData::new(meta.family, meta.weight, meta.italic, data); + + self.families + .entry(family_lower) + .or_default() + .insert(variant_key, font_data); + + true + } + + /// Get a specific font's raw bytes + pub fn get_font_bytes(&self, family: &str, weight: u16, italic: bool) -> Option<&[u8]> { + let family_lower = family.to_lowercase(); + let key = FontVariantKey { weight, italic }; + self.families + .get(&family_lower) + .and_then(|variants| variants.get(&key)) + .map(|fd| fd.data.as_slice()) + } + + /// Get all FontData for given family names (for passing to layout engine) + pub fn fonts_for_families(&self, families: &[String]) -> Vec { + let mut result = Vec::new(); + let mut loaded = std::collections::HashSet::new(); + + // Always include default family + let default_lower = "noto sans".to_string(); + let mut to_load: Vec = vec![default_lower.clone()]; + for f in families { + let fl = f.to_lowercase(); + if !to_load.contains(&fl) { + to_load.push(fl); + } + } + + for family_lower in &to_load { + if loaded.contains(family_lower) { + continue; + } + if let Some(variants) = self.families.get(family_lower) { + for fd in variants.values() { + result.push(fd.clone()); + } + loaded.insert(family_lower.clone()); + } + } + + result + } +} + +impl FontProvider for FontRegistry { + fn list_families(&self) -> Vec { + self.families + .iter() + .map(|(family_lower, variants)| { + let family = self.family_names + .get(family_lower) + .cloned() + .unwrap_or_else(|| family_lower.clone()); + FontFamilyInfo { + family, + variants: variants.keys().cloned().collect(), + } + }) + .collect() + } + + fn load_font(&self, family: &str, weight: u16, italic: bool) -> Option { + let family_lower = family.to_lowercase(); + let key = FontVariantKey { weight, italic }; + self.families + .get(&family_lower) + .and_then(|variants| variants.get(&key)) + .cloned() + } +} diff --git a/backend/src/main.rs b/backend/src/main.rs index 84ab012..31f3ef2 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,17 +1,21 @@ use axum::{Router, serve}; -use dreport_layout::FontData; use std::sync::Arc; use tokio::net::TcpListener; use tower_http::cors::{Any, CorsLayer}; +mod font_registry; mod models; mod routes; +use font_registry::FontRegistry; + #[tokio::main] async fn main() -> anyhow::Result<()> { - println!("Fontlar yukleniyor..."); - let fonts = Arc::new(load_fonts()); - println!("Fontlar yuklendi ({} font dosyasi)", fonts.len()); + println!("Font registry başlatılıyor..."); + let registry = Arc::new(FontRegistry::new()); + + let family_count = dreport_layout::font_provider::FontProvider::list_families(registry.as_ref()).len(); + println!("Font registry hazır ({} font ailesi)", family_count); let cors = CorsLayer::new() .allow_origin(Any) @@ -21,7 +25,7 @@ async fn main() -> anyhow::Result<()> { let app = Router::new() .merge(routes::router()) .layer(cors) - .with_state(fonts); + .with_state(registry); let listener = TcpListener::bind("0.0.0.0:3001").await?; println!("dreport backend listening on http://localhost:3001"); @@ -29,31 +33,3 @@ async fn main() -> anyhow::Result<()> { Ok(()) } - -/// Proje fontlarını yükler (backend/fonts/ dizininden). -fn load_fonts() -> Vec { - let font_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("fonts"); - let mut fonts = Vec::new(); - - let entries = std::fs::read_dir(&font_dir).expect("backend/fonts dizini bulunamadi"); - for entry in entries { - let entry = entry.unwrap(); - let path = entry.path(); - if path.extension().is_some_and(|e| e == "ttf" || e == "otf") { - let data = std::fs::read(&path).unwrap(); - let family = if path - .file_name() - .unwrap() - .to_str() - .unwrap() - .contains("Mono") - { - "Noto Sans Mono".to_string() - } else { - "Noto Sans".to_string() - }; - fonts.push(FontData { family, data }); - } - } - fonts -} diff --git a/backend/src/routes/fonts.rs b/backend/src/routes/fonts.rs new file mode 100644 index 0000000..ed52203 --- /dev/null +++ b/backend/src/routes/fonts.rs @@ -0,0 +1,77 @@ +use axum::{ + Router, + extract::{Path, State}, + http::{StatusCode, header}, + response::IntoResponse, + routing::get, + Json, +}; +use dreport_layout::font_provider::FontProvider; +use serde::Serialize; +use std::sync::Arc; + +use crate::font_registry::FontRegistry; + +#[derive(Serialize)] +struct FontFamilyResponse { + family: String, + variants: Vec, +} + +#[derive(Serialize)] +struct FontVariantResponse { + weight: u16, + italic: bool, +} + +/// GET /api/fonts — list all available font families +async fn list_fonts( + State(registry): State>, +) -> Json> { + let families = registry.list_families(); + let response: Vec = families + .into_iter() + .map(|f| FontFamilyResponse { + family: f.family, + variants: f.variants + .into_iter() + .map(|v| FontVariantResponse { + weight: v.weight, + italic: v.italic, + }) + .collect(), + }) + .collect(); + Json(response) +} + +/// GET /api/fonts/:family/:weight/:italic — serve font binary +async fn get_font( + State(registry): State>, + Path((family, weight, italic)): Path<(String, u16, String)>, +) -> impl IntoResponse { + let is_italic = italic == "true" || italic == "1"; + + match registry.get_font_bytes(&family, weight, is_italic) { + Some(data) => ( + StatusCode::OK, + [(header::CONTENT_TYPE, "font/ttf")], + data.to_vec(), + ) + .into_response(), + None => ( + StatusCode::NOT_FOUND, + format!( + "Font bulunamadı: {} weight={} italic={}", + family, weight, is_italic + ), + ) + .into_response(), + } +} + +pub fn router() -> Router> { + Router::new() + .route("/api/fonts", get(list_fonts)) + .route("/api/fonts/{family}/{weight}/{italic}", get(get_font)) +} diff --git a/backend/src/routes/health.rs b/backend/src/routes/health.rs index bac831a..0535cd8 100644 --- a/backend/src/routes/health.rs +++ b/backend/src/routes/health.rs @@ -1,8 +1,9 @@ use axum::{Router, routing::get, Json}; -use dreport_layout::FontData; use serde::Serialize; use std::sync::Arc; +use crate::font_registry::FontRegistry; + #[derive(Serialize)] struct HealthResponse { status: &'static str, @@ -16,6 +17,6 @@ async fn health() -> Json { }) } -pub fn router() -> Router>> { +pub fn router() -> Router> { Router::new().route("/api/health", get(health)) } diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs index b4f3eb8..a1c9ba1 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -1,12 +1,15 @@ +mod fonts; mod health; mod render; use axum::Router; -use dreport_layout::FontData; use std::sync::Arc; -pub fn router() -> Router>> { +use crate::font_registry::FontRegistry; + +pub fn router() -> Router> { Router::new() .merge(health::router()) .merge(render::router()) + .merge(fonts::router()) } diff --git a/backend/src/routes/render.rs b/backend/src/routes/render.rs index 0d4bede..61550eb 100644 --- a/backend/src/routes/render.rs +++ b/backend/src/routes/render.rs @@ -6,10 +6,10 @@ use axum::{ routing::post, Json, }; -use dreport_layout::FontData; use serde::Deserialize; use std::sync::Arc; +use crate::font_registry::FontRegistry; use crate::models::Template; #[derive(Deserialize)] @@ -20,28 +20,39 @@ pub struct RenderRequest { /// POST /api/render — Template + Data → PDF pub async fn render( - State(fonts): State>>, + State(registry): State>, Json(payload): Json, ) -> impl IntoResponse { - // 1. Layout hesapla - let layout = dreport_layout::compute_layout(&payload.template, &payload.data, &fonts); + // CPU-intensive layout + PDF render'ı blocking thread'de çalıştır + let result = tokio::task::spawn_blocking(move || { + // Template'in fonts alanına göre sadece gerekli fontları yükle + let fonts = registry.fonts_for_families(&payload.template.fonts); + let layout = dreport_layout::compute_layout(&payload.template, &payload.data, &fonts) + .map_err(|e| format!("Layout error: {}", e))?; + dreport_layout::pdf_render::render_pdf(&layout, &fonts) + }) + .await; - // 2. PDF render - match dreport_layout::pdf_render::render_pdf(&layout, &fonts) { - Ok(pdf_bytes) => ( + match result { + Ok(Ok(pdf_bytes)) => ( StatusCode::OK, [(header::CONTENT_TYPE, "application/pdf")], pdf_bytes, ) .into_response(), + Ok(Err(err)) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("PDF render hatası: {}", err), + ) + .into_response(), Err(err) => ( StatusCode::INTERNAL_SERVER_ERROR, - format!("PDF render hatasi: {}", err), + format!("Task hatası: {}", err), ) .into_response(), } } -pub fn router() -> Router>> { +pub fn router() -> Router> { Router::new().route("/api/render", post(render)) } diff --git a/core/src/models.rs b/core/src/models.rs index 23e6eb7..0af3f27 100644 --- a/core/src/models.rs +++ b/core/src/models.rs @@ -74,6 +74,7 @@ pub enum PositionMode { pub struct TextStyle { pub font_size: Option, pub font_weight: Option, + pub font_style: Option, pub font_family: Option, pub color: Option, pub align: Option, @@ -559,4 +560,42 @@ pub struct Template { #[serde(default)] pub footer: Option, pub root: ContainerElement, + #[serde(default)] + pub format_config: Option, +} + +/// Sayı/para birimi formatlama ayarları. +/// Belirtilmezse Türk Lirası varsayılan (. binlik, , ondalık, ₺ sembol). +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct FormatConfig { + /// Binlik ayırıcı (varsayılan ".") + #[serde(default = "FormatConfig::default_thousands_sep")] + pub thousands_separator: String, + /// Ondalık ayırıcı (varsayılan ",") + #[serde(default = "FormatConfig::default_decimal_sep")] + pub decimal_separator: String, + /// Para birimi sembolü (varsayılan "₺") + #[serde(default = "FormatConfig::default_currency_symbol")] + pub currency_symbol: String, + /// Para birimi sembolü pozisyonu: "suffix" (varsayılan) veya "prefix" + #[serde(default = "FormatConfig::default_currency_position")] + pub currency_position: String, +} + +impl FormatConfig { + fn default_thousands_sep() -> String { ".".to_string() } + fn default_decimal_sep() -> String { ",".to_string() } + fn default_currency_symbol() -> String { "₺".to_string() } + fn default_currency_position() -> String { "suffix".to_string() } +} + +impl Default for FormatConfig { + fn default() -> Self { + Self { + thousands_separator: Self::default_thousands_sep(), + decimal_separator: Self::default_decimal_sep(), + currency_symbol: Self::default_currency_symbol(), + currency_position: Self::default_currency_position(), + } + } } diff --git a/frontend/bun.lock b/frontend/bun.lock index 4c29d2c..084709a 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -9,6 +9,7 @@ "@codemirror/language": "^6.12.3", "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.41.0", + "@duhanbalci/codemirror-lang-dexpr": "0.1.0", "@lezer/highlight": "^1.2.3", "@lezer/lr": "^1.4.8", "codemirror": "^6.0.2", @@ -56,6 +57,8 @@ "@codemirror/view": ["@codemirror/view@6.41.0", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA=="], + "@duhanbalci/codemirror-lang-dexpr": ["@duhanbalci/codemirror-lang-dexpr@0.1.0", "https://gitea.duhanbalci.com/api/packages/duhanbalci/npm/%40duhanbalci%2Fcodemirror-lang-dexpr/-/0.1.0/codemirror-lang-dexpr-0.1.0.tgz", { "peerDependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-cR8SGbtW3Dq1/w7RyWG0yJeBfl3sqQGtJtPZE/d7hziIy75tvt1zEtYxODyuzlDkteX9XzUnaLrS3/TK9dZj8Q=="], + "@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="], "@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="], diff --git a/frontend/package.json b/frontend/package.json index 02e9fd0..01e94fa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,10 +17,10 @@ "@codemirror/language": "^6.12.3", "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.41.0", + "@duhanbalci/codemirror-lang-dexpr": "0.1.0", "@lezer/highlight": "^1.2.3", "@lezer/lr": "^1.4.8", "codemirror": "^6.0.2", - "codemirror-lang-dexpr": "file:../../rust-expr/editor", "pinia": "^3.0.4", "vue": "^3.5.31" }, diff --git a/frontend/src/components/editor/EditorCanvas.vue b/frontend/src/components/editor/EditorCanvas.vue index db254f5..d14f9d5 100644 --- a/frontend/src/components/editor/EditorCanvas.vue +++ b/frontend/src/components/editor/EditorCanvas.vue @@ -6,6 +6,7 @@ import { useEditorStore } from '../../stores/editor' import { useLayoutEngine } from '../../composables/useLayoutEngine' import LayoutRenderer from './LayoutRenderer.vue' import InteractionOverlay from './InteractionOverlay.vue' +import RulerBar from './RulerBar.vue' const props = withDefaults(defineProps<{ handleErrors?: boolean @@ -204,6 +205,15 @@ function onPointerUp(e: PointerEvent) { diff --git a/frontend/src/components/editor/InteractionOverlay.vue b/frontend/src/components/editor/InteractionOverlay.vue index 43ffbcc..3dca3ff 100644 --- a/frontend/src/components/editor/InteractionOverlay.vue +++ b/frontend/src/components/editor/InteractionOverlay.vue @@ -98,7 +98,11 @@ function getElementStyle(el: TemplateElement) { function onElementClick(e: PointerEvent, id: string) { e.stopPropagation() if (didDrag.value) return - editorStore.selectElement(id) + if (e.shiftKey) { + editorStore.toggleSelection(id) + } else { + editorStore.selectElement(id) + } } function onCanvasClick() { @@ -637,7 +641,7 @@ const isAnyDragActive = computed(() => :key="el.id" class="element-handle" :class="{ - 'element-handle--selected': editorStore.selectedElementId === el.id, + 'element-handle--selected': editorStore.isSelected(el.id), 'element-handle--container': isContainer(el), 'element-handle--dragging': isDragging && dragElementId === el.id, 'element-handle--drop-target': isContainer(el) && dropTargetContainerId === el.id && isAnyDragActive, @@ -646,10 +650,10 @@ const isAnyDragActive = computed(() => @pointerdown="(e: PointerEvent) => { onElementClick(e, el.id); onDragStart(e, el) }" > -
+
- -