mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-01 18:39:16 +00:00
refactor
This commit is contained in:
@@ -29,14 +29,14 @@ pub fn render_svg(data: &ResolvedChartData, width_mm: f64, height_mm: f64) -> St
|
||||
// Title
|
||||
if let Some(ref title) = cl.title {
|
||||
let anchor = match title.align.as_str() {
|
||||
"left" => "start",
|
||||
"right" => "end",
|
||||
_ => "middle",
|
||||
"left" => SvgAnchor::Start,
|
||||
"right" => SvgAnchor::End,
|
||||
_ => SvgAnchor::Middle,
|
||||
};
|
||||
write!(
|
||||
svg,
|
||||
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="{}" font-weight="bold">{}</text>"##,
|
||||
title.x, title.y, title.font_size, title.color, anchor, escape_xml(&title.text)
|
||||
title.x, title.y, title.font_size, title.color, anchor.as_str(), escape_xml(&title.text)
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -56,14 +56,7 @@ pub fn render_svg(data: &ResolvedChartData, width_mm: f64, height_mm: f64) -> St
|
||||
let has_axis = !matches!(data.chart_type, dreport_core::models::ChartType::Pie);
|
||||
if has_axis && let Some(ref axis) = data.axis {
|
||||
if let Some(ref x_label) = axis.x_label {
|
||||
let x = cl.plot_x + cl.plot_w / 2.0;
|
||||
let y = height_mm - 2.0;
|
||||
write!(
|
||||
svg,
|
||||
r##"<text x="{:.2}" y="{:.2}" font-size="2.8" fill="#666" text-anchor="middle">{}</text>"##,
|
||||
x, y, escape_xml(x_label)
|
||||
)
|
||||
.unwrap();
|
||||
svg_text(&mut svg, cl.plot_x + cl.plot_w / 2.0, height_mm - 2.0, 2.8, "#666", SvgAnchor::Middle, x_label);
|
||||
}
|
||||
if let Some(ref y_label) = axis.y_label {
|
||||
let x = 3.0;
|
||||
@@ -101,24 +94,8 @@ fn render_bar(svg: &mut String, data: &ResolvedChartData, cl: &ChartLayout) {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if bl.show_labels {
|
||||
if bl.stacked {
|
||||
if bar.value > 0.0 {
|
||||
write!(
|
||||
svg,
|
||||
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="middle">{}</text>"##,
|
||||
bar.label_x, bar.label_y, bl.label_font, bl.label_color, format_value(bar.value)
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
} else {
|
||||
write!(
|
||||
svg,
|
||||
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="middle">{}</text>"##,
|
||||
bar.label_x, bar.label_y, bl.label_font, bl.label_color, format_value(bar.value)
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
if bl.show_labels && (!bl.stacked || bar.value > 0.0) {
|
||||
svg_text(svg, bar.label_x, bar.label_y, bl.label_font, &bl.label_color, SvgAnchor::Middle, &format_value(bar.value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,12 +139,7 @@ fn render_line(svg: &mut String, data: &ResolvedChartData, cl: &ChartLayout) {
|
||||
}
|
||||
|
||||
if ll.show_labels {
|
||||
write!(
|
||||
svg,
|
||||
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="middle">{}</text>"##,
|
||||
pt.x, pt.y - 1.5, ll.label_font, ll.label_color, format_value(pt.value)
|
||||
)
|
||||
.unwrap();
|
||||
svg_text(svg, pt.x, pt.y - 1.5, ll.label_font, &ll.label_color, SvgAnchor::Middle, &format_value(pt.value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,18 +211,11 @@ fn render_pie(svg: &mut String, data: &ResolvedChartData, cl: &ChartLayout) {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Percentage label inside slice
|
||||
if pl.show_labels {
|
||||
write!(
|
||||
svg,
|
||||
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="middle" dominant-baseline="central">{}%</text>"##,
|
||||
slice.label_x, slice.label_y, pl.label_font, pl.label_color,
|
||||
(slice.fraction * 100.0).round()
|
||||
)
|
||||
.unwrap();
|
||||
let pct = format!("{}%", (slice.fraction * 100.0).round());
|
||||
svg_text_central(svg, slice.label_x, slice.label_y, pl.label_font, &pl.label_color, SvgAnchor::Middle, &pct);
|
||||
}
|
||||
|
||||
// Category name label outside slice with leader line
|
||||
if pl.show_cat_labels && !slice.cat_label_text.is_empty() {
|
||||
write!(
|
||||
svg,
|
||||
@@ -259,18 +224,8 @@ fn render_pie(svg: &mut String, data: &ResolvedChartData, cl: &ChartLayout) {
|
||||
slice.leader_end_x, slice.leader_end_y
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let anchor = if slice.cat_label_anchor_end {
|
||||
"end"
|
||||
} else {
|
||||
"start"
|
||||
};
|
||||
write!(
|
||||
svg,
|
||||
r##"<text x="{:.2}" y="{:.2}" font-size="2.5" fill="#555" text-anchor="{}" dominant-baseline="central">{}</text>"##,
|
||||
slice.cat_label_x, slice.cat_label_y, anchor, escape_xml(&slice.cat_label_text)
|
||||
)
|
||||
.unwrap();
|
||||
let anchor = if slice.cat_label_anchor_end { SvgAnchor::End } else { SvgAnchor::Start };
|
||||
svg_text_central(svg, slice.cat_label_x, slice.cat_label_y, 2.5, "#555", anchor, &slice.cat_label_text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,15 +247,7 @@ fn render_legend(
|
||||
item.swatch_x, item.swatch_y, color
|
||||
)
|
||||
.unwrap();
|
||||
write!(
|
||||
svg,
|
||||
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="#666">{}</text>"##,
|
||||
item.text_x,
|
||||
item.text_y,
|
||||
legend.font_size,
|
||||
escape_xml(&item.name)
|
||||
)
|
||||
.unwrap();
|
||||
svg_text(svg, item.text_x, item.text_y, legend.font_size, "#666", SvgAnchor::Start, &item.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,12 +257,7 @@ fn render_legend(
|
||||
|
||||
fn render_y_axis_svg(svg: &mut String, y_axis: &chart_layout::YAxisLayout) {
|
||||
for tick in &y_axis.ticks {
|
||||
write!(
|
||||
svg,
|
||||
r##"<text x="{:.2}" y="{:.2}" font-size="2.3" fill="#666" text-anchor="end">{}</text>"##,
|
||||
y_axis.axis_x - 1.5, tick.y + 0.8, tick.label
|
||||
)
|
||||
.unwrap();
|
||||
svg_text(svg, y_axis.axis_x - 1.5, tick.y + 0.8, 2.3, "#666", SvgAnchor::End, &tick.label);
|
||||
|
||||
if y_axis.show_grid {
|
||||
write!(
|
||||
@@ -340,6 +282,7 @@ 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 angle > 0.0 {
|
||||
// Döndürülmüş etiket — transform gerektiğinden helper kullanamıyoruz
|
||||
write!(
|
||||
svg,
|
||||
r##"<text x="{:.2}" y="{:.2}" font-size="2.2" fill="#666" text-anchor="end" transform="rotate(-{:.1},{:.2},{:.2})">{}</text>"##,
|
||||
@@ -347,12 +290,7 @@ fn render_x_labels_svg(svg: &mut String, x_labels: &chart_layout::XLabelLayout)
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
write!(
|
||||
svg,
|
||||
r##"<text x="{:.2}" y="{:.2}" font-size="2.5" fill="#666" text-anchor="middle">{}</text>"##,
|
||||
label.x, label.y, escape_xml(&label.text)
|
||||
)
|
||||
.unwrap();
|
||||
svg_text(svg, label.x, label.y, 2.5, "#666", SvgAnchor::Middle, &label.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -364,6 +302,61 @@ fn escape_xml(s: &str) -> String {
|
||||
.replace('"', """)
|
||||
}
|
||||
|
||||
/// SVG text hizalama modu
|
||||
enum SvgAnchor {
|
||||
Start,
|
||||
Middle,
|
||||
End,
|
||||
}
|
||||
|
||||
impl SvgAnchor {
|
||||
fn as_str(&self) -> &str {
|
||||
match self {
|
||||
SvgAnchor::Start => "start",
|
||||
SvgAnchor::Middle => "middle",
|
||||
SvgAnchor::End => "end",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tekrarlayan SVG text element yazımını soyutlar.
|
||||
fn svg_text(
|
||||
svg: &mut String,
|
||||
x: f64,
|
||||
y: f64,
|
||||
font_size: f64,
|
||||
fill: &str,
|
||||
anchor: SvgAnchor,
|
||||
text: &str,
|
||||
) {
|
||||
write!(
|
||||
svg,
|
||||
r##"<text x="{x:.2}" y="{y:.2}" font-size="{font_size:.1}" fill="{fill}" text-anchor="{anchor}">{text}</text>"##,
|
||||
anchor = anchor.as_str(),
|
||||
text = escape_xml(text),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// SVG text with dominant-baseline="central" (pie labels vb.)
|
||||
fn svg_text_central(
|
||||
svg: &mut String,
|
||||
x: f64,
|
||||
y: f64,
|
||||
font_size: f64,
|
||||
fill: &str,
|
||||
anchor: SvgAnchor,
|
||||
text: &str,
|
||||
) {
|
||||
write!(
|
||||
svg,
|
||||
r##"<text x="{x:.2}" y="{y:.2}" font-size="{font_size:.1}" fill="{fill}" text-anchor="{anchor}" dominant-baseline="central">{text}</text>"##,
|
||||
anchor = anchor.as_str(),
|
||||
text = escape_xml(text),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -292,6 +292,58 @@ impl From<&dreport_core::models::CheckboxStyle> for ResolvedStyle {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&data_resolve::ResolvedChartData> for ChartRenderData {
|
||||
fn from(cd: &data_resolve::ResolvedChartData) -> Self {
|
||||
let n_colors = cd.categories.len().max(cd.series.len()).max(1);
|
||||
let colors: Vec<String> = (0..n_colors)
|
||||
.map(|i| {
|
||||
cd.style
|
||||
.colors
|
||||
.as_ref()
|
||||
.and_then(|c| c.get(i).cloned())
|
||||
.unwrap_or_else(|| {
|
||||
chart_layout::DEFAULT_COLORS[i % chart_layout::DEFAULT_COLORS.len()]
|
||||
.to_string()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
chart_type: cd.chart_type.clone(),
|
||||
categories: cd.categories.clone(),
|
||||
series: cd
|
||||
.series
|
||||
.iter()
|
||||
.map(|s| ChartSeriesData {
|
||||
name: s.name.clone(),
|
||||
values: s.values.clone(),
|
||||
})
|
||||
.collect(),
|
||||
title_text: cd.title.as_ref().map(|t| t.text.clone()),
|
||||
title_font_size: cd.title.as_ref().and_then(|t| t.font_size),
|
||||
title_color: cd.title.as_ref().and_then(|t| t.color.clone()),
|
||||
title_align: cd.title.as_ref().and_then(|t| t.align.clone()),
|
||||
colors,
|
||||
show_labels: cd.labels.as_ref().is_some_and(|l| l.show),
|
||||
label_font_size: cd.labels.as_ref().and_then(|l| l.font_size),
|
||||
label_color: cd.labels.as_ref().and_then(|l| l.color.clone()),
|
||||
show_grid: cd.axis.as_ref().and_then(|a| a.show_grid).unwrap_or(true),
|
||||
grid_color: cd.axis.as_ref().and_then(|a| a.grid_color.clone()),
|
||||
bar_gap: cd.style.bar_gap,
|
||||
stacked: matches!(cd.group_mode, Some(dreport_core::models::GroupMode::Stacked)),
|
||||
inner_radius: cd.style.inner_radius,
|
||||
show_points: cd.style.show_points,
|
||||
line_width: cd.style.line_width,
|
||||
background_color: cd.style.background_color.clone(),
|
||||
legend_show: cd.legend.as_ref().is_some_and(|l| l.show),
|
||||
legend_position: cd.legend.as_ref().and_then(|l| l.position.clone()),
|
||||
legend_font_size: cd.legend.as_ref().and_then(|l| l.font_size),
|
||||
x_label: cd.axis.as_ref().and_then(|a| a.x_label.clone()),
|
||||
y_label: cd.axis.as_ref().and_then(|a| a.y_label.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ana layout hesaplama fonksiyonu.
|
||||
/// Template + data + font verileri alır, her element için pozisyon döner.
|
||||
pub fn compute_layout(
|
||||
|
||||
@@ -21,11 +21,6 @@ fn mm(v: f64) -> f32 {
|
||||
v as f32 * MM_TO_PT
|
||||
}
|
||||
|
||||
/// f64 mm degerini f32 pt'ye cevir (chart render icin)
|
||||
fn pt(mm_val: f64) -> f32 {
|
||||
mm_val as f32 * MM_TO_PT
|
||||
}
|
||||
|
||||
/// Hex renk (#RRGGBB veya #RGB) → rgb::Color
|
||||
fn parse_color(hex: &str) -> rgb::Color {
|
||||
let hex = hex.trim_start_matches('#');
|
||||
@@ -46,6 +41,18 @@ fn parse_color(hex: &str) -> rgb::Color {
|
||||
rgb::Color::new(r, g, b)
|
||||
}
|
||||
|
||||
fn fill_from_color(color: rgb::Color) -> Fill {
|
||||
Fill {
|
||||
paint: color.into(),
|
||||
opacity: NormalizedF32::ONE,
|
||||
rule: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Path builders
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Rounded rectangle path oluştur. radius 0 ise düz dikdörtgen.
|
||||
fn build_rect_path(x: f32, y: f32, w: f32, h: f32, radius: f32) -> Option<krilla::geom::Path> {
|
||||
let mut pb = PathBuilder::new();
|
||||
@@ -92,11 +99,132 @@ fn build_ellipse_path(x: f32, y: f32, w: f32, h: f32) -> Option<krilla::geom::Pa
|
||||
pb.finish()
|
||||
}
|
||||
|
||||
fn fill_from_color(color: rgb::Color) -> Fill {
|
||||
Fill {
|
||||
paint: color.into(),
|
||||
opacity: NormalizedF32::ONE,
|
||||
rule: Default::default(),
|
||||
/// Merkez + radius'tan daire path'i oluştur (build_ellipse_path'in kısa hali)
|
||||
fn build_circle_path(cx: f32, cy: f32, r: f32) -> Option<krilla::geom::Path> {
|
||||
build_ellipse_path(cx - r, cy - r, r * 2.0, r * 2.0)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SurfaceExt — krilla surface üzerinde tekrar eden draw kalıplarını soyutlar
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
trait SurfaceExt {
|
||||
fn draw_filled(&mut self, path: &krilla::geom::Path, color: rgb::Color);
|
||||
fn draw_stroked(&mut self, path: &krilla::geom::Path, color: rgb::Color, width: f32);
|
||||
fn draw_filled_stroked(
|
||||
&mut self,
|
||||
path: &krilla::geom::Path,
|
||||
fill: Option<rgb::Color>,
|
||||
stroke_color: rgb::Color,
|
||||
stroke_width: f32,
|
||||
);
|
||||
}
|
||||
|
||||
impl SurfaceExt for krilla::surface::Surface<'_> {
|
||||
fn draw_filled(&mut self, path: &krilla::geom::Path, color: rgb::Color) {
|
||||
self.set_fill(Some(fill_from_color(color)));
|
||||
self.set_stroke(None);
|
||||
self.draw_path(path);
|
||||
self.set_fill(None);
|
||||
}
|
||||
|
||||
fn draw_stroked(&mut self, path: &krilla::geom::Path, color: rgb::Color, width: f32) {
|
||||
self.set_fill(None);
|
||||
self.set_stroke(Some(Stroke {
|
||||
paint: color.into(),
|
||||
width,
|
||||
opacity: NormalizedF32::ONE,
|
||||
..Default::default()
|
||||
}));
|
||||
self.draw_path(path);
|
||||
self.set_stroke(None);
|
||||
}
|
||||
|
||||
fn draw_filled_stroked(
|
||||
&mut self,
|
||||
path: &krilla::geom::Path,
|
||||
fill: Option<rgb::Color>,
|
||||
stroke_color: rgb::Color,
|
||||
stroke_width: f32,
|
||||
) {
|
||||
self.set_fill(fill.map(fill_from_color));
|
||||
self.set_stroke(Some(Stroke {
|
||||
paint: stroke_color.into(),
|
||||
width: stroke_width,
|
||||
opacity: NormalizedF32::ONE,
|
||||
..Default::default()
|
||||
}));
|
||||
self.draw_path(path);
|
||||
self.set_fill(None);
|
||||
self.set_stroke(None);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// draw_box — container ve shape'in ortak fill+border çizim mantığı
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Kutu şekli: dikdörtgen, yuvarlatılmış dikdörtgen veya elips.
|
||||
enum BoxShape {
|
||||
Rect { radius: f32 },
|
||||
Ellipse,
|
||||
}
|
||||
|
||||
/// Arka plan + border'ı tek seferde çizer (CSS border-box modeli).
|
||||
/// Container ve shape render'larının ortak kodu.
|
||||
fn draw_box(
|
||||
surface: &mut krilla::surface::Surface<'_>,
|
||||
x: f32,
|
||||
y: f32,
|
||||
w: f32,
|
||||
h: f32,
|
||||
bg_color: Option<&str>,
|
||||
border_color: Option<&str>,
|
||||
border_width: Option<f64>,
|
||||
shape: BoxShape,
|
||||
) {
|
||||
let has_bg = bg_color.is_some();
|
||||
let has_border = border_color.is_some() && border_width.unwrap_or(0.0) > 0.0;
|
||||
|
||||
if !has_bg && !has_border {
|
||||
return;
|
||||
}
|
||||
|
||||
let build_path = |bx: f32, by: f32, bw: f32, bh: f32, shape: &BoxShape| -> Option<krilla::geom::Path> {
|
||||
match shape {
|
||||
BoxShape::Ellipse => build_ellipse_path(bx, by, bw, bh),
|
||||
BoxShape::Rect { radius } => build_rect_path(bx, by, bw, bh, *radius),
|
||||
}
|
||||
};
|
||||
|
||||
if has_border {
|
||||
let bw = mm(border_width.unwrap_or(0.5));
|
||||
let bc = parse_color(border_color.unwrap_or("#000000"));
|
||||
let inset = bw / 2.0;
|
||||
|
||||
// Border durumunda radius'u inset kadar küçült
|
||||
let inset_shape = match shape {
|
||||
BoxShape::Ellipse => BoxShape::Ellipse,
|
||||
BoxShape::Rect { radius } => BoxShape::Rect {
|
||||
radius: (radius - inset).max(0.0),
|
||||
},
|
||||
};
|
||||
|
||||
let path = build_path(x + inset, y + inset, w - bw, h - bw, &inset_shape);
|
||||
if let Some(p) = path {
|
||||
surface.draw_filled_stroked(
|
||||
&p,
|
||||
bg_color.map(parse_color),
|
||||
bc,
|
||||
bw,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let fill = parse_color(bg_color.unwrap_or("#ffffff"));
|
||||
let path = build_path(x, y, w, h, &shape);
|
||||
if let Some(p) = path {
|
||||
surface.draw_filled(&p, fill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,80 +457,29 @@ fn render_shape(
|
||||
style: &ResolvedStyle,
|
||||
content: &Option<ResolvedContent>,
|
||||
) {
|
||||
let has_bg = style.background_color.is_some();
|
||||
let has_border = style.border_color.is_some() && style.border_width.unwrap_or(0.0) > 0.0;
|
||||
|
||||
if !has_bg && !has_border {
|
||||
return;
|
||||
}
|
||||
|
||||
let shape_type = match content {
|
||||
Some(ResolvedContent::Shape { shape_type }) => shape_type.as_str(),
|
||||
_ => "rectangle",
|
||||
};
|
||||
|
||||
let rect_radius = |s: &ResolvedStyle| -> f32 {
|
||||
if shape_type == "rounded_rectangle" {
|
||||
s.border_radius.map(mm).unwrap_or(mm(3.0))
|
||||
} else {
|
||||
s.border_radius.map(mm).unwrap_or(0.0)
|
||||
}
|
||||
let shape = match shape_type {
|
||||
"ellipse" => BoxShape::Ellipse,
|
||||
"rounded_rectangle" => BoxShape::Rect {
|
||||
radius: style.border_radius.map(mm).unwrap_or(mm(3.0)),
|
||||
},
|
||||
_ => BoxShape::Rect {
|
||||
radius: style.border_radius.map(mm).unwrap_or(0.0),
|
||||
},
|
||||
};
|
||||
|
||||
if has_border {
|
||||
let border_width = mm(style.border_width.unwrap_or(0.5));
|
||||
let border_color = parse_color(style.border_color.as_deref().unwrap_or("#000000"));
|
||||
let inset = border_width / 2.0;
|
||||
|
||||
// Fill + stroke tek path ile — anti-aliasing uyumu
|
||||
if let Some(ref bg) = style.background_color {
|
||||
surface.set_fill(Some(fill_from_color(parse_color(bg))));
|
||||
} else {
|
||||
surface.set_fill(None);
|
||||
}
|
||||
surface.set_stroke(Some(Stroke {
|
||||
paint: border_color.into(),
|
||||
width: border_width,
|
||||
opacity: NormalizedF32::ONE,
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
let path = match shape_type {
|
||||
"ellipse" => {
|
||||
build_ellipse_path(x + inset, y + inset, w - border_width, h - border_width)
|
||||
}
|
||||
_ => {
|
||||
let radius = rect_radius(style);
|
||||
build_rect_path(
|
||||
x + inset,
|
||||
y + inset,
|
||||
w - border_width,
|
||||
h - border_width,
|
||||
(radius - inset).max(0.0),
|
||||
)
|
||||
}
|
||||
};
|
||||
if let Some(p) = path {
|
||||
surface.draw_path(&p);
|
||||
}
|
||||
} else {
|
||||
// Sadece fill, border yok
|
||||
surface.set_fill(Some(fill_from_color(parse_color(
|
||||
style.background_color.as_deref().unwrap_or("#ffffff"),
|
||||
))));
|
||||
surface.set_stroke(None);
|
||||
|
||||
let path = match shape_type {
|
||||
"ellipse" => build_ellipse_path(x, y, w, h),
|
||||
_ => build_rect_path(x, y, w, h, rect_radius(style)),
|
||||
};
|
||||
if let Some(p) = path {
|
||||
surface.draw_path(&p);
|
||||
}
|
||||
}
|
||||
|
||||
surface.set_fill(None);
|
||||
surface.set_stroke(None);
|
||||
draw_box(
|
||||
surface,
|
||||
x, y, w, h,
|
||||
style.background_color.as_deref(),
|
||||
style.border_color.as_deref(),
|
||||
style.border_width,
|
||||
shape,
|
||||
);
|
||||
}
|
||||
|
||||
fn render_checkbox(
|
||||
@@ -418,54 +495,24 @@ fn render_checkbox(
|
||||
let border_width = mm(style.border_width.unwrap_or(0.3));
|
||||
let inset = border_width / 2.0;
|
||||
|
||||
// Draw box outline (inset for CSS border-box match)
|
||||
surface.set_fill(None);
|
||||
surface.set_stroke(Some(Stroke {
|
||||
paint: border_color.into(),
|
||||
width: border_width,
|
||||
opacity: NormalizedF32::ONE,
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
if let Some(p) = build_rect_path(
|
||||
x + inset,
|
||||
y + inset,
|
||||
w - border_width,
|
||||
h - border_width,
|
||||
0.0,
|
||||
) {
|
||||
surface.draw_path(&p);
|
||||
if let Some(p) = build_rect_path(x + inset, y + inset, w - border_width, h - border_width, 0.0) {
|
||||
surface.draw_stroked(&p, border_color, border_width);
|
||||
}
|
||||
|
||||
// Draw checkmark if checked
|
||||
if checked {
|
||||
let check_color = parse_color(style.color.as_deref().unwrap_or("#000000"));
|
||||
let stroke_w = w.min(h) * 0.12;
|
||||
surface.set_fill(None);
|
||||
surface.set_stroke(Some(Stroke {
|
||||
paint: check_color.into(),
|
||||
width: stroke_w,
|
||||
opacity: NormalizedF32::ONE,
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
// Checkmark: two lines forming a "✓"
|
||||
let check_path = {
|
||||
let mut pb = PathBuilder::new();
|
||||
let mx = w * 0.2;
|
||||
let my = h * 0.5;
|
||||
pb.move_to(x + mx, y + my);
|
||||
pb.move_to(x + w * 0.2, y + h * 0.5);
|
||||
pb.line_to(x + w * 0.4, y + h * 0.75);
|
||||
pb.line_to(x + w * 0.8, y + h * 0.25);
|
||||
pb.finish()
|
||||
};
|
||||
if let Some(p) = check_path {
|
||||
surface.draw_path(&p);
|
||||
surface.draw_stroked(&p, check_color, stroke_w);
|
||||
}
|
||||
}
|
||||
|
||||
surface.set_fill(None);
|
||||
surface.set_stroke(None);
|
||||
}
|
||||
|
||||
fn render_container_bg(
|
||||
@@ -476,55 +523,16 @@ fn render_container_bg(
|
||||
h: f32,
|
||||
style: &ResolvedStyle,
|
||||
) {
|
||||
let has_bg = style.background_color.is_some();
|
||||
let has_border = style.border_color.is_some() && style.border_width.unwrap_or(0.0) > 0.0;
|
||||
|
||||
if !has_bg && !has_border {
|
||||
return;
|
||||
}
|
||||
|
||||
let radius = style.border_radius.map(mm).unwrap_or(0.0);
|
||||
|
||||
if has_border {
|
||||
let border_width = mm(style.border_width.unwrap_or(0.5));
|
||||
let border_color = parse_color(style.border_color.as_deref().unwrap_or("#000000"));
|
||||
let inset = border_width / 2.0;
|
||||
|
||||
// CSS border-box: stroke path'i border_width/2 içeri çek.
|
||||
// Tek draw_path ile hem fill hem stroke çizerek anti-aliasing uyumunu sağla.
|
||||
if let Some(ref bg) = style.background_color {
|
||||
surface.set_fill(Some(fill_from_color(parse_color(bg))));
|
||||
} else {
|
||||
surface.set_fill(None);
|
||||
}
|
||||
surface.set_stroke(Some(Stroke {
|
||||
paint: border_color.into(),
|
||||
width: border_width,
|
||||
opacity: NormalizedF32::ONE,
|
||||
..Default::default()
|
||||
}));
|
||||
if let Some(path) = build_rect_path(
|
||||
x + inset,
|
||||
y + inset,
|
||||
w - border_width,
|
||||
h - border_width,
|
||||
(radius - inset).max(0.0),
|
||||
) {
|
||||
surface.draw_path(&path);
|
||||
}
|
||||
} else {
|
||||
// Sadece background, border yok
|
||||
surface.set_fill(Some(fill_from_color(parse_color(
|
||||
style.background_color.as_deref().unwrap_or("#ffffff"),
|
||||
))));
|
||||
surface.set_stroke(None);
|
||||
if let Some(path) = build_rect_path(x, y, w, h, radius) {
|
||||
surface.draw_path(&path);
|
||||
}
|
||||
}
|
||||
|
||||
surface.set_fill(None);
|
||||
surface.set_stroke(None);
|
||||
draw_box(
|
||||
surface,
|
||||
x, y, w, h,
|
||||
style.background_color.as_deref(),
|
||||
style.border_color.as_deref(),
|
||||
style.border_width,
|
||||
BoxShape::Rect {
|
||||
radius: style.border_radius.map(mm).unwrap_or(0.0),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -707,32 +715,16 @@ fn render_line(
|
||||
h: f32,
|
||||
style: &ResolvedStyle,
|
||||
) {
|
||||
let stroke_color = style
|
||||
let color = style
|
||||
.stroke_color
|
||||
.as_deref()
|
||||
.map(parse_color)
|
||||
.unwrap_or(rgb::Color::new(0, 0, 0));
|
||||
|
||||
// Çizgiyi filled rectangle olarak çiz — CSS borderTop ile aynı davranış.
|
||||
// Stroke kullanmak sub-pixel anti-aliasing farkları yaratır.
|
||||
surface.set_fill(Some(fill_from_color(stroke_color)));
|
||||
surface.set_stroke(None);
|
||||
|
||||
let rect_path = {
|
||||
let mut pb = PathBuilder::new();
|
||||
// Eleman yüksekliği layout engine tarafından stroke_width olarak hesaplandı.
|
||||
// Tüm eleman alanını dolduran ince dikdörtgen çiz.
|
||||
if let Some(rect) = krilla::geom::Rect::from_xywh(x, y, w, h) {
|
||||
pb.push_rect(rect);
|
||||
}
|
||||
pb.finish()
|
||||
};
|
||||
|
||||
if let Some(p) = rect_path {
|
||||
surface.draw_path(&p);
|
||||
if let Some(path) = build_rect_path(x, y, w, h, 0.0) {
|
||||
surface.draw_filled(&path, color);
|
||||
}
|
||||
|
||||
surface.set_fill(None);
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@@ -935,14 +927,14 @@ fn render_chart(
|
||||
if let Some(f) = font {
|
||||
surface.set_fill(Some(fill_from_color(color)));
|
||||
surface.set_stroke(None);
|
||||
let fs_pt = pt(title.font_size);
|
||||
let fs_pt = mm(title.font_size);
|
||||
let (tw, _) = measurer.measure(&title.text, None, fs_pt, Some("bold"), None);
|
||||
let tx = match title.align.as_str() {
|
||||
"left" => pt(title.x),
|
||||
"right" => pt(title.x) - tw,
|
||||
_ => pt(title.x) - tw / 2.0,
|
||||
"left" => mm(title.x),
|
||||
"right" => mm(title.x) - tw,
|
||||
_ => mm(title.x) - tw / 2.0,
|
||||
};
|
||||
let ty = pt(title.y);
|
||||
let ty = mm(title.y);
|
||||
surface.draw_text(
|
||||
Point::from_xy(tx, ty),
|
||||
f.clone(),
|
||||
@@ -966,26 +958,28 @@ fn render_chart(
|
||||
if bl.stacked {
|
||||
if bar.value > 0.0 {
|
||||
let label = format_value(bar.value);
|
||||
chart_text_centered(
|
||||
chart_text(
|
||||
surface,
|
||||
bar.label_x,
|
||||
bar.label_y,
|
||||
&label,
|
||||
bl.label_font,
|
||||
&bl.label_color,
|
||||
ChartTextAlign::Center,
|
||||
fonts,
|
||||
measurer,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let label = format_value(bar.value);
|
||||
chart_text_centered(
|
||||
chart_text(
|
||||
surface,
|
||||
bar.label_x,
|
||||
bar.label_y,
|
||||
&label,
|
||||
bl.label_font,
|
||||
&bl.label_color,
|
||||
ChartTextAlign::Center,
|
||||
fonts,
|
||||
measurer,
|
||||
);
|
||||
@@ -1015,7 +1009,7 @@ fn render_chart(
|
||||
surface.set_fill(None);
|
||||
surface.set_stroke(Some(Stroke {
|
||||
paint: color.into(),
|
||||
width: pt(ll.line_width),
|
||||
width: mm(ll.line_width),
|
||||
opacity: NormalizedF32::ONE,
|
||||
..Default::default()
|
||||
}));
|
||||
@@ -1023,9 +1017,9 @@ fn render_chart(
|
||||
let mut pb = PathBuilder::new();
|
||||
for (i, (lx, ly)) in points.iter().enumerate() {
|
||||
if i == 0 {
|
||||
pb.move_to(pt(*lx), pt(*ly));
|
||||
pb.move_to(mm(*lx), mm(*ly));
|
||||
} else {
|
||||
pb.line_to(pt(*lx), pt(*ly));
|
||||
pb.line_to(mm(*lx), mm(*ly));
|
||||
}
|
||||
}
|
||||
pb.finish()
|
||||
@@ -1037,24 +1031,8 @@ fn render_chart(
|
||||
// Points
|
||||
if ll.show_points {
|
||||
for (lx, ly) in &points {
|
||||
let r = pt(0.8);
|
||||
let cx = pt(*lx);
|
||||
let cy = pt(*ly);
|
||||
surface.set_fill(Some(fill_from_color(color)));
|
||||
surface.set_stroke(None);
|
||||
let circle = {
|
||||
let mut pb = PathBuilder::new();
|
||||
let k = r * 0.5522848;
|
||||
pb.move_to(cx, cy - r);
|
||||
pb.cubic_to(cx + k, cy - r, cx + r, cy - k, cx + r, cy);
|
||||
pb.cubic_to(cx + r, cy + k, cx + k, cy + r, cx, cy + r);
|
||||
pb.cubic_to(cx - k, cy + r, cx - r, cy + k, cx - r, cy);
|
||||
pb.cubic_to(cx - r, cy - k, cx - k, cy - r, cx, cy - r);
|
||||
pb.close();
|
||||
pb.finish()
|
||||
};
|
||||
if let Some(p) = circle {
|
||||
surface.draw_path(&p);
|
||||
if let Some(circle) = build_circle_path(mm(*lx), mm(*ly), mm(0.8)) {
|
||||
surface.draw_filled(&circle, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1063,13 +1041,14 @@ fn render_chart(
|
||||
if ll.show_labels {
|
||||
for lp in &series_layout.points {
|
||||
let label = format_value(lp.value);
|
||||
chart_text_centered(
|
||||
chart_text(
|
||||
surface,
|
||||
lp.x,
|
||||
lp.y - 1.5,
|
||||
&label,
|
||||
ll.label_font,
|
||||
&ll.label_color,
|
||||
ChartTextAlign::Center,
|
||||
fonts,
|
||||
measurer,
|
||||
);
|
||||
@@ -1114,13 +1093,14 @@ fn render_chart(
|
||||
if pl.show_labels {
|
||||
let pct = (slice.fraction * 100.0).round();
|
||||
let label = format!("{}%", pct);
|
||||
chart_text_centered(
|
||||
chart_text(
|
||||
surface,
|
||||
slice.label_x,
|
||||
slice.label_y,
|
||||
&label,
|
||||
pl.label_font,
|
||||
&pl.label_color,
|
||||
ChartTextAlign::Center,
|
||||
fonts,
|
||||
measurer,
|
||||
);
|
||||
@@ -1136,29 +1116,22 @@ fn render_chart(
|
||||
parse_color("#999999"),
|
||||
0.5,
|
||||
);
|
||||
if slice.cat_label_anchor_end {
|
||||
chart_text_end(
|
||||
surface,
|
||||
slice.cat_label_x,
|
||||
slice.cat_label_y,
|
||||
&slice.cat_label_text,
|
||||
2.5,
|
||||
"#555555",
|
||||
fonts,
|
||||
measurer,
|
||||
);
|
||||
let align = if slice.cat_label_anchor_end {
|
||||
ChartTextAlign::End
|
||||
} else {
|
||||
chart_text_start(
|
||||
surface,
|
||||
slice.cat_label_x,
|
||||
slice.cat_label_y,
|
||||
&slice.cat_label_text,
|
||||
2.5,
|
||||
"#555555",
|
||||
fonts,
|
||||
measurer,
|
||||
);
|
||||
}
|
||||
ChartTextAlign::Start
|
||||
};
|
||||
chart_text(
|
||||
surface,
|
||||
slice.cat_label_x,
|
||||
slice.cat_label_y,
|
||||
&slice.cat_label_text,
|
||||
2.5,
|
||||
"#555555",
|
||||
align,
|
||||
fonts,
|
||||
measurer,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1177,13 +1150,14 @@ fn render_chart(
|
||||
legend.swatch_size,
|
||||
color,
|
||||
);
|
||||
chart_text_start(
|
||||
chart_text(
|
||||
surface,
|
||||
item.text_x,
|
||||
item.text_y,
|
||||
&item.name,
|
||||
legend.font_size,
|
||||
"#666666",
|
||||
ChartTextAlign::Start,
|
||||
fonts,
|
||||
measurer,
|
||||
);
|
||||
@@ -1196,14 +1170,14 @@ fn render_chart(
|
||||
if let Some(ref x_label) = data.x_label {
|
||||
let lx = cl.plot_x + cl.plot_w / 2.0;
|
||||
let ly = base_y_mm + h_mm - 2.0;
|
||||
chart_text_centered(surface, lx, ly, x_label, 2.8, "#666666", fonts, measurer);
|
||||
chart_text(surface, lx, ly, x_label, 2.8, "#666666", ChartTextAlign::Center, fonts, measurer);
|
||||
}
|
||||
if let Some(ref y_label) = data.y_label {
|
||||
let lx = base_x_mm + 3.0;
|
||||
let ly = cl.plot_y + cl.plot_h / 2.0;
|
||||
surface.push_transform(&Transform::from_translate(pt(lx), pt(ly)));
|
||||
surface.push_transform(&Transform::from_translate(mm(lx), mm(ly)));
|
||||
surface.push_transform(&Transform::from_row(0.0, -1.0, 1.0, 0.0, 0.0, 0.0));
|
||||
chart_text_centered(surface, 0.0, 0.0, y_label, 2.8, "#666666", fonts, measurer);
|
||||
chart_text(surface, 0.0, 0.0, y_label, 2.8, "#666666", ChartTextAlign::Center, fonts, measurer);
|
||||
surface.pop();
|
||||
surface.pop();
|
||||
}
|
||||
@@ -1219,7 +1193,7 @@ fn chart_rect(
|
||||
rh: f64,
|
||||
color: rgb::Color,
|
||||
) {
|
||||
let (rx, ry, rw, rh) = (pt(rx), pt(ry), pt(rw), pt(rh));
|
||||
let (rx, ry, rw, rh) = (mm(rx), mm(ry), mm(rw), mm(rh));
|
||||
surface.set_fill(Some(fill_from_color(color)));
|
||||
surface.set_stroke(None);
|
||||
let path = {
|
||||
@@ -1243,7 +1217,7 @@ fn chart_line_seg(
|
||||
color: rgb::Color,
|
||||
width: f32,
|
||||
) {
|
||||
let (x1, y1, x2, y2) = (pt(x1), pt(y1), pt(x2), pt(y2));
|
||||
let (x1, y1, x2, y2) = (mm(x1), mm(y1), mm(x2), mm(y2));
|
||||
surface.set_fill(None);
|
||||
surface.set_stroke(Some(Stroke {
|
||||
paint: color.into(),
|
||||
@@ -1262,93 +1236,52 @@ fn chart_line_seg(
|
||||
}
|
||||
}
|
||||
|
||||
/// Chart icin metin ciz — tek satirlik, centered
|
||||
/// font_size_mm: SVG viewBox'taki mm cinsinden boyut, pt'ye cevrilir
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn chart_text_centered(
|
||||
surface: &mut krilla::surface::Surface<'_>,
|
||||
cx_mm: f64,
|
||||
cy_mm: f64,
|
||||
text: &str,
|
||||
font_size_mm: f64,
|
||||
color_hex: &str,
|
||||
fonts: &FontCollection,
|
||||
measurer: &mut TextMeasurer,
|
||||
) {
|
||||
let font = fonts.get(None, None, None);
|
||||
let Some(f) = font else {
|
||||
return;
|
||||
};
|
||||
let color = parse_color(color_hex);
|
||||
let fs_pt = pt(font_size_mm);
|
||||
let (tw, _) = measurer.measure(text, None, fs_pt, None, None);
|
||||
surface.set_fill(Some(fill_from_color(color)));
|
||||
surface.set_stroke(None);
|
||||
surface.draw_text(
|
||||
Point::from_xy(pt(cx_mm) - tw / 2.0, pt(cy_mm)),
|
||||
f.clone(),
|
||||
fs_pt,
|
||||
text,
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
/// Chart metin hizalama modu
|
||||
enum ChartTextAlign {
|
||||
Start,
|
||||
Center,
|
||||
End,
|
||||
}
|
||||
|
||||
/// Chart icin metin ciz — end-aligned (sag hizali)
|
||||
/// Chart için tek satır metin çiz (mm cinsinden koordinatlar, pt'ye çevrilir)
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn chart_text_end(
|
||||
surface: &mut krilla::surface::Surface<'_>,
|
||||
right_x_mm: f64,
|
||||
cy_mm: f64,
|
||||
text: &str,
|
||||
font_size_mm: f64,
|
||||
color_hex: &str,
|
||||
fonts: &FontCollection,
|
||||
measurer: &mut TextMeasurer,
|
||||
) {
|
||||
let font = fonts.get(None, None, None);
|
||||
let Some(f) = font else {
|
||||
return;
|
||||
};
|
||||
let color = parse_color(color_hex);
|
||||
let fs_pt = pt(font_size_mm);
|
||||
let (tw, _) = measurer.measure(text, None, fs_pt, None, None);
|
||||
surface.set_fill(Some(fill_from_color(color)));
|
||||
surface.set_stroke(None);
|
||||
surface.draw_text(
|
||||
Point::from_xy(pt(right_x_mm) - tw, pt(cy_mm)),
|
||||
f.clone(),
|
||||
fs_pt,
|
||||
text,
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
}
|
||||
|
||||
/// Chart icin metin ciz — start-aligned (sol hizali)
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn chart_text_start(
|
||||
fn chart_text(
|
||||
surface: &mut krilla::surface::Surface<'_>,
|
||||
x_mm: f64,
|
||||
cy_mm: f64,
|
||||
y_mm: f64,
|
||||
text: &str,
|
||||
font_size_mm: f64,
|
||||
color_hex: &str,
|
||||
align: ChartTextAlign,
|
||||
fonts: &FontCollection,
|
||||
_measurer: &mut TextMeasurer,
|
||||
measurer: &mut TextMeasurer,
|
||||
) {
|
||||
let font = fonts.get(None, None, None);
|
||||
let Some(f) = font else {
|
||||
let Some(font) = fonts.get(None, None, None) else {
|
||||
return;
|
||||
};
|
||||
let color = parse_color(color_hex);
|
||||
let fs_pt = pt(font_size_mm);
|
||||
let fs = mm(font_size_mm);
|
||||
let px = mm(x_mm);
|
||||
let py = mm(y_mm);
|
||||
|
||||
let draw_x = match align {
|
||||
ChartTextAlign::Start => px,
|
||||
ChartTextAlign::Center => {
|
||||
let (tw, _) = measurer.measure(text, None, fs, None, None);
|
||||
px - tw / 2.0
|
||||
}
|
||||
ChartTextAlign::End => {
|
||||
let (tw, _) = measurer.measure(text, None, fs, None, None);
|
||||
px - tw
|
||||
}
|
||||
};
|
||||
|
||||
surface.set_fill(Some(fill_from_color(color)));
|
||||
surface.set_stroke(None);
|
||||
surface.draw_text(
|
||||
Point::from_xy(pt(x_mm), pt(cy_mm)),
|
||||
f.clone(),
|
||||
fs_pt,
|
||||
Point::from_xy(draw_x, py),
|
||||
font.clone(),
|
||||
fs,
|
||||
text,
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
@@ -1363,13 +1296,14 @@ fn render_chart_y_axis(
|
||||
measurer: &mut TextMeasurer,
|
||||
) {
|
||||
for tick in &y_axis.ticks {
|
||||
chart_text_end(
|
||||
chart_text(
|
||||
surface,
|
||||
y_axis.axis_x - 1.5,
|
||||
tick.y + 0.8,
|
||||
&tick.label,
|
||||
2.3,
|
||||
"#666666",
|
||||
ChartTextAlign::End,
|
||||
fonts,
|
||||
measurer,
|
||||
);
|
||||
@@ -1409,31 +1343,33 @@ fn render_chart_x_labels(
|
||||
let angle = x_labels.rotate_angle;
|
||||
for label in &x_labels.labels {
|
||||
if angle > 0.0 {
|
||||
surface.push_transform(&Transform::from_translate(pt(label.x), pt(label.y)));
|
||||
surface.push_transform(&Transform::from_translate(mm(label.x), mm(label.y)));
|
||||
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(
|
||||
chart_text(
|
||||
surface,
|
||||
0.0,
|
||||
0.0,
|
||||
&label.text,
|
||||
2.2,
|
||||
"#666666",
|
||||
ChartTextAlign::End,
|
||||
fonts,
|
||||
measurer,
|
||||
);
|
||||
surface.pop();
|
||||
surface.pop();
|
||||
} else {
|
||||
chart_text_centered(
|
||||
chart_text(
|
||||
surface,
|
||||
label.x,
|
||||
label.y,
|
||||
&label.text,
|
||||
2.5,
|
||||
"#666666",
|
||||
ChartTextAlign::Center,
|
||||
fonts,
|
||||
measurer,
|
||||
);
|
||||
@@ -1452,19 +1388,19 @@ fn build_arc_path(
|
||||
) -> Option<krilla::geom::Path> {
|
||||
let mut pb = PathBuilder::new();
|
||||
|
||||
let sx = pt(cx + radius * start.cos());
|
||||
let sy = pt(cy + radius * start.sin());
|
||||
let sx = mm(cx + radius * start.cos());
|
||||
let sy = mm(cy + radius * start.sin());
|
||||
|
||||
if inner_r > 0.0 {
|
||||
pb.move_to(sx, sy);
|
||||
approximate_arc(&mut pb, cx, cy, radius, start, end);
|
||||
let ix = pt(cx + inner_r * end.cos());
|
||||
let iy = pt(cy + inner_r * end.sin());
|
||||
let ix = mm(cx + inner_r * end.cos());
|
||||
let iy = mm(cy + inner_r * end.sin());
|
||||
pb.line_to(ix, iy);
|
||||
approximate_arc(&mut pb, cx, cy, inner_r, end, start);
|
||||
pb.close();
|
||||
} else {
|
||||
pb.move_to(pt(cx), pt(cy));
|
||||
pb.move_to(mm(cx), mm(cy));
|
||||
pb.line_to(sx, sy);
|
||||
approximate_arc(&mut pb, cx, cy, radius, start, end);
|
||||
pb.close();
|
||||
@@ -1496,7 +1432,7 @@ fn approximate_arc(pb: &mut PathBuilder, cx: f64, cy: f64, r: f64, start: f64, e
|
||||
let c2x = p2x + k * r * a2.sin();
|
||||
let c2y = p2y - k * r * a2.cos();
|
||||
|
||||
pb.cubic_to(pt(c1x), pt(c1y), pt(c2x), pt(c2y), pt(p2x), pt(p2y));
|
||||
pb.cubic_to(mm(c1x), mm(c1y), mm(c2x), mm(c2y), mm(p2x), mm(p2y));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1792,7 +1728,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_pt_conversion() {
|
||||
let result = pt(25.4);
|
||||
let result = mm(25.4);
|
||||
assert!((result - 72.0).abs() < 0.01);
|
||||
}
|
||||
|
||||
|
||||
@@ -279,6 +279,30 @@ fn build_container(
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
/// Leaf node oluştur ve node_map'e kaydet (tekrarlayan boilerplate'i ortadan kaldırır).
|
||||
fn register_leaf(
|
||||
taffy: &mut TaffyTree<MeasureContext>,
|
||||
node_map: &mut HashMap<NodeId, NodeInfo>,
|
||||
style: Style,
|
||||
id: &str,
|
||||
element_type: &str,
|
||||
content: Option<ResolvedContent>,
|
||||
resolved_style: ResolvedStyle,
|
||||
) -> Result<NodeId, LayoutError> {
|
||||
let node = taffy.new_leaf(style)?;
|
||||
node_map.insert(
|
||||
node,
|
||||
NodeInfo {
|
||||
element_id: id.to_string(),
|
||||
element_type: element_type.to_string(),
|
||||
content,
|
||||
style: resolved_style,
|
||||
children_ids: vec![],
|
||||
},
|
||||
);
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
/// Herhangi bir element tipini taffy node'a çevir
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_element(
|
||||
@@ -354,78 +378,45 @@ fn build_element(
|
||||
),
|
||||
TemplateElement::Line(e) => {
|
||||
let stroke_w = e.style.stroke_width.unwrap_or(0.5);
|
||||
let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction);
|
||||
|
||||
// Line: genişlik parent'tan, yükseklik stroke width
|
||||
let mut leaf_style = style;
|
||||
let mut style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction);
|
||||
if matches!(e.base.size.height, SizeValue::Auto) {
|
||||
leaf_style.size.height = Dimension::length(mm_to_pt(stroke_w));
|
||||
style.size.height = Dimension::length(mm_to_pt(stroke_w));
|
||||
}
|
||||
|
||||
let node = taffy.new_leaf(leaf_style)?;
|
||||
node_map.insert(
|
||||
node,
|
||||
NodeInfo {
|
||||
element_id: e.base.id.clone(),
|
||||
element_type: e.type_str().to_string(),
|
||||
content: Some(ResolvedContent::Line),
|
||||
style: {
|
||||
let mut s: ResolvedStyle = (&e.style).into();
|
||||
s.stroke_width = Some(stroke_w);
|
||||
s
|
||||
},
|
||||
children_ids: vec![],
|
||||
},
|
||||
);
|
||||
Ok(node)
|
||||
let mut rs: ResolvedStyle = (&e.style).into();
|
||||
rs.stroke_width = Some(stroke_w);
|
||||
register_leaf(
|
||||
taffy, node_map, style,
|
||||
&e.base.id, e.type_str(),
|
||||
Some(ResolvedContent::Line),
|
||||
rs,
|
||||
)
|
||||
}
|
||||
TemplateElement::Image(e) => {
|
||||
let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction);
|
||||
let src = resolved.images.get(&e.base.id).cloned().unwrap_or_default();
|
||||
|
||||
let node = taffy.new_leaf(style)?;
|
||||
node_map.insert(
|
||||
node,
|
||||
NodeInfo {
|
||||
element_id: e.base.id.clone(),
|
||||
element_type: e.type_str().to_string(),
|
||||
content: Some(ResolvedContent::Image { src }),
|
||||
style: (&e.style).into(),
|
||||
children_ids: vec![],
|
||||
},
|
||||
);
|
||||
Ok(node)
|
||||
register_leaf(
|
||||
taffy, node_map, style,
|
||||
&e.base.id, e.type_str(),
|
||||
Some(ResolvedContent::Image { src }),
|
||||
(&e.style).into(),
|
||||
)
|
||||
}
|
||||
TemplateElement::Barcode(e) => {
|
||||
let mut style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction);
|
||||
let value = resolved.barcodes.get(&e.base.id).cloned().unwrap_or_default();
|
||||
|
||||
// Barcode leaf'e minimum boyut ver (MeasureFunc yok, Auto=0 olur)
|
||||
let is_qr = e.format == "qr";
|
||||
let default_h = if is_qr { 20.0 } else { 15.0 }; // mm
|
||||
let default_w = if is_qr { 20.0 } else { 40.0 }; // mm
|
||||
if matches!(e.base.size.height, SizeValue::Auto) {
|
||||
style.min_size.height = Dimension::length(mm_to_pt(default_h));
|
||||
style.min_size.height = Dimension::length(mm_to_pt(if is_qr { 20.0 } else { 15.0 }));
|
||||
}
|
||||
if matches!(e.base.size.width, SizeValue::Auto) {
|
||||
style.min_size.width = Dimension::length(mm_to_pt(default_w));
|
||||
style.min_size.width = Dimension::length(mm_to_pt(if is_qr { 20.0 } else { 40.0 }));
|
||||
}
|
||||
|
||||
let node = taffy.new_leaf(style)?;
|
||||
node_map.insert(
|
||||
node,
|
||||
NodeInfo {
|
||||
element_id: e.base.id.clone(),
|
||||
element_type: e.type_str().to_string(),
|
||||
content: Some(ResolvedContent::Barcode {
|
||||
format: e.format.clone(),
|
||||
value,
|
||||
}),
|
||||
style: (&e.style).into(),
|
||||
children_ids: vec![],
|
||||
},
|
||||
);
|
||||
Ok(node)
|
||||
register_leaf(
|
||||
taffy, node_map, style,
|
||||
&e.base.id, e.type_str(),
|
||||
Some(ResolvedContent::Barcode { format: e.format.clone(), value }),
|
||||
(&e.style).into(),
|
||||
)
|
||||
}
|
||||
TemplateElement::RepeatingTable(e) => {
|
||||
// Tabloyu container ağacına expand et (cache ile)
|
||||
@@ -459,52 +450,33 @@ fn build_element(
|
||||
}
|
||||
TemplateElement::Shape(e) => {
|
||||
let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction);
|
||||
let node = taffy.new_leaf(style)?;
|
||||
node_map.insert(
|
||||
node,
|
||||
NodeInfo {
|
||||
element_id: e.base.id.clone(),
|
||||
element_type: e.type_str().to_string(),
|
||||
content: Some(ResolvedContent::Shape {
|
||||
shape_type: e.shape_type.clone(),
|
||||
}),
|
||||
style: (&e.style).into(),
|
||||
children_ids: vec![],
|
||||
},
|
||||
);
|
||||
Ok(node)
|
||||
register_leaf(
|
||||
taffy, node_map, style,
|
||||
&e.base.id, e.type_str(),
|
||||
Some(ResolvedContent::Shape { shape_type: e.shape_type.clone() }),
|
||||
(&e.style).into(),
|
||||
)
|
||||
}
|
||||
TemplateElement::Checkbox(e) => {
|
||||
let checked_str = resolved
|
||||
let checked = resolved
|
||||
.texts
|
||||
.get(&e.base.id)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("false");
|
||||
let checked = checked_str == "true";
|
||||
.map(|s| s == "true")
|
||||
.unwrap_or(false);
|
||||
let box_size_mm = e.style.size.unwrap_or(4.0);
|
||||
let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction);
|
||||
|
||||
// Auto size → square based on style.size
|
||||
let mut leaf_style = style;
|
||||
let mut style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction);
|
||||
if matches!(e.base.size.width, SizeValue::Auto) {
|
||||
leaf_style.size.width = Dimension::length(mm_to_pt(box_size_mm));
|
||||
style.size.width = Dimension::length(mm_to_pt(box_size_mm));
|
||||
}
|
||||
if matches!(e.base.size.height, SizeValue::Auto) {
|
||||
leaf_style.size.height = Dimension::length(mm_to_pt(box_size_mm));
|
||||
style.size.height = Dimension::length(mm_to_pt(box_size_mm));
|
||||
}
|
||||
|
||||
let node = taffy.new_leaf(leaf_style)?;
|
||||
node_map.insert(
|
||||
node,
|
||||
NodeInfo {
|
||||
element_id: e.base.id.clone(),
|
||||
element_type: e.type_str().to_string(),
|
||||
content: Some(ResolvedContent::Checkbox { checked }),
|
||||
style: (&e.style).into(),
|
||||
children_ids: vec![],
|
||||
},
|
||||
);
|
||||
Ok(node)
|
||||
register_leaf(
|
||||
taffy, node_map, style,
|
||||
&e.base.id, e.type_str(),
|
||||
Some(ResolvedContent::Checkbox { checked }),
|
||||
(&e.style).into(),
|
||||
)
|
||||
}
|
||||
TemplateElement::RichText(e) => {
|
||||
let spans = resolved.rich_texts.get(&e.base.id).cloned().unwrap_or_default();
|
||||
@@ -563,28 +535,20 @@ fn build_element(
|
||||
}
|
||||
TemplateElement::Chart(e) => {
|
||||
let mut style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction);
|
||||
// Default minimum boyut — Auto ise chart cok kucuk olmasin
|
||||
if matches!(e.base.size.width, SizeValue::Auto) {
|
||||
style.min_size.width = Dimension::length(mm_to_pt(80.0));
|
||||
}
|
||||
if matches!(e.base.size.height, SizeValue::Auto) {
|
||||
style.min_size.height = Dimension::length(mm_to_pt(60.0));
|
||||
}
|
||||
let node = taffy.new_leaf(style)?;
|
||||
node_map.insert(
|
||||
node,
|
||||
NodeInfo {
|
||||
element_id: e.base.id.clone(),
|
||||
element_type: e.type_str().to_string(),
|
||||
content: None, // SVG collect_layout'ta uretilecek
|
||||
style: ResolvedStyle::default(),
|
||||
children_ids: vec![],
|
||||
},
|
||||
);
|
||||
Ok(node)
|
||||
register_leaf(
|
||||
taffy, node_map, style,
|
||||
&e.base.id, e.type_str(),
|
||||
None, // SVG collect_layout'ta üretilecek
|
||||
ResolvedStyle::default(),
|
||||
)
|
||||
}
|
||||
TemplateElement::PageBreak(e) => {
|
||||
// Küçük yükseklik — editörde görünür olması için (0.5mm ≈ 1.4pt)
|
||||
let style = Style {
|
||||
size: Size {
|
||||
width: Dimension::auto(),
|
||||
@@ -592,18 +556,12 @@ fn build_element(
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let node = taffy.new_leaf(style)?;
|
||||
node_map.insert(
|
||||
node,
|
||||
NodeInfo {
|
||||
element_id: e.base.id.clone(),
|
||||
element_type: e.type_str().to_string(),
|
||||
content: None,
|
||||
style: ResolvedStyle::default(),
|
||||
children_ids: vec![],
|
||||
},
|
||||
);
|
||||
Ok(node)
|
||||
register_leaf(
|
||||
taffy, node_map, style,
|
||||
&e.base.id, e.type_str(),
|
||||
None,
|
||||
ResolvedStyle::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -763,62 +721,12 @@ fn collect_layout(
|
||||
let w_mm = pt_to_mm(layout.size.width);
|
||||
let h_mm = pt_to_mm(layout.size.height);
|
||||
|
||||
// Chart elementleri icin SVG uret (boyutlar artik belli)
|
||||
// Chart elementleri için SVG üret (boyutlar artık belli)
|
||||
let content = if info.element_type == "chart" {
|
||||
resolved.charts.get(&info.element_id).map(|cd| {
|
||||
use crate::chart_layout::DEFAULT_COLORS;
|
||||
use crate::{ChartRenderData, ChartSeriesData};
|
||||
|
||||
// Renk paleti olustur
|
||||
let n_colors = cd.categories.len().max(cd.series.len()).max(1);
|
||||
let colors: Vec<String> = (0..n_colors)
|
||||
.map(|i| {
|
||||
cd.style
|
||||
.colors
|
||||
.as_ref()
|
||||
.and_then(|c| c.get(i).cloned())
|
||||
.unwrap_or_else(|| DEFAULT_COLORS[i % DEFAULT_COLORS.len()].to_string())
|
||||
})
|
||||
.collect();
|
||||
|
||||
ResolvedContent::Chart {
|
||||
svg: crate::chart_render::render_svg(cd, w_mm, h_mm),
|
||||
chart_data: Box::new(ChartRenderData {
|
||||
chart_type: cd.chart_type.clone(),
|
||||
categories: cd.categories.clone(),
|
||||
series: cd
|
||||
.series
|
||||
.iter()
|
||||
.map(|s| ChartSeriesData {
|
||||
name: s.name.clone(),
|
||||
values: s.values.clone(),
|
||||
})
|
||||
.collect(),
|
||||
title_text: cd.title.as_ref().map(|t| t.text.clone()),
|
||||
title_font_size: cd.title.as_ref().and_then(|t| t.font_size),
|
||||
title_color: cd.title.as_ref().and_then(|t| t.color.clone()),
|
||||
colors,
|
||||
show_labels: cd.labels.as_ref().is_some_and(|l| l.show),
|
||||
label_font_size: cd.labels.as_ref().and_then(|l| l.font_size),
|
||||
show_grid: cd.axis.as_ref().and_then(|a| a.show_grid).unwrap_or(true),
|
||||
grid_color: cd.axis.as_ref().and_then(|a| a.grid_color.clone()),
|
||||
bar_gap: cd.style.bar_gap,
|
||||
stacked: matches!(
|
||||
cd.group_mode,
|
||||
Some(dreport_core::models::GroupMode::Stacked)
|
||||
),
|
||||
inner_radius: cd.style.inner_radius,
|
||||
show_points: cd.style.show_points,
|
||||
line_width: cd.style.line_width,
|
||||
background_color: cd.style.background_color.clone(),
|
||||
label_color: cd.labels.as_ref().and_then(|l| l.color.clone()),
|
||||
legend_show: cd.legend.as_ref().is_some_and(|l| l.show),
|
||||
legend_position: cd.legend.as_ref().and_then(|l| l.position.clone()),
|
||||
legend_font_size: cd.legend.as_ref().and_then(|l| l.font_size),
|
||||
x_label: cd.axis.as_ref().and_then(|a| a.x_label.clone()),
|
||||
y_label: cd.axis.as_ref().and_then(|a| a.y_label.clone()),
|
||||
title_align: cd.title.as_ref().and_then(|t| t.align.clone()),
|
||||
}),
|
||||
chart_data: Box::new(crate::ChartRenderData::from(cd)),
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user