From 7684a2a871ce5c92379757e3311cca461d63f564 Mon Sep 17 00:00:00 2001 From: Duhan BALCI Date: Fri, 3 Apr 2026 01:26:54 +0300 Subject: [PATCH] add elements --- ELEMENTS.md | 42 +- core/src/models.rs | 131 +++ .../src/components/editor/EditorCanvas.vue | 28 +- .../components/editor/InteractionOverlay.vue | 46 +- .../src/components/editor/LayoutRenderer.vue | 264 +++-- .../src/components/panels/PropertiesPanel.vue | 131 ++- .../src/components/panels/ToolboxPanel.vue | 74 +- .../properties/CalculatedTextProperties.vue | 75 ++ .../properties/CheckboxProperties.vue | 50 + .../properties/ContainerProperties.vue | 10 + .../properties/CurrentDateProperties.vue | 59 ++ .../properties/RepeatingTableProperties.vue | 11 + .../properties/RichTextProperties.vue | 182 ++++ .../components/properties/ShapeProperties.vue | 60 ++ frontend/src/composables/useLayoutEngine.ts | 14 +- frontend/src/core/layout-types.ts | 15 + frontend/src/core/types.ts | 56 +- frontend/src/stores/template.ts | 83 +- layout-engine/src/data_resolve.rs | 156 ++- layout-engine/src/expr_eval.rs | 510 +++++++++ layout-engine/src/lib.rs | 18 + layout-engine/src/page_break.rs | 995 ++++++++++++++++++ layout-engine/src/pdf_render.rs | 243 +++++ layout-engine/src/sizing.rs | 2 + layout-engine/src/table_layout.rs | 6 + layout-engine/src/text_measure.rs | 86 ++ layout-engine/src/tree.rs | 336 +++++- layout-engine/tests/layout_integration.rs | 9 + layout-engine/tests/pdf_render_test.rs | 85 ++ 29 files changed, 3600 insertions(+), 177 deletions(-) create mode 100644 frontend/src/components/properties/CalculatedTextProperties.vue create mode 100644 frontend/src/components/properties/CheckboxProperties.vue create mode 100644 frontend/src/components/properties/CurrentDateProperties.vue create mode 100644 frontend/src/components/properties/RichTextProperties.vue create mode 100644 frontend/src/components/properties/ShapeProperties.vue create mode 100644 layout-engine/src/expr_eval.rs create mode 100644 layout-engine/src/page_break.rs diff --git a/ELEMENTS.md b/ELEMENTS.md index da5846a..256b3cd 100644 --- a/ELEMENTS.md +++ b/ELEMENTS.md @@ -81,7 +81,7 @@ Cok sayfali belgelerde otomatik sayfa numarasi. Format sablonu destekler (or: "S ## Planlanmis Elemanlar -### `rich_text` — Zengin Metin [Henuz implemente edilmedi] +### `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. @@ -103,7 +103,7 @@ Tek bir metin blogu icinde karisik formatlama destekleyen eleman. Kalin, italik, --- -### `shape` — Sekil (Dikdortgen / Elips) [Henuz implemente edilmedi] +### `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. @@ -128,7 +128,7 @@ Cocuk eleman barindirmayan sade gorsel element. Vurgu kutulari, dekoratif cercev --- -### `checkbox` — Onay Kutusu [Henuz implemente edilmedi] +### `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. @@ -147,7 +147,7 @@ Boolean deger gosteren isaret kutusu. Isaretsiz kare veya isaretli (checkmark) k --- -### `calculated_text` — Hesaplanmis Alan [Henuz implemente edilmedi] +### `calculated_text` — Hesaplanmis Alan [Yapildi] Basit ifadeler (expression) ile hesaplanmis deger gosteren metin elemani. Aritmetik islemler, string birlestirme ve kosullu metin destekler. @@ -168,7 +168,7 @@ Basit ifadeler (expression) ile hesaplanmis deger gosteren metin elemani. Aritme --- -### `current_date` — Tarih / Zaman [Henuz implemente edilmedi] +### `current_date` — Tarih / Zaman [Yapildi] Belgenin basilma/render anindaki tarihi otomatik gosteren element. `page_number` gibi otomatik deger uretir, veri baglantisi gerektirmez. @@ -188,7 +188,7 @@ Belgenin basilma/render anindaki tarihi otomatik gosteren element. `page_number` --- -### `page_break` — Sayfa Sonu [Henuz implemente edilmedi] +### `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. @@ -236,22 +236,22 @@ Veri gorselIestirme icin basit grafik elemani. Rapor ciktilari icin degerli, fat Toolbar ├── Duzen │ ├── Container (mevcut) -│ └── Page Break (planlanmis) +│ └── Page Break (mevcut) ├── Metin │ ├── Statik Metin (mevcut) -│ ├── Rich Text (planlanmis) -│ └── Hesaplanmis Alan (planlanmis) +│ ├── Rich Text (mevcut) +│ └── Hesaplanmis Alan (mevcut) ├── Veri │ ├── Tekrarlayan Tablo (mevcut) -│ └── Checkbox (planlanmis) +│ └── Checkbox (mevcut) ├── Gorsel │ ├── Gorsel (mevcut) │ ├── Cizgi (mevcut) -│ ├── Sekil (planlanmis) +│ ├── Sekil (mevcut) │ └── Barkod / QR (mevcut) ├── Otomatik │ ├── Sayfa No (mevcut) -│ └── Tarih (planlanmis) +│ └── Tarih (mevcut) └── Rapor └── Grafik (planlanmis) ``` @@ -260,12 +260,12 @@ Toolbar ## Oncelik Sirasi -| Oncelik | Element | Gerekce | -|---------|---------|---------| -| 1 | `rich_text` | Karisik formatlama en cok talep edilen ozellik, cosmic-text uyumlu | -| 2 | `shape` | Basit implementasyon, gorsel zenginlik katiyor | -| 3 | `checkbox` | Boolean gosterim, form/irsaliye icin onemli | -| 4 | `calculated_text` | Hesaplama ihtiyaci fatura/rapor icin kritik | -| 5 | `current_date` | Kucuk ama kullanisli, hizli implemente edilir | -| 6 | `page_break` | Manuel sayfa kontrolu, rapor senaryolari icin | -| 7 | `chart` | En karmasik, rapor fazinda ele alinabilir | +| 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/core/src/models.rs b/core/src/models.rs index 49a7310..ee1fdb4 100644 --- a/core/src/models.rs +++ b/core/src/models.rs @@ -145,6 +145,19 @@ pub struct BarcodeStyle { pub include_text: Option, } +// --- Rich Text --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RichTextSpan { + #[serde(default)] + pub text: Option, + #[serde(default)] + pub binding: Option, + #[serde(default)] + pub style: TextStyle, +} + // --- Element tipleri --- #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -172,6 +185,18 @@ pub enum TemplateElement { PageNumber(PageNumberElement), #[serde(rename = "barcode")] Barcode(BarcodeElement), + #[serde(rename = "page_break")] + PageBreak(PageBreakElement), + #[serde(rename = "current_date")] + CurrentDate(CurrentDateElement), + #[serde(rename = "shape")] + Shape(ShapeElement), + #[serde(rename = "checkbox")] + Checkbox(CheckboxElement), + #[serde(rename = "calculated_text")] + CalculatedText(CalculatedTextElement), + #[serde(rename = "rich_text")] + RichText(RichTextElement), } impl TemplateElement { @@ -185,6 +210,12 @@ impl TemplateElement { Self::Image(e) => &e.id, Self::PageNumber(e) => &e.id, Self::Barcode(e) => &e.id, + Self::PageBreak(e) => &e.id, + Self::CurrentDate(e) => &e.id, + Self::Shape(e) => &e.id, + Self::Checkbox(e) => &e.id, + Self::CalculatedText(e) => &e.id, + Self::RichText(e) => &e.id, } } @@ -198,10 +229,24 @@ impl TemplateElement { Self::Image(e) => &e.position, Self::PageNumber(e) => &e.position, Self::Barcode(e) => &e.position, + Self::PageBreak(_) => &PositionMode::Flow, + Self::CurrentDate(e) => &e.position, + Self::Shape(e) => &e.position, + Self::Checkbox(e) => &e.position, + Self::CalculatedText(e) => &e.position, + Self::RichText(e) => &e.position, } } pub fn size(&self) -> &SizeConstraint { + static DEFAULT_SIZE: SizeConstraint = SizeConstraint { + width: SizeValue::Auto, + height: SizeValue::Auto, + min_width: None, + min_height: None, + max_width: None, + max_height: None, + }; match self { Self::Container(e) => &e.size, Self::StaticText(e) => &e.size, @@ -211,10 +256,27 @@ impl TemplateElement { Self::Image(e) => &e.size, Self::PageNumber(e) => &e.size, Self::Barcode(e) => &e.size, + Self::PageBreak(_) => &DEFAULT_SIZE, + Self::CurrentDate(e) => &e.size, + Self::Shape(e) => &e.size, + Self::Checkbox(e) => &e.size, + Self::CalculatedText(e) => &e.size, + Self::RichText(e) => &e.size, } } } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RichTextElement { + pub id: String, + pub position: PositionMode, + pub size: SizeConstraint, + #[serde(default)] + pub style: TextStyle, // varsayilan stil (span'lar override edebilir) + pub content: Vec, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ContainerElement { @@ -237,8 +299,12 @@ pub struct ContainerElement { pub style: ContainerStyle, #[serde(default)] pub children: Vec, + #[serde(default = "default_auto")] + pub break_inside: String, } +fn default_auto() -> String { "auto".to_string() } + fn default_column() -> String { "column".to_string() } fn default_stretch() -> String { "stretch".to_string() } fn default_start() -> String { "start".to_string() } @@ -315,6 +381,67 @@ pub struct RepeatingTableElement { pub data_source: ArrayBinding, pub columns: Vec, pub style: TableStyle, + #[serde(default = "default_true")] + pub repeat_header: Option, +} + +fn default_true() -> Option { Some(true) } + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PageBreakElement { + pub id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CurrentDateElement { + pub id: String, + pub position: PositionMode, + pub size: SizeConstraint, + pub style: TextStyle, + pub format: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ShapeElement { + pub id: String, + pub position: PositionMode, + pub size: SizeConstraint, + pub shape_type: String, // rectangle, ellipse, rounded_rectangle + pub style: ContainerStyle, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct CheckboxStyle { + pub size: Option, // mm — kare boyutu + pub check_color: Option, // checkmark rengi + pub border_color: Option, // kare kenar rengi + pub border_width: Option, // kenar kalınlığı +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CheckboxElement { + pub id: String, + pub position: PositionMode, + pub size: SizeConstraint, + pub checked: Option, // statik değer + pub binding: Option, // dinamik boolean binding + pub style: CheckboxStyle, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CalculatedTextElement { + pub id: String, + pub position: PositionMode, + pub size: SizeConstraint, + pub style: TextStyle, + pub expression: String, + pub format: Option, } // --- Template --- @@ -325,5 +452,9 @@ pub struct Template { pub name: String, pub page: PageSettings, pub fonts: Vec, + #[serde(default)] + pub header: Option, + #[serde(default)] + pub footer: Option, pub root: ContainerElement, } diff --git a/frontend/src/components/editor/EditorCanvas.vue b/frontend/src/components/editor/EditorCanvas.vue index a940be1..4d432c4 100644 --- a/frontend/src/components/editor/EditorCanvas.vue +++ b/frontend/src/components/editor/EditorCanvas.vue @@ -37,14 +37,24 @@ const scale = computed(() => { return (containerWidth.value / templateStore.template.page.width) * editorStore.zoom }) -// Sayfa boyutu px cinsinden + margin CSS variables -const pageStyle = computed(() => { +// Layout sayfaları +const layoutPages = computed(() => layout.value?.pages ?? []) + +// Sayfa yüksekliği px cinsinden +const pageHeightPx = computed(() => templateStore.template.page.height * scale.value) + +// Sayfalar container stili — tüm sayfaları kapsayan dış kutu +const pagesContainerStyle = computed(() => { const w = templateStore.template.page.width * scale.value - const h = templateStore.template.page.height * scale.value const m = templateStore.template.root.padding + const pageCount = Math.max(1, layoutPages.value.length) + const pageGap = 24 + const totalH = pageHeightPx.value * pageCount + pageGap * (pageCount - 1) return { width: `${w}px`, - height: `${h}px`, + height: `${totalH}px`, + position: 'relative' as const, + flexShrink: 0, '--page-margin-top': `${m.top * scale.value}px`, '--page-margin-right': `${m.right * scale.value}px`, '--page-margin-bottom': `${m.bottom * scale.value}px`, @@ -204,10 +214,10 @@ function onPointerUp(e: PointerEvent) { @pointermove="onPointerMove" @pointerup="onPointerUp" > - -
+ +
- +
@@ -244,9 +254,7 @@ function onPointerUp(e: PointerEvent) { padding: 40px; } -.editor-canvas__page { - background: white; - box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15); +.editor-canvas__pages { position: relative; flex-shrink: 0; } diff --git a/frontend/src/components/editor/InteractionOverlay.vue b/frontend/src/components/editor/InteractionOverlay.vue index d3f68bf..1bb7749 100644 --- a/frontend/src/components/editor/InteractionOverlay.vue +++ b/frontend/src/components/editor/InteractionOverlay.vue @@ -2,15 +2,19 @@ import { computed, ref } from 'vue' import { useTemplateStore } from '../../stores/template' import { useEditorStore } from '../../stores/editor' -import type { ElementLayout } from '../../core/layout-types' +import type { LayoutMapEntry } from '../../core/layout-types' import type { TemplateElement, SizeValue, ContainerElement } from '../../core/types' import { isContainer, sz } from '../../core/types' import ElementToolbar from './ElementToolbar.vue' import { useSnapGuides } from '../../composables/useSnapGuides' +const PAGE_GAP_PX = 24 + const props = defineProps<{ scale: number - layoutMap: Record + layoutMap: Record + pageCount?: number + pageHeightPx?: number }>() const templateStore = useTemplateStore() @@ -28,7 +32,16 @@ const flatElements = computed(() => { } } } + // Header ve footer container'larını ve elemanlarını dahil et + if (templateStore.template.header) { + result.push(templateStore.template.header as unknown as TemplateElement) + walk(templateStore.template.header as unknown as TemplateElement) + } walk(templateStore.template.root) + if (templateStore.template.footer) { + result.push(templateStore.template.footer as unknown as TemplateElement) + walk(templateStore.template.footer as unknown as TemplateElement) + } return result }) @@ -41,10 +54,25 @@ const allContainers = computed(() => { for (const child of el.children) walk(child) } } + if (templateStore.template.header) { + result.push(templateStore.template.header) + for (const child of templateStore.template.header.children) walk(child) + } for (const child of templateStore.template.root.children) walk(child) + if (templateStore.template.footer) { + result.push(templateStore.template.footer) + for (const child of templateStore.template.footer.children) walk(child) + } return result }) +/** Sayfa index'ine göre y offset hesapla (sayfalar arası gap dahil) */ +function pageYOffset(pageIndex: number): number { + if (pageIndex <= 0) return 0 + const pageH = props.pageHeightPx ?? (templateStore.template.page.height * props.scale) + return pageIndex * (pageH + PAGE_GAP_PX) +} + function getElementStyle(el: TemplateElement) { const l = props.layoutMap[el.id] if (!l) return { display: 'none' } @@ -53,12 +81,13 @@ function getElementStyle(el: TemplateElement) { const h = l.height_mm * s const minH = 8 const actualH = Math.max(h, minH) - const yOffset = h < minH ? (minH - h) / 2 : 0 + const yOff = h < minH ? (minH - h) / 2 : 0 + const pYOff = pageYOffset(l.pageIndex) return { position: 'absolute' as const, left: `${l.x_mm * s}px`, - top: `${l.y_mm * s - yOffset}px`, + top: `${l.y_mm * s - yOff + pYOff}px`, width: `${l.width_mm * s}px`, height: `${actualH}px`, } @@ -113,7 +142,7 @@ function findDeepestContainer(mouseX: number, mouseY: number, excludeId?: string /** Container içinde drop index hesapla */ function computeDropIndex(container: ContainerElement, mouseX: number, mouseY: number, excludeId?: string) { const s = props.scale - const flowChildren = container.children.filter(c => c.position.type !== 'absolute' && c.id !== excludeId) + const flowChildren = container.children.filter(c => c.type !== 'page_break' && c.position.type !== 'absolute' && c.id !== excludeId) const isRow = container.direction === 'row' let visualIdx = flowChildren.length @@ -133,7 +162,7 @@ function computeDropIndex(container: ContainerElement, mouseX: number, mouseY: n // Mantıksal index: excludeId aynı container'daysa offset hesapla let logicalIdx = visualIdx if (excludeId) { - const allFlow = container.children.filter(c => c.position.type !== 'absolute') + const allFlow = container.children.filter(c => c.type !== 'page_break' && c.position.type !== 'absolute') const currentIdx = allFlow.findIndex(c => c.id === excludeId) if (currentIdx >= 0) { // visualIdx, excludeId çıkarılmış listede. Gerçek listedeki pozisyona çevir. @@ -186,7 +215,7 @@ const dropIndicatorStyle = computed(() => { // Sürüklenen elemanı çıkar const dragId = dragElementId.value - const flowChildren = container.children.filter(c => c.position.type !== 'absolute' && c.id !== dragId) + const flowChildren = container.children.filter(c => c.type !== 'page_break' && c.position.type !== 'absolute' && c.id !== dragId) const cl = props.layoutMap[container.id] if (!cl) return { display: 'none' } @@ -288,6 +317,7 @@ const dragOffset = ref({ x: 0, y: 0 }) const dragGhost = ref({ x: 0, y: 0, width: 0, height: 0 }) function onDragStart(e: PointerEvent, el: TemplateElement) { + if (el.type === 'page_break') return if (el.position.type === 'absolute') { onAbsoluteDragStart(e, el) return @@ -617,7 +647,7 @@ const isAnyDragActive = computed(() =>
-