1use crate::theme::{Palette, palette};
5use drawlang_core::geom::*;
6use drawlang_core::model::*;
7use drawlang_core::text::FONT_FAMILY;
8
9pub fn fmt_f(v: f64) -> String {
12 let r = (v * 100.0).round() / 100.0;
13 if r == r.trunc() {
14 format!("{}", r as i64)
15 } else {
16 let s = format!("{r:.2}");
17 s.trim_end_matches('0').trim_end_matches('.').to_string()
18 }
19}
20
21fn esc(s: &str) -> String {
22 s.replace('&', "&")
23 .replace('<', "<")
24 .replace('>', ">")
25 .replace('"', """)
26}
27
28pub fn render_svg(doc: &Document, g: &Geometry) -> String {
29 let p = palette(doc.canvas.theme);
30 let mut out = String::with_capacity(16 * 1024);
31 let (w, h) = (fmt_f(g.width), fmt_f(g.height));
32 out.push_str(&format!(
33 r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" width="{w}" height="{h}" font-family="{FONT_FAMILY}, Helvetica, Arial, sans-serif">"#
34 ));
35 out.push('\n');
36 out.push_str(&format!(
37 r#"<rect width="{w}" height="{h}" fill="{}"/>"#,
38 p.bg
39 ));
40 out.push('\n');
41
42 if let Some(t) = &g.title {
43 draw_label(&mut out, t, p.ink);
44 }
45
46 for id in doc.walk() {
48 if id == doc.root {
49 continue;
50 }
51 draw_element(&mut out, doc, g, id, p);
52 }
53
54 for route in &g.routes {
56 draw_edge(&mut out, doc, route, p);
57 }
58 for route in &g.routes {
59 if let Some(label) = &route.label {
60 draw_halo_label(&mut out, label, p);
61 }
62 }
63 draw_ports(&mut out, doc, g, p);
64
65 out.push_str("</svg>\n");
66 out
67}
68
69fn draw_element(out: &mut String, doc: &Document, g: &Geometry, id: ElementId, p: &Palette) {
70 let el = doc.el(id);
71 if el.kind.is_container() {
72 return; }
74 let r = g.rect(id);
75 let style = doc.resolved_style(id);
76
77 match el.kind {
78 ElementKind::Group => {
79 let stroke = p.resolve(style.color.as_ref(), p.group_border);
80 let sw = style.stroke.unwrap_or(1.2);
81 let corner = style.corner.unwrap_or(10.0);
82 let wash = p.resolve(style.fill.as_ref(), p.ink);
83 let wash_op = if style.fill.is_some() {
84 1.0
85 } else {
86 p.group_wash_opacity
87 };
88 out.push_str(&format!(
89 r#"<rect x="{}" y="{}" width="{}" height="{}" rx="{}" fill="{}" fill-opacity="{}" stroke="{}" stroke-width="{}"{}/>"#,
90 fmt_f(r.x), fmt_f(r.y), fmt_f(r.w), fmt_f(r.h), fmt_f(corner),
91 wash, fmt_f(wash_op), stroke, fmt_f(sw),
92 dash_attr(&style),
93 ));
94 out.push('\n');
95 if let Some(label) = g.labels.get(&id) {
96 let color = p.resolve(style.text_color.as_ref(), p.group_label);
97 draw_label(out, label, &color);
98 }
99 }
100 ElementKind::Node => {
101 let parent_is_node = el
102 .parent
103 .map(|q| doc.el(q).kind == ElementKind::Node)
104 .unwrap_or(false);
105 let default_fill = if parent_is_node { p.inner } else { p.surface };
106 let fill = p.resolve(style.fill.as_ref(), default_fill);
107 let stroke = p.resolve(style.color.as_ref(), p.node_border);
108 let sw = style.stroke.unwrap_or(1.4);
109 let shape = style.shape.unwrap_or(Shape::Rect);
110 match shape {
111 Shape::Rect | Shape::Pill => {
112 let corner = if shape == Shape::Pill {
113 r.h / 2.0
114 } else {
115 style.corner.unwrap_or(7.0)
116 };
117 out.push_str(&format!(
118 r#"<rect x="{}" y="{}" width="{}" height="{}" rx="{}" fill="{}" stroke="{}" stroke-width="{}"{}/>"#,
119 fmt_f(r.x), fmt_f(r.y), fmt_f(r.w), fmt_f(r.h), fmt_f(corner),
120 fill, stroke, fmt_f(sw),
121 dash_attr(&style),
122 ));
123 }
124 Shape::Ellipse => {
125 out.push_str(&format!(
126 r#"<ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="{}" stroke="{}" stroke-width="{}"{}/>"#,
127 fmt_f(r.cx()), fmt_f(r.cy()), fmt_f(r.w / 2.0), fmt_f(r.h / 2.0),
128 fill, stroke, fmt_f(sw),
129 dash_attr(&style),
130 ));
131 }
132 }
133 out.push('\n');
134 if let Some(label) = g.labels.get(&id) {
135 let color = match &style.text_color {
136 Some(c) => p.resolve(Some(c), p.ink),
137 None if style.fill.is_some() && luminance(&fill) < 0.45 => {
139 "#FAFBFC".to_string()
140 }
141 None if parent_is_node => p.edge_label.to_string(),
142 None => p.ink.to_string(),
143 };
144 draw_label(out, label, &color);
145 }
146 }
147 _ => {}
148 }
149}
150
151fn luminance(hex: &str) -> f64 {
153 let h = hex.trim_start_matches('#');
154 let (r, g, b) = match h.len() {
155 3 => (
156 u8::from_str_radix(&h[0..1].repeat(2), 16).unwrap_or(0),
157 u8::from_str_radix(&h[1..2].repeat(2), 16).unwrap_or(0),
158 u8::from_str_radix(&h[2..3].repeat(2), 16).unwrap_or(0),
159 ),
160 6 | 8 => (
161 u8::from_str_radix(&h[0..2], 16).unwrap_or(0),
162 u8::from_str_radix(&h[2..4], 16).unwrap_or(0),
163 u8::from_str_radix(&h[4..6], 16).unwrap_or(0),
164 ),
165 _ => return 1.0,
166 };
167 (0.2126 * r as f64 + 0.7152 * g as f64 + 0.0722 * b as f64) / 255.0
168}
169
170fn dash_attr(style: &Style) -> &'static str {
171 if style.dashed == Some(true) {
172 r#" stroke-dasharray="6 4""#
173 } else {
174 ""
175 }
176}
177
178fn draw_label(out: &mut String, l: &LabelBlock, color: &str) {
179 let weight = if l.bold { r#" font-weight="bold""# } else { "" };
180 for (i, line) in l.lines.iter().enumerate() {
181 if line.is_empty() {
182 continue;
183 }
184 let y = l.y + l.baseline + l.line_height * i as f64;
185 let (x, anchor) = match l.align {
186 TextAlign::Center => (l.x + l.width / 2.0, r#" text-anchor="middle""#),
187 TextAlign::Left => (l.x, ""),
188 };
189 out.push_str(&format!(
190 r#"<text x="{}" y="{}" font-size="{}"{}{} fill="{}">{}</text>"#,
191 fmt_f(x),
192 fmt_f(y),
193 fmt_f(l.size),
194 weight,
195 anchor,
196 color,
197 esc(line)
198 ));
199 out.push('\n');
200 }
201}
202
203fn draw_halo_label(out: &mut String, l: &LabelBlock, p: &Palette) {
204 let pad_x = 4.0;
205 let pad_y = 2.0;
206 out.push_str(&format!(
207 r#"<rect x="{}" y="{}" width="{}" height="{}" rx="3" fill="{}" fill-opacity="0.92"/>"#,
208 fmt_f(l.x - pad_x),
209 fmt_f(l.y - pad_y),
210 fmt_f(l.width + 2.0 * pad_x),
211 fmt_f(l.height + 2.0 * pad_y),
212 p.bg,
213 ));
214 out.push('\n');
215 draw_label(out, l, p.edge_label);
216}
217
218fn draw_edge(out: &mut String, doc: &Document, route: &EdgeRoute, p: &Palette) {
219 let edge = &doc.edges[route.edge];
220 let style = doc.edge_style(edge);
221 let color = p.resolve(style.color.as_ref(), p.edge);
222 let sw = style.stroke.unwrap_or(1.6);
223 let arrow_len = 7.0 + sw * 1.6;
224
225 let mut pts = route.points.clone();
227 let (end_apex, end_dir) = end_tangent(&pts, route.kind);
228 if route.arrow_end {
229 let trimmed = (
230 end_apex.0 - end_dir.0 * arrow_len * 0.8,
231 end_apex.1 - end_dir.1 * arrow_len * 0.8,
232 );
233 set_endpoint(&mut pts, route.kind, true, trimmed);
234 }
235 let (start_apex, start_dir) = start_tangent(&pts, route.kind);
236 if route.arrow_start {
237 let trimmed = (
238 start_apex.0 - start_dir.0 * arrow_len * 0.8,
239 start_apex.1 - start_dir.1 * arrow_len * 0.8,
240 );
241 set_endpoint(&mut pts, route.kind, false, trimmed);
242 }
243
244 let d = match route.kind {
245 RouteKind::Straight => {
246 format!(
247 "M {} {} L {} {}",
248 fmt_f(pts[0].0),
249 fmt_f(pts[0].1),
250 fmt_f(pts[1].0),
251 fmt_f(pts[1].1)
252 )
253 }
254 RouteKind::Cubic => format!(
255 "M {} {} C {} {}, {} {}, {} {}",
256 fmt_f(pts[0].0),
257 fmt_f(pts[0].1),
258 fmt_f(pts[1].0),
259 fmt_f(pts[1].1),
260 fmt_f(pts[2].0),
261 fmt_f(pts[2].1),
262 fmt_f(pts[3].0),
263 fmt_f(pts[3].1),
264 ),
265 RouteKind::Ortho => ortho_path(&pts),
266 };
267
268 out.push_str(&format!(
269 r#"<path d="{d}" fill="none" stroke="{color}" stroke-width="{}" stroke-linecap="round" stroke-linejoin="round"{}/>"#,
270 fmt_f(sw),
271 dash_attr(&style),
272 ));
273 out.push('\n');
274
275 if route.arrow_end {
276 draw_arrow(out, end_apex, end_dir, arrow_len, &color);
277 }
278 if route.arrow_start {
279 draw_arrow(out, start_apex, start_dir, arrow_len, &color);
280 }
281}
282
283fn ortho_path(pts: &[(f64, f64)]) -> String {
285 const R: f64 = 8.0;
286 if pts.len() < 3 {
287 return format!(
288 "M {} {} L {} {}",
289 fmt_f(pts[0].0),
290 fmt_f(pts[0].1),
291 fmt_f(pts[pts.len() - 1].0),
292 fmt_f(pts[pts.len() - 1].1)
293 );
294 }
295 let mut d = format!("M {} {}", fmt_f(pts[0].0), fmt_f(pts[0].1));
296 for i in 1..pts.len() - 1 {
297 let prev = pts[i - 1];
298 let cur = pts[i];
299 let next = pts[i + 1];
300 let in_len = ((cur.0 - prev.0).abs() + (cur.1 - prev.1).abs()).max(1e-6);
301 let out_len = ((next.0 - cur.0).abs() + (next.1 - cur.1).abs()).max(1e-6);
302 let r = R.min(in_len / 2.0).min(out_len / 2.0);
303 let in_dir = axis_dir(prev, cur);
305 let out_dir = axis_dir(cur, next);
306 let before = (cur.0 - in_dir.0 * r, cur.1 - in_dir.1 * r);
307 let after = (cur.0 + out_dir.0 * r, cur.1 + out_dir.1 * r);
308 d.push_str(&format!(
309 " L {} {} Q {} {}, {} {}",
310 fmt_f(before.0),
311 fmt_f(before.1),
312 fmt_f(cur.0),
313 fmt_f(cur.1),
314 fmt_f(after.0),
315 fmt_f(after.1),
316 ));
317 }
318 let last = pts[pts.len() - 1];
319 d.push_str(&format!(" L {} {}", fmt_f(last.0), fmt_f(last.1)));
320 d
321}
322
323fn axis_dir(a: (f64, f64), b: (f64, f64)) -> (f64, f64) {
325 let dx = b.0 - a.0;
326 let dy = b.1 - a.1;
327 if dx.abs() >= dy.abs() {
328 (if dx >= 0.0 { 1.0 } else { -1.0 }, 0.0)
329 } else {
330 (0.0, if dy >= 0.0 { 1.0 } else { -1.0 })
331 }
332}
333
334fn end_tangent(pts: &[(f64, f64)], kind: RouteKind) -> ((f64, f64), (f64, f64)) {
335 let (a, b) = match kind {
336 RouteKind::Cubic => (pts[2], pts[3]),
337 _ => (pts[pts.len() - 2], pts[pts.len() - 1]),
338 };
339 (b, norm((b.0 - a.0, b.1 - a.1)))
340}
341
342fn start_tangent(pts: &[(f64, f64)], kind: RouteKind) -> ((f64, f64), (f64, f64)) {
343 let (a, b) = match kind {
344 RouteKind::Cubic => (pts[1], pts[0]),
345 _ => (pts[1], pts[0]),
346 };
347 (b, norm((b.0 - a.0, b.1 - a.1)))
348}
349
350fn set_endpoint(pts: &mut [(f64, f64)], _kind: RouteKind, end: bool, p: (f64, f64)) {
351 if end {
352 let n = pts.len();
353 pts[n - 1] = p;
354 } else {
355 pts[0] = p;
356 }
357}
358
359fn norm(v: (f64, f64)) -> (f64, f64) {
360 let len = (v.0 * v.0 + v.1 * v.1).sqrt();
361 if len < 1e-9 {
362 (1.0, 0.0)
363 } else {
364 (v.0 / len, v.1 / len)
365 }
366}
367
368fn draw_arrow(out: &mut String, apex: (f64, f64), dir: (f64, f64), len: f64, color: &str) {
369 let half_w = len * 0.42;
370 let base = (apex.0 - dir.0 * len, apex.1 - dir.1 * len);
371 let perp = (-dir.1, dir.0);
372 let p1 = (base.0 + perp.0 * half_w, base.1 + perp.1 * half_w);
373 let p2 = (base.0 - perp.0 * half_w, base.1 - perp.1 * half_w);
374 out.push_str(&format!(
375 r#"<path d="M {} {} L {} {} L {} {} Z" fill="{color}"/>"#,
376 fmt_f(apex.0),
377 fmt_f(apex.1),
378 fmt_f(p1.0),
379 fmt_f(p1.1),
380 fmt_f(p2.0),
381 fmt_f(p2.1),
382 ));
383 out.push('\n');
384}
385
386fn draw_ports(out: &mut String, doc: &Document, g: &Geometry, p: &Palette) {
387 const PORT_LABEL_SIZE: f64 = 9.5;
388 for (&(eid, pi), &(x, y)) in &g.ports {
389 let el = doc.el(ElementId(eid));
390 let style = doc.resolved_style(el.id);
391 let stroke = p.resolve(style.color.as_ref(), p.node_border);
392 out.push_str(&format!(
393 r#"<circle cx="{}" cy="{}" r="3.2" fill="{}" stroke="{}" stroke-width="1.2"/>"#,
394 fmt_f(x),
395 fmt_f(y),
396 p.surface,
397 stroke
398 ));
399 out.push('\n');
400 let port = &el.ports[pi];
403 if let Some(text) = &port.label {
404 use drawlang_core::model::Side;
405 let (lx, ly, anchor) = match port.side {
406 Side::Top => (x + 6.0, y - 7.0, ""),
407 Side::Bottom => (x + 6.0, y + 7.0 + PORT_LABEL_SIZE * 0.8, ""),
408 Side::Left => (x - 7.0, y - 6.0, r#" text-anchor="end""#),
409 Side::Right => (x + 7.0, y - 6.0, ""),
410 };
411 out.push_str(&format!(
412 r#"<text x="{}" y="{}" font-size="{}"{} fill="{}">{}</text>"#,
413 fmt_f(lx),
414 fmt_f(ly),
415 fmt_f(PORT_LABEL_SIZE),
416 anchor,
417 p.muted,
418 esc(text)
419 ));
420 out.push('\n');
421 }
422 }
423}