1use eframe::egui::{
19 Color32, Mesh, Pos2, Rect, Shape, Stroke, Ui, UiBuilder, epaint::Vertex, pos2, vec2,
20};
21use odox_core::draw::Geometry;
22use odox_core::{
23 Color, Document, Element, Family, Fill, Gradient, GradientStyle, Length, Ns, Properties,
24};
25
26use crate::flow::{Flow, Pictures};
27use crate::format::{self, Palette};
28
29pub struct Canvas<'a> {
31 pub document: &'a Document,
33 pub pictures: &'a mut Pictures,
35 pub page: Rect,
37 pub scale: f32,
39 pub palette: Palette,
41}
42
43impl Canvas<'_> {
44 pub fn background(&mut self, ui: &Ui, fill: &Fill) {
46 let page = self.page;
47 self.fill(
48 ui,
49 page,
50 fill,
51 None,
52 &[
53 page.left_top(),
54 page.right_top(),
55 page.right_bottom(),
56 page.left_bottom(),
57 ],
58 );
59 }
60
61 pub fn shape(&mut self, ui: &mut Ui, shape: &Element) {
69 if shape.is(&Ns::Draw, "g") {
70 for child in shape.elements() {
71 self.shape(ui, child);
72 }
73 return;
74 }
75
76 let properties = self.style_of(shape);
77 let outline = self.stroke(&properties);
78
79 if shape.is(&Ns::Draw, "line") {
82 let ends = [self.point(shape, "x1", "y1"), self.point(shape, "x2", "y2")];
83 if let ([Some(from), Some(to)], Some(stroke)) = ([ends[0], ends[1]], outline) {
84 self.painter(ui)
85 .add(Shape::line_segment([from, to], stroke));
86 }
87 return;
88 }
89
90 let Some(rect) = self.rect(shape) else { return };
91
92 if shape.is(&Ns::Draw, "polygon") || shape.is(&Ns::Draw, "polyline") {
93 let points = points(shape, rect);
94 if points.len() >= 2 {
95 if shape.is(&Ns::Draw, "polygon") {
96 self.fill(
97 ui,
98 rect,
99 &properties.graphic.fill(),
100 properties.graphic.opacity,
101 &points,
102 );
103 if let Some(stroke) = outline {
104 self.painter(ui).add(Shape::closed_line(points, stroke));
105 }
106 } else if let Some(stroke) = outline {
107 self.painter(ui).add(Shape::line(points, stroke));
108 }
109 }
110 return;
111 }
112
113 if shape.is(&Ns::Draw, "ellipse") || shape.is(&Ns::Draw, "circle") {
114 let (centre, radius) = (rect.center(), rect.size() / 2.0);
115 if let Some(colour) = self.flat(&properties.graphic.fill(), properties.graphic.opacity)
116 {
117 self.painter(ui)
118 .add(Shape::ellipse_filled(centre, radius, colour));
119 }
120 if let Some(stroke) = outline {
121 self.painter(ui)
122 .add(Shape::ellipse_stroke(centre, radius, stroke));
123 }
124 return;
125 }
126
127 if shape.is(&Ns::Draw, "custom-shape") {
128 if let Some(geometry) = shape
131 .child(&Ns::Draw, "enhanced-geometry")
132 .and_then(Geometry::read)
133 {
134 self.geometry(ui, &geometry, rect, &properties, outline);
135 }
136 self.text(ui, shape, rect);
137 return;
138 }
139
140 if !shape.is(&Ns::Draw, "rect") && !shape.is(&Ns::Draw, "frame") {
141 return;
142 }
143
144 let corners = [
145 rect.left_top(),
146 rect.right_top(),
147 rect.right_bottom(),
148 rect.left_bottom(),
149 ];
150 self.fill(
151 ui,
152 rect,
153 &properties.graphic.fill(),
154 properties.graphic.opacity,
155 &corners,
156 );
157 if let Some(stroke) = outline {
158 self.painter(ui)
159 .add(Shape::closed_line(corners.to_vec(), stroke));
160 }
161 self.text(ui, shape, rect);
162 }
163
164 fn geometry(
167 &mut self,
168 ui: &Ui,
169 geometry: &Geometry,
170 rect: Rect,
171 properties: &Properties,
172 outline: Option<Stroke>,
173 ) {
174 let view = geometry.view;
175 let place = |(x, y): (f32, f32)| {
176 pos2(
177 rect.left() + (x - view.x) / view.width * rect.width(),
178 rect.top() + (y - view.y) / view.height * rect.height(),
179 )
180 };
181 for stroke in &geometry.paths {
182 let points: Vec<Pos2> = stroke.points.iter().copied().map(place).collect();
183 if points.len() < 2 {
184 continue;
185 }
186 if stroke.fill {
187 self.fill(
188 ui,
189 rect,
190 &properties.graphic.fill(),
191 properties.graphic.opacity,
192 &points,
193 );
194 }
195 if stroke.stroke
196 && let Some(pen) = outline
197 {
198 let shape = if stroke.closed {
199 Shape::closed_line(points, pen)
200 } else {
201 Shape::line(points, pen)
202 };
203 self.painter(ui).add(shape);
204 }
205 }
206 }
207
208 fn text(&mut self, ui: &mut Ui, shape: &Element, rect: Rect) {
210 let picture = shape.child(&Ns::Draw, "image").is_some();
215 let content = if picture {
216 shape.clone()
217 } else {
218 match shape.child(&Ns::Draw, "text-box") {
219 Some(box_) => box_.clone(),
220 None => return,
221 }
222 };
223 let (page, scale, palette) = (self.page, self.scale, self.palette);
224 let document = self.document;
225 let pictures = &mut *self.pictures;
226 ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
227 ui.set_clip_rect(rect.intersect(page));
228 let mut flow = Flow::new(document, pictures, scale);
229 flow.palette = palette;
230 if picture {
231 flow.frame(ui, &content, rect.width());
232 } else {
233 flow.blocks(ui, &content, rect.width());
234 }
235 });
236 }
237
238 fn style_of(&self, shape: &Element) -> std::rc::Rc<Properties> {
245 if let Some(name) = shape.attr(&Ns::Presentation, "style-name") {
246 return self.document.styles.resolve(&Family::Presentation, name);
247 }
248 let name = shape.attr(&Ns::Draw, "style-name").unwrap_or_default();
249 self.document.styles.resolve(&Family::Graphic, name)
250 }
251
252 fn painter(&self, ui: &Ui) -> eframe::egui::Painter {
255 ui.painter()
256 .with_clip_rect(self.page.intersect(ui.clip_rect()))
257 }
258
259 fn rect(&self, shape: &Element) -> Option<Rect> {
261 let at = |local: &str| shape.attr(&Ns::Svg, local).and_then(Length::parse);
262 let (x, y) = (at("x")?, at("y")?);
263 let width = at("width").map_or(0.0, |w| w.points() * self.scale);
264 let height = at("height").map_or(0.0, |h| h.points() * self.scale);
265 Some(Rect::from_min_size(
266 pos2(
267 self.page.left() + x.points() * self.scale,
268 self.page.top() + y.points() * self.scale,
269 ),
270 vec2(width, height),
271 ))
272 }
273
274 fn point(&self, shape: &Element, x: &str, y: &str) -> Option<Pos2> {
275 let at = |local: &str| shape.attr(&Ns::Svg, local).and_then(Length::parse);
276 Some(pos2(
277 self.page.left() + at(x)?.points() * self.scale,
278 self.page.top() + at(y)?.points() * self.scale,
279 ))
280 }
281
282 fn stroke(&self, properties: &Properties) -> Option<Stroke> {
283 let colour = properties.graphic.stroke?;
284 let width = properties
285 .graphic
286 .stroke_width
287 .map_or(1.0, |w| w.points() * self.scale)
288 .max(1.0);
289 Some(Stroke::new(width, format::color32(colour)))
290 }
291
292 fn fill(&mut self, ui: &Ui, rect: Rect, fill: &Fill, opacity: Option<f32>, points: &[Pos2]) {
302 let document = self.document;
305
306 if let Fill::Image(name) = fill {
307 let Some(href) = document.styles.fill_image(name) else {
308 return;
309 };
310 let Some(texture) = self
311 .pictures
312 .get(ui.ctx(), document, href)
313 .map(eframe::egui::TextureHandle::id)
314 else {
315 return;
316 };
317 let tint = alpha(Color32::WHITE, opacity);
320 let mut mesh = Mesh::with_texture(texture);
321 for point in points {
322 mesh.vertices.push(Vertex {
323 pos: *point,
324 uv: pos2(
325 (point.x - rect.left()) / rect.width().max(f32::EPSILON),
326 (point.y - rect.top()) / rect.height().max(f32::EPSILON),
327 ),
328 color: tint,
329 });
330 }
331 for [a, b, c] in triangulate(points) {
332 mesh.add_triangle(a, b, c);
333 }
334 self.painter(ui).add(Shape::mesh(mesh));
335 return;
336 }
337
338 let gradient = match fill {
339 Fill::None | Fill::Image(_) => return,
340 Fill::Solid(_) => None,
341 Fill::Gradient(name) => match document.styles.gradient(name) {
342 Some(gradient) => Some(gradient),
343 None => return,
344 },
345 };
346 let flat = match fill {
347 Fill::Solid(colour) => Some(alpha(format::color32(*colour), opacity)),
348 _ => None,
349 };
350
351 let mut mesh = Mesh::default();
352 for point in points {
353 let colour = flat.unwrap_or_else(|| {
354 gradient.map_or(Color32::TRANSPARENT, |gradient| {
355 alpha(gradient_colour(*point, rect, gradient), opacity)
356 })
357 });
358 mesh.colored_vertex(*point, colour);
359 }
360 for [a, b, c] in triangulate(points) {
361 mesh.add_triangle(a, b, c);
362 }
363 self.painter(ui).add(Shape::mesh(mesh));
364 }
365
366 fn flat(&self, fill: &Fill, opacity: Option<f32>) -> Option<Color32> {
368 match fill {
369 Fill::None | Fill::Image(_) => None,
372 Fill::Solid(colour) => Some(alpha(format::color32(*colour), opacity)),
373 Fill::Gradient(name) => {
374 let gradient = self.document.styles.gradient(name)?;
375 Some(alpha(blend(gradient.start, gradient.end, 0.5), opacity))
376 }
377 }
378 }
379}
380
381fn points(shape: &Element, rect: Rect) -> Vec<Pos2> {
389 let view: Vec<f32> = shape
390 .attr(&Ns::Svg, "viewBox")
391 .unwrap_or_default()
392 .split_whitespace()
393 .filter_map(|n| n.parse().ok())
394 .collect();
395 let [left, top, width, height] = view[..] else {
396 return Vec::new();
397 };
398 if width <= 0.0 || height <= 0.0 {
399 return Vec::new();
400 }
401 shape
402 .attr(&Ns::Draw, "points")
403 .unwrap_or_default()
404 .split_whitespace()
405 .filter_map(|pair| {
406 let (x, y) = pair.split_once(',')?;
407 let x: f32 = x.trim().parse().ok()?;
408 let y: f32 = y.trim().parse().ok()?;
409 Some(pos2(
410 rect.left() + (x - left) / width * rect.width(),
411 rect.top() + (y - top) / height * rect.height(),
412 ))
413 })
414 .collect()
415}
416
417fn alpha(colour: Color32, opacity: Option<f32>) -> Color32 {
418 match opacity {
419 Some(opacity) if opacity < 1.0 => colour.gamma_multiply(opacity),
420 _ => colour,
421 }
422}
423
424fn blend(from: Color, to: Color, t: f32) -> Color32 {
425 let mix = |a: u8, b: u8| {
426 let a = f32::from(a);
427 let b = f32::from(b);
428 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
429 {
430 (a + (b - a) * t).round().clamp(0.0, 255.0) as u8
431 }
432 };
433 Color32::from_rgb(mix(from.r, to.r), mix(from.g, to.g), mix(from.b, to.b))
434}
435
436fn gradient_colour(point: Pos2, rect: Rect, gradient: &Gradient) -> Color32 {
444 match gradient.style {
445 GradientStyle::Linear | GradientStyle::Axial => {}
446 _ => return blend(gradient.start, gradient.end, 0.5),
447 }
448
449 let radians = gradient.angle.to_radians();
454 let axis = vec2(radians.sin(), -radians.cos());
455 let corners = [
456 rect.left_top(),
457 rect.right_top(),
458 rect.right_bottom(),
459 rect.left_bottom(),
460 ];
461 let projections = corners.map(|corner| (corner - rect.center()).dot(axis));
462 let low = projections.iter().copied().fold(f32::MAX, f32::min);
463 let high = projections.iter().copied().fold(f32::MIN, f32::max);
464 let span = (high - low).max(f32::EPSILON);
465
466 let mut t = ((point - rect.center()).dot(axis) - low) / span;
467 let border = gradient.border.clamp(0.0, 0.99);
470 t = ((t - border) / (1.0 - border)).clamp(0.0, 1.0);
471 if gradient.style == GradientStyle::Axial {
474 t = (t - 0.5).abs() * 2.0;
475 }
476 blend(gradient.start, gradient.end, t)
477}
478
479fn triangulate(points: &[Pos2]) -> Vec<[u32; 3]> {
487 let count = points.len();
488 if count < 3 {
489 return Vec::new();
490 }
491 let fan = || -> Vec<[u32; 3]> {
492 (1..count - 1)
493 .map(|i| {
494 [
495 0,
496 u32::try_from(i).unwrap_or(0),
497 u32::try_from(i + 1).unwrap_or(0),
498 ]
499 })
500 .collect()
501 };
502
503 let area: f32 = (0..count)
505 .map(|i| {
506 let (a, b) = (points[i], points[(i + 1) % count]);
507 a.x * b.y - b.x * a.y
508 })
509 .sum();
510 let winding = if area >= 0.0 { 1.0 } else { -1.0 };
511
512 let cross = |a: Pos2, b: Pos2, c: Pos2| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
513 let inside = |a: Pos2, b: Pos2, c: Pos2, p: Pos2| {
514 cross(a, b, p) * winding >= 0.0
515 && cross(b, c, p) * winding >= 0.0
516 && cross(c, a, p) * winding >= 0.0
517 };
518
519 let mut remaining: Vec<usize> = (0..count).collect();
520 let mut triangles = Vec::with_capacity(count);
521 let mut stuck = 0;
522 while remaining.len() > 3 {
523 if stuck > remaining.len() {
524 return fan();
525 }
526 let mut clipped = false;
527 for position in 0..remaining.len() {
528 let corner = [
529 remaining[(position + remaining.len() - 1) % remaining.len()],
530 remaining[position],
531 remaining[(position + 1) % remaining.len()],
532 ];
533 let ear = corner.map(|index| points[index]);
534 if cross(ear[0], ear[1], ear[2]) * winding <= 0.0 {
537 continue;
538 }
539 if remaining
540 .iter()
541 .filter(|other| !corner.contains(other))
542 .any(|other| inside(ear[0], ear[1], ear[2], points[*other]))
543 {
544 continue;
545 }
546 triangles.push(corner.map(|index| u32::try_from(index).unwrap_or(0)));
547 remaining.remove(position);
548 clipped = true;
549 stuck = 0;
550 break;
551 }
552 if !clipped {
553 stuck += 1;
554 }
555 }
556 if remaining.len() == 3 {
557 triangles.push([
558 u32::try_from(remaining[0]).unwrap_or(0),
559 u32::try_from(remaining[1]).unwrap_or(0),
560 u32::try_from(remaining[2]).unwrap_or(0),
561 ]);
562 }
563 triangles
564}
565
566#[cfg(test)]
567mod tests {
568 use super::triangulate;
569 use eframe::egui::{Pos2, pos2};
570
571 fn areas(points: &[Pos2]) -> (f32, f32) {
574 let cross =
575 |a: Pos2, b: Pos2, c: Pos2| (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
576 let triangles: f32 = triangulate(points)
577 .iter()
578 .map(|[a, b, c]| {
579 cross(
580 points[*a as usize],
581 points[*b as usize],
582 points[*c as usize],
583 )
584 .abs()
585 })
586 .sum();
587 let outline: f32 = (0..points.len())
588 .map(|i| {
589 let (a, b) = (points[i], points[(i + 1) % points.len()]);
590 a.x * b.y - b.x * a.y
591 })
592 .sum::<f32>()
593 .abs();
594 (triangles, outline)
595 }
596
597 #[test]
600 fn a_concave_outline_is_cut_into_the_shape_and_not_its_hull() {
601 let l = [
602 pos2(0.0, 0.0),
603 pos2(2.0, 0.0),
604 pos2(2.0, 1.0),
605 pos2(1.0, 1.0),
606 pos2(1.0, 2.0),
607 pos2(0.0, 2.0),
608 ];
609 let (triangles, outline) = areas(&l);
610 assert!(
611 (triangles - outline).abs() < 1e-3,
612 "{triangles} against {outline}"
613 );
614 assert!((outline - 6.0).abs() < 1e-3, "{outline}");
616 }
617
618 #[test]
620 fn a_cross_is_cut_correctly_too() {
621 let cross = [
622 pos2(1.0, 0.0),
623 pos2(2.0, 0.0),
624 pos2(2.0, 1.0),
625 pos2(3.0, 1.0),
626 pos2(3.0, 2.0),
627 pos2(2.0, 2.0),
628 pos2(2.0, 3.0),
629 pos2(1.0, 3.0),
630 pos2(1.0, 2.0),
631 pos2(0.0, 2.0),
632 pos2(0.0, 1.0),
633 pos2(1.0, 1.0),
634 ];
635 let (triangles, outline) = areas(&cross);
636 assert!(
637 (triangles - outline).abs() < 1e-3,
638 "{triangles} against {outline}"
639 );
640 }
641
642 #[test]
645 fn winding_does_not_matter() {
646 let mut l = vec![
647 pos2(0.0, 0.0),
648 pos2(2.0, 0.0),
649 pos2(2.0, 1.0),
650 pos2(1.0, 1.0),
651 pos2(1.0, 2.0),
652 pos2(0.0, 2.0),
653 ];
654 l.reverse();
655 let (triangles, outline) = areas(&l);
656 assert!(
657 (triangles - outline).abs() < 1e-3,
658 "{triangles} against {outline}"
659 );
660 }
661
662 #[test]
664 fn a_square_is_two_triangles() {
665 let square = [
666 pos2(0.0, 0.0),
667 pos2(1.0, 0.0),
668 pos2(1.0, 1.0),
669 pos2(0.0, 1.0),
670 ];
671 assert_eq!(triangulate(&square).len(), 2);
672 let (triangles, outline) = areas(&square);
673 assert!((triangles - outline).abs() < 1e-4);
674 }
675}