1use crate::renderer::{RenderContext, Renderer, ShapeRenderer};
4use crate::text_editor::TextEditState;
5use kurbo::{Affine, BezPath, PathEl, Point, Rect, Shape as KurboShape, Stroke, Vec2};
6use parley::layout::PositionedLayoutItem;
7use parley::{FontContext, LayoutContext};
8use peniko::{Brush, Color, Fill};
9use drafftink_core::selection::{get_handles, Handle, HandleKind};
10use drafftink_core::shapes::{Shape, ShapeStyle, ShapeTrait};
11use drafftink_core::snap::{SnapTarget, SnapTargetKind};
12use vello::Scene;
13
14#[derive(Debug)]
16pub struct PngRenderResult {
17 pub rgba_data: Vec<u8>,
19 pub width: u32,
21 pub height: u32,
23}
24
25static GELPEN_REGULAR: &[u8] = include_bytes!("../assets/GelPen.ttf");
27static GELPEN_LIGHT: &[u8] = include_bytes!("../assets/GelPenLight.ttf");
28static GELPEN_HEAVY: &[u8] = include_bytes!("../assets/GelPenHeavy.ttf");
29static ROBOTO_LIGHT: &[u8] = include_bytes!("../assets/Roboto-Light.ttf");
31static ROBOTO_REGULAR: &[u8] = include_bytes!("../assets/Roboto-Regular.ttf");
32static ROBOTO_BOLD: &[u8] = include_bytes!("../assets/Roboto-Bold.ttf");
33static ARCHITECTS_DAUGHTER: &[u8] = include_bytes!("../assets/ArchitectsDaughter.ttf");
35
36pub struct VelloRenderer {
38 scene: Scene,
40 selection_color: Color,
42 font_cx: FontContext,
44 layout_cx: LayoutContext<Brush>,
46 zoom: f64,
48 image_cache: std::collections::HashMap<String, peniko::ImageData>,
51}
52
53impl Default for VelloRenderer {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59fn convert_rect(rect: &parley::BoundingBox) -> Rect {
61 Rect::new(rect.x0, rect.y0, rect.x1, rect.y1)
62}
63
64struct SimpleRng {
67 state: u32,
68}
69
70impl SimpleRng {
71 fn new(seed: u32) -> Self {
72 Self { state: seed.max(1) }
73 }
74
75 fn next_u32(&mut self) -> u32 {
76 let mut x = self.state;
77 x ^= x << 13;
78 x ^= x >> 17;
79 x ^= x << 5;
80 self.state = x;
81 x
82 }
83
84 fn next_f64(&mut self) -> f64 {
86 (self.next_u32() as f64 / u32::MAX as f64) * 2.0 - 1.0
87 }
88
89 fn offset(&mut self, amount: f64) -> f64 {
91 self.next_f64() * amount
92 }
93}
94
95fn apply_hand_drawn_effect(path: &BezPath, roughness: f64, zoom: f64, seed: u32, stroke_index: u32) -> BezPath {
105 if roughness <= 0.0 {
106 return path.clone();
107 }
108
109 let scale = 1.0 / zoom.sqrt();
111
112 let max_randomness_offset = roughness * 2.0 * scale;
115 let bowing = roughness * 1.0;
116
117 let combined_seed = seed.wrapping_add(stroke_index.wrapping_mul(99991)); let mut rng = SimpleRng::new(combined_seed);
121
122 let mut result = BezPath::new();
123 let mut last_point = Point::ZERO;
124
125 for el in path.elements() {
126 match el {
127 PathEl::MoveTo(p) => {
128 let wobbled = Point::new(
130 p.x + rng.offset(max_randomness_offset),
131 p.y + rng.offset(max_randomness_offset),
132 );
133 result.move_to(wobbled);
134 last_point = *p;
135 }
136 PathEl::LineTo(p) => {
137 let dx = p.x - last_point.x;
143 let dy = p.y - last_point.y;
144 let len = (dx * dx + dy * dy).sqrt();
145
146 let bow_offset = bowing * roughness * len / 200.0;
148 let bow = rng.offset(bow_offset) * scale;
149
150 let (perp_x, perp_y) = if len > 0.001 {
152 (-dy / len, dx / len)
153 } else {
154 (0.0, 0.0)
155 };
156
157 let mid_x = (last_point.x + p.x) / 2.0 + perp_x * bow;
159 let mid_y = (last_point.y + p.y) / 2.0 + perp_y * bow;
160
161 let end = Point::new(
163 p.x + rng.offset(max_randomness_offset),
164 p.y + rng.offset(max_randomness_offset),
165 );
166
167 result.quad_to(Point::new(mid_x, mid_y), end);
169 last_point = *p;
170 }
171 PathEl::QuadTo(p1, p2) => {
172 let wobbled_p1 = Point::new(
173 p1.x + rng.offset(max_randomness_offset * 0.7),
174 p1.y + rng.offset(max_randomness_offset * 0.7),
175 );
176 let wobbled_p2 = Point::new(
177 p2.x + rng.offset(max_randomness_offset),
178 p2.y + rng.offset(max_randomness_offset),
179 );
180 result.quad_to(wobbled_p1, wobbled_p2);
181 last_point = *p2;
182 }
183 PathEl::CurveTo(p1, p2, p3) => {
184 let wobbled_p1 = Point::new(
185 p1.x + rng.offset(max_randomness_offset * 0.5),
186 p1.y + rng.offset(max_randomness_offset * 0.5),
187 );
188 let wobbled_p2 = Point::new(
189 p2.x + rng.offset(max_randomness_offset * 0.5),
190 p2.y + rng.offset(max_randomness_offset * 0.5),
191 );
192 let wobbled_p3 = Point::new(
193 p3.x + rng.offset(max_randomness_offset),
194 p3.y + rng.offset(max_randomness_offset),
195 );
196 result.curve_to(wobbled_p1, wobbled_p2, wobbled_p3);
197 last_point = *p3;
198 }
199 PathEl::ClosePath => {
200 result.close_path();
203 }
204 }
205 }
206
207 result
208}
209
210impl VelloRenderer {
211 pub fn new() -> Self {
213 let mut font_cx = FontContext::new();
214 font_cx.collection.register_fonts(
216 vello::peniko::Blob::new(std::sync::Arc::new(GELPEN_REGULAR)),
217 None,
218 );
219 font_cx.collection.register_fonts(
220 vello::peniko::Blob::new(std::sync::Arc::new(GELPEN_LIGHT)),
221 None,
222 );
223 font_cx.collection.register_fonts(
224 vello::peniko::Blob::new(std::sync::Arc::new(GELPEN_HEAVY)),
225 None,
226 );
227 font_cx.collection.register_fonts(
228 vello::peniko::Blob::new(std::sync::Arc::new(ROBOTO_LIGHT)),
229 None,
230 );
231 font_cx.collection.register_fonts(
232 vello::peniko::Blob::new(std::sync::Arc::new(ROBOTO_REGULAR)),
233 None,
234 );
235 font_cx.collection.register_fonts(
236 vello::peniko::Blob::new(std::sync::Arc::new(ROBOTO_BOLD)),
237 None,
238 );
239 font_cx.collection.register_fonts(
240 vello::peniko::Blob::new(std::sync::Arc::new(ARCHITECTS_DAUGHTER)),
241 None,
242 );
243
244 Self {
245 scene: Scene::new(),
246 selection_color: Color::from_rgba8(59, 130, 246, 255),
247 font_cx,
248 layout_cx: LayoutContext::new(),
249 zoom: 1.0,
250 image_cache: std::collections::HashMap::new(),
251 }
252 }
253
254 pub fn scene(&self) -> &Scene {
256 &self.scene
257 }
258
259 pub fn take_scene(&mut self) -> Scene {
261 std::mem::take(&mut self.scene)
262 }
263
264 pub fn contexts_mut(&mut self) -> (&mut FontContext, &mut LayoutContext<Brush>) {
266 (&mut self.font_cx, &mut self.layout_cx)
267 }
268
269 pub fn build_export_scene(&mut self, document: &drafftink_core::canvas::CanvasDocument, scale: f64) -> (Scene, Option<Rect>) {
274 self.scene.reset();
275 self.zoom = scale;
276
277 let bounds = document.bounds();
278
279 if bounds.is_none() {
281 return (std::mem::take(&mut self.scene), None);
282 }
283
284 let bounds = bounds.unwrap();
285
286 let padding = 20.0;
288 let padded_bounds = bounds.inflate(padding, padding);
289
290 let transform = Affine::scale(scale)
292 * Affine::translate((-padded_bounds.x0, -padded_bounds.y0));
293
294 let scaled_width = padded_bounds.width() * scale;
296 let scaled_height = padded_bounds.height() * scale;
297
298 let bg_rect = Rect::new(0.0, 0.0, scaled_width, scaled_height);
300 self.scene.fill(
301 Fill::NonZero,
302 Affine::IDENTITY,
303 Color::WHITE,
304 None,
305 &bg_rect,
306 );
307
308 for shape in document.shapes_ordered() {
310 self.render_shape(shape, transform, false);
311 }
312
313 let scaled_bounds = Rect::new(0.0, 0.0, scaled_width, scaled_height);
315 (std::mem::take(&mut self.scene), Some(scaled_bounds))
316 }
317
318 pub fn build_export_scene_selection(
322 &mut self,
323 document: &drafftink_core::canvas::CanvasDocument,
324 selection: &[drafftink_core::shapes::ShapeId],
325 scale: f64,
326 ) -> (Scene, Option<Rect>) {
327 self.scene.reset();
328 self.zoom = scale;
329
330 if selection.is_empty() {
331 return (std::mem::take(&mut self.scene), None);
332 }
333
334 let mut min_x = f64::MAX;
336 let mut min_y = f64::MAX;
337 let mut max_x = f64::MIN;
338 let mut max_y = f64::MIN;
339
340 let mut shapes_to_render = Vec::new();
341 for &shape_id in selection {
342 if let Some(shape) = document.get_shape(shape_id) {
343 let b = shape.bounds();
344 min_x = min_x.min(b.x0);
345 min_y = min_y.min(b.y0);
346 max_x = max_x.max(b.x1);
347 max_y = max_y.max(b.y1);
348 shapes_to_render.push(shape);
349 }
350 }
351
352 if shapes_to_render.is_empty() {
353 return (std::mem::take(&mut self.scene), None);
354 }
355
356 let bounds = Rect::new(min_x, min_y, max_x, max_y);
357
358 let padding = 20.0;
360 let padded_bounds = bounds.inflate(padding, padding);
361
362 let transform = Affine::scale(scale)
364 * Affine::translate((-padded_bounds.x0, -padded_bounds.y0));
365
366 let scaled_width = padded_bounds.width() * scale;
368 let scaled_height = padded_bounds.height() * scale;
369
370 let bg_rect = Rect::new(0.0, 0.0, scaled_width, scaled_height);
372 self.scene.fill(
373 Fill::NonZero,
374 Affine::IDENTITY,
375 Color::WHITE,
376 None,
377 &bg_rect,
378 );
379
380 for shape in shapes_to_render {
382 self.render_shape(shape, transform, false);
383 }
384
385 let scaled_bounds = Rect::new(0.0, 0.0, scaled_width, scaled_height);
387 (std::mem::take(&mut self.scene), Some(scaled_bounds))
388 }
389
390 fn render_path(&mut self, path: &BezPath, style: &ShapeStyle, transform: Affine) {
392 let roughness = style.sloppiness.roughness();
393 let seed = style.seed;
394
395 if let Some(fill_color) = style.fill() {
397 let fill_path = if roughness > 0.0 {
398 apply_hand_drawn_effect(path, roughness * 0.3, self.zoom, seed, 0)
399 } else {
400 path.clone()
401 };
402 self.scene.fill(
403 Fill::NonZero,
404 transform,
405 fill_color,
406 None,
407 &fill_path,
408 );
409 }
410
411 if roughness > 0.0 {
413 let stroke = Stroke::new(style.stroke_width);
414
415 let path1 = apply_hand_drawn_effect(path, roughness, self.zoom, seed, 0);
417 self.scene.stroke(
418 &stroke,
419 transform,
420 style.stroke(),
421 None,
422 &path1,
423 );
424
425 let path2 = apply_hand_drawn_effect(path, roughness, self.zoom, seed, 1);
427 self.scene.stroke(
428 &stroke,
429 transform,
430 style.stroke(),
431 None,
432 &path2,
433 );
434 } else {
435 let stroke = Stroke::new(style.stroke_width);
437 self.scene.stroke(
438 &stroke,
439 transform,
440 style.stroke(),
441 None,
442 path,
443 );
444 }
445 }
446
447 fn render_text(&mut self, text: &drafftink_core::shapes::Text, transform: Affine) {
449 use parley::layout::PositionedLayoutItem;
450 use parley::StyleProperty;
451
452 if text.content.is_empty() {
454 let cursor_height = text.font_size * 1.2;
456 let cursor = kurbo::Line::new(
457 Point::new(text.position.x, text.position.y),
458 Point::new(text.position.x, text.position.y + cursor_height),
459 );
460 let stroke = Stroke::new(2.0);
461 self.scene.stroke(&stroke, transform, Color::from_rgba8(100, 100, 100, 200), None, &cursor);
462 return;
463 }
464
465 use drafftink_core::shapes::{FontFamily, FontWeight};
466
467 let style = &text.style;
468 let brush = Brush::Solid(style.stroke());
469 let font_size = text.font_size as f32;
470
471 let (font_name, parley_weight) = match (&text.font_family, &text.font_weight) {
480 (FontFamily::GelPen, FontWeight::Light) => ("GelPenLight", parley::FontWeight::NORMAL),
481 (FontFamily::GelPen, FontWeight::Regular) => ("GelPen", parley::FontWeight::NORMAL),
482 (FontFamily::GelPen, FontWeight::Heavy) => ("GelPenHeavy", parley::FontWeight::NORMAL),
483 (FontFamily::Roboto, FontWeight::Light) => ("Roboto", parley::FontWeight::LIGHT),
484 (FontFamily::Roboto, FontWeight::Regular) => ("Roboto", parley::FontWeight::NORMAL),
485 (FontFamily::Roboto, FontWeight::Heavy) => ("Roboto", parley::FontWeight::BOLD),
486 (FontFamily::ArchitectsDaughter, _) => ("Architects Daughter", parley::FontWeight::NORMAL),
487 };
488
489 let mut builder = self.layout_cx.ranged_builder(&mut self.font_cx, &text.content, 1.0, false);
491 builder.push_default(StyleProperty::FontSize(font_size));
492 builder.push_default(StyleProperty::Brush(brush.clone()));
493 builder.push_default(StyleProperty::FontWeight(parley_weight));
494 builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(
495 parley::FontFamily::Named(font_name.into())
496 )));
497 let mut layout = builder.build(&text.content);
498
499 layout.break_all_lines(None);
501 layout.align(None, parley::Alignment::Start, parley::AlignmentOptions::default());
502
503 let layout_width = layout.width() as f64;
505 let layout_height = layout.height() as f64;
506 text.set_cached_size(layout_width, layout_height);
507
508 let text_transform = transform * Affine::translate((text.position.x, text.position.y));
512
513 let mut glyph_count = 0;
515
516 for line in layout.lines() {
518 for item in line.items() {
519 let PositionedLayoutItem::GlyphRun(glyph_run) = item else {
520 continue;
521 };
522 let mut x = glyph_run.offset();
523 let y = glyph_run.baseline();
524 let run = glyph_run.run();
525 let font = run.font();
526 let font_size = run.font_size();
527 let synthesis = run.synthesis();
528 let glyph_xform = synthesis
529 .skew()
530 .map(|angle| Affine::skew(angle.to_radians().tan() as f64, 0.0));
531
532 let glyphs: Vec<vello::Glyph> = glyph_run.glyphs().map(|glyph| {
533 let gx = x + glyph.x;
534 let gy = y - glyph.y;
535 x += glyph.advance;
536 glyph_count += 1;
537 vello::Glyph {
538 id: glyph.id,
539 x: gx,
540 y: gy,
541 }
542 }).collect();
543
544 if !glyphs.is_empty() {
545 self.scene
546 .draw_glyphs(font)
547 .brush(&brush)
548 .hint(true)
549 .transform(text_transform)
550 .glyph_transform(glyph_xform)
551 .font_size(font_size)
552 .normalized_coords(run.normalized_coords())
553 .draw(Fill::NonZero, glyphs.into_iter());
554 }
555 }
556 }
557
558 if glyph_count == 0 {
560 let width = text.content.len() as f64 * text.font_size * 0.6;
562 let height = text.font_size * 1.2;
563 let rect = Rect::new(
564 text.position.x,
565 text.position.y,
566 text.position.x + width.max(20.0),
567 text.position.y + height,
568 );
569 self.scene.fill(Fill::NonZero, transform, Color::from_rgba8(255, 100, 100, 100), None, &rect);
570 }
571 }
572
573 fn render_image(&mut self, image: &drafftink_core::shapes::Image, transform: Affine) {
575 use std::sync::Arc;
576
577 let id_str = image.id().to_string();
578
579 let image_data = if let Some(cached) = self.image_cache.get(&id_str) {
581 cached.clone()
582 } else {
583 if let Some(raw_data) = image.data() {
585 if let Ok(decoded) = ::image::load_from_memory(&raw_data) {
587 let rgba = decoded.to_rgba8();
588 let (width, height) = rgba.dimensions();
589 let blob = peniko::Blob::new(Arc::new(rgba.into_vec()));
590 let img_data = peniko::ImageData {
591 data: blob,
592 format: peniko::ImageFormat::Rgba8,
593 width,
594 height,
595 alpha_type: peniko::ImageAlphaType::Alpha,
596 };
597 self.image_cache.insert(id_str.clone(), img_data.clone());
598 img_data
599 } else {
600 self.render_image_placeholder(image, transform, "Failed to decode");
602 return;
603 }
604 } else {
605 self.render_image_placeholder(image, transform, "No image data");
607 return;
608 }
609 };
610
611 let bounds = image.bounds();
613 let scale_x = bounds.width() / image_data.width as f64;
614 let scale_y = bounds.height() / image_data.height as f64;
615
616 let image_transform = transform
617 * Affine::translate((bounds.x0, bounds.y0))
618 * Affine::scale_non_uniform(scale_x, scale_y);
619
620 self.scene.draw_image(&image_data.into(), image_transform);
621 }
622
623 fn render_image_placeholder(&mut self, image: &drafftink_core::shapes::Image, transform: Affine, _msg: &str) {
625 let bounds = image.bounds();
626
627 let rect_path = bounds.to_path(0.1);
629 self.scene.fill(Fill::NonZero, transform, Color::from_rgba8(200, 200, 200, 255), None, &rect_path);
630
631 let stroke = Stroke::new(2.0);
633 let mut x_path = BezPath::new();
634 x_path.move_to(Point::new(bounds.x0, bounds.y0));
635 x_path.line_to(Point::new(bounds.x1, bounds.y1));
636 x_path.move_to(Point::new(bounds.x1, bounds.y0));
637 x_path.line_to(Point::new(bounds.x0, bounds.y1));
638 self.scene.stroke(&stroke, transform, Color::from_rgba8(150, 150, 150, 255), None, &x_path);
639
640 self.scene.stroke(&stroke, transform, Color::from_rgba8(100, 100, 100, 255), None, &rect_path);
642 }
643
644 pub fn render_text_editing(
647 &mut self,
648 text: &drafftink_core::shapes::Text,
649 edit_state: &mut TextEditState,
650 transform: Affine,
651 ) {
652 use drafftink_core::shapes::{FontFamily as ShapeFontFamily, FontWeight};
653
654 let style = &text.style;
655 let brush = Brush::Solid(style.stroke());
656
657 let (font_name, parley_weight) = match (&text.font_family, &text.font_weight) {
660 (ShapeFontFamily::GelPen, FontWeight::Light) => ("GelPenLight", parley::FontWeight::NORMAL),
661 (ShapeFontFamily::GelPen, FontWeight::Regular) => ("GelPen", parley::FontWeight::NORMAL),
662 (ShapeFontFamily::GelPen, FontWeight::Heavy) => ("GelPenHeavy", parley::FontWeight::NORMAL),
663 (ShapeFontFamily::Roboto, FontWeight::Light) => ("Roboto", parley::FontWeight::LIGHT),
664 (ShapeFontFamily::Roboto, FontWeight::Regular) => ("Roboto", parley::FontWeight::NORMAL),
665 (ShapeFontFamily::Roboto, FontWeight::Heavy) => ("Roboto", parley::FontWeight::BOLD),
666 (ShapeFontFamily::ArchitectsDaughter, _) => ("Architects Daughter", parley::FontWeight::NORMAL),
667 };
668
669 edit_state.set_font_size(text.font_size as f32);
671 edit_state.set_brush(brush.clone());
672
673 {
675 use parley::{FontStack, FontFamily, StyleProperty};
676 let styles = edit_state.editor_mut().edit_styles();
677 styles.insert(StyleProperty::FontStack(FontStack::Single(
678 FontFamily::Named(font_name.into())
679 )));
680 styles.insert(StyleProperty::FontWeight(parley_weight));
681 }
682
683 let text_transform = transform * Affine::translate((text.position.x, text.position.y));
685
686 let layout = edit_state.editor_mut().layout(&mut self.font_cx, &mut self.layout_cx);
688
689 for line in layout.lines() {
691 for item in line.items() {
692 let PositionedLayoutItem::GlyphRun(glyph_run) = item else {
693 continue;
694 };
695 let glyph_style = glyph_run.style();
696 let mut x = glyph_run.offset();
697 let y = glyph_run.baseline();
698 let run = glyph_run.run();
699 let font = run.font();
700 let font_size = run.font_size();
701 let synthesis = run.synthesis();
702 let glyph_xform = synthesis
703 .skew()
704 .map(|angle| Affine::skew(angle.to_radians().tan() as f64, 0.0));
705
706 let glyphs: Vec<vello::Glyph> = glyph_run.glyphs().map(|glyph| {
707 let gx = x + glyph.x;
708 let gy = y - glyph.y;
709 x += glyph.advance;
710 vello::Glyph {
711 id: glyph.id,
712 x: gx,
713 y: gy,
714 }
715 }).collect();
716
717 if !glyphs.is_empty() {
718 self.scene
719 .draw_glyphs(font)
720 .brush(&glyph_style.brush)
721 .hint(true)
722 .transform(text_transform)
723 .glyph_transform(glyph_xform)
724 .font_size(font_size)
725 .normalized_coords(run.normalized_coords())
726 .draw(Fill::NonZero, glyphs.into_iter());
727 }
728 }
729 }
730
731 let selection_color = Color::from_rgba8(70, 130, 180, 128); edit_state.editor().selection_geometry_with(|rect, _| {
736 self.scene.fill(
737 Fill::NonZero,
738 text_transform,
739 selection_color,
740 None,
741 &convert_rect(&rect),
742 );
743 });
744
745 if edit_state.is_cursor_visible() {
747 if let Some(cursor) = edit_state.editor().cursor_geometry(1.5) {
748 let cursor_color = Color::from_rgba8(0, 0, 0, 255);
750 self.scene.fill(
751 Fill::NonZero,
752 text_transform,
753 cursor_color,
754 None,
755 &convert_rect(&cursor),
756 );
757 } else if edit_state.text().is_empty() {
758 let cursor_height = text.font_size * 1.2;
760 let cursor_rect = Rect::new(0.0, 0.0, 1.5, cursor_height);
761 self.scene.fill(
762 Fill::NonZero,
763 text_transform,
764 Color::from_rgba8(0, 0, 0, 255),
765 None,
766 &cursor_rect,
767 );
768 }
769 }
770 }
771
772 fn render_shape_handles(&mut self, shape: &Shape, transform: Affine) {
775 let handles = get_handles(shape);
776 let handle_size = 8.0 / self.zoom;
778 let stroke_width = 1.0 / self.zoom;
779 let dash_len = 4.0 / self.zoom;
780
781 match shape {
784 Shape::Line(_) | Shape::Arrow(_) => {
785 }
787 _ => {
788 let bounds = shape.bounds();
790 let stroke = Stroke::new(stroke_width).with_dashes(0.0, &[dash_len, dash_len]);
791 let mut path = BezPath::new();
792 path.move_to(Point::new(bounds.x0, bounds.y0));
793 path.line_to(Point::new(bounds.x1, bounds.y0));
794 path.line_to(Point::new(bounds.x1, bounds.y1));
795 path.line_to(Point::new(bounds.x0, bounds.y1));
796 path.close_path();
797
798 self.scene.stroke(
799 &stroke,
800 transform,
801 self.selection_color,
802 None,
803 &path,
804 );
805 }
806 }
807
808 for handle in handles {
810 self.render_handle(&handle, transform, handle_size);
811 }
812 }
813
814 fn render_handle(&mut self, handle: &Handle, transform: Affine, size: f64) {
817 let pos = handle.position;
818 let stroke_width_thick = 2.0 / self.zoom;
819 let stroke_width_thin = 1.5 / self.zoom;
820
821 match handle.kind {
823 HandleKind::Endpoint(_) => {
824 let radius = size / 2.0;
826 let ellipse = kurbo::Ellipse::new(pos, (radius, radius), 0.0);
827 let path = ellipse.to_path(0.1);
828
829 self.scene.fill(
831 Fill::NonZero,
832 transform,
833 Color::WHITE,
834 None,
835 &path,
836 );
837
838 self.scene.stroke(
840 &Stroke::new(stroke_width_thick),
841 transform,
842 self.selection_color,
843 None,
844 &path,
845 );
846 }
847 HandleKind::Corner(_) | HandleKind::Edge(_) => {
848 let half = size / 2.0;
850 let rect = Rect::new(
851 pos.x - half,
852 pos.y - half,
853 pos.x + half,
854 pos.y + half,
855 );
856 let path = rect.to_path(0.1);
857
858 self.scene.fill(
860 Fill::NonZero,
861 transform,
862 Color::WHITE,
863 None,
864 &path,
865 );
866
867 self.scene.stroke(
869 &Stroke::new(stroke_width_thin),
870 transform,
871 self.selection_color,
872 None,
873 &path,
874 );
875 }
876 }
877 }
878}
879
880impl Renderer for VelloRenderer {
881 fn build_scene(&mut self, ctx: &RenderContext) {
882 self.scene.reset();
884 self.selection_color = ctx.selection_color;
885 self.zoom = ctx.canvas.camera.zoom;
886
887 let camera_transform = ctx.canvas.camera.transform();
888
889 use crate::renderer::GridStyle;
891 match ctx.grid_style {
892 GridStyle::None => {}
893 GridStyle::Lines => {
894 self.render_grid_lines(
895 Rect::new(0.0, 0.0, ctx.viewport_size.width, ctx.viewport_size.height),
896 camera_transform,
897 20.0,
898 );
899 }
900 GridStyle::CrossPlus => {
901 self.render_grid_crosses(
902 Rect::new(0.0, 0.0, ctx.viewport_size.width, ctx.viewport_size.height),
903 camera_transform,
904 20.0,
905 );
906 }
907 GridStyle::Dots => {
908 self.render_grid_dots(
909 Rect::new(0.0, 0.0, ctx.viewport_size.width, ctx.viewport_size.height),
910 camera_transform,
911 20.0,
912 );
913 }
914 }
915
916 for shape in ctx.canvas.document.shapes_ordered() {
918 if ctx.editing_shape_id == Some(shape.id()) {
920 continue;
921 }
922 let is_selected = ctx.canvas.is_selected(shape.id());
923 self.render_shape(shape, camera_transform, is_selected);
924 }
925
926 if let Some(preview) = ctx.canvas.tool_manager.preview_shape() {
928 self.render_shape(&preview, camera_transform, false);
929 }
930
931 if let Some(rect) = ctx.selection_rect {
933 self.render_selection_rect(rect, camera_transform);
934 }
935
936 if !ctx.nearby_snap_targets.is_empty() {
938 self.render_snap_targets(&ctx.nearby_snap_targets, camera_transform);
939 }
940
941 if let Some(snap_point) = ctx.snap_point {
943 self.render_snap_guides(snap_point, camera_transform, ctx.viewport_size);
944 }
945
946 if let Some(ref angle_info) = ctx.angle_snap_info {
948 self.render_angle_snap_guides(angle_info, camera_transform, ctx.viewport_size);
949 }
950 }
951}
952
953impl VelloRenderer {
954 fn render_snap_guides(&mut self, snap_point: Point, transform: Affine, viewport_size: kurbo::Size) {
956 let guide_color = Color::from_rgba8(236, 72, 153, 180); let stroke_width = 1.0 / self.zoom;
961 let stroke = Stroke::new(stroke_width);
962
963 let inv_transform = transform.inverse();
966 let world_top_left = inv_transform * Point::new(0.0, 0.0);
967 let world_bottom_right = inv_transform * Point::new(viewport_size.width, viewport_size.height);
968
969 let mut h_path = BezPath::new();
971 h_path.move_to(Point::new(world_top_left.x, snap_point.y));
972 h_path.line_to(Point::new(world_bottom_right.x, snap_point.y));
973 self.scene.stroke(&stroke, transform, guide_color, None, &h_path);
974
975 let mut v_path = BezPath::new();
977 v_path.move_to(Point::new(snap_point.x, world_top_left.y));
978 v_path.line_to(Point::new(snap_point.x, world_bottom_right.y));
979 self.scene.stroke(&stroke, transform, guide_color, None, &v_path);
980
981 let circle_radius = 4.0 / self.zoom;
983 let circle = kurbo::Circle::new(snap_point, circle_radius);
984 self.scene.stroke(&Stroke::new(stroke_width * 2.0), transform, guide_color, None, &circle);
985 }
986
987 fn render_snap_targets(&mut self, targets: &[SnapTarget], transform: Affine) {
989 let corner_color = Color::from_rgba8(59, 130, 246, 150); let midpoint_color = Color::from_rgba8(16, 185, 129, 150); let center_color = Color::from_rgba8(245, 158, 11, 150); let size = 4.0 / self.zoom;
996 let stroke_width = 1.0 / self.zoom;
997
998 for target in targets {
999 let color = match target.kind {
1000 SnapTargetKind::Corner => corner_color,
1001 SnapTargetKind::Midpoint => midpoint_color,
1002 SnapTargetKind::Center => center_color,
1003 SnapTargetKind::Edge => corner_color, };
1005
1006 match target.kind {
1007 SnapTargetKind::Corner | SnapTargetKind::Edge => {
1008 let half = size;
1010 let rect = Rect::new(
1011 target.point.x - half,
1012 target.point.y - half,
1013 target.point.x + half,
1014 target.point.y + half,
1015 );
1016 self.scene.stroke(&Stroke::new(stroke_width), transform, color, None, &rect);
1017 }
1018 SnapTargetKind::Midpoint => {
1019 let mut path = BezPath::new();
1021 path.move_to(Point::new(target.point.x, target.point.y - size));
1022 path.line_to(Point::new(target.point.x + size, target.point.y));
1023 path.line_to(Point::new(target.point.x, target.point.y + size));
1024 path.line_to(Point::new(target.point.x - size, target.point.y));
1025 path.close_path();
1026 self.scene.stroke(&Stroke::new(stroke_width), transform, color, None, &path);
1027 }
1028 SnapTargetKind::Center => {
1029 let circle = kurbo::Circle::new(target.point, size);
1031 self.scene.stroke(&Stroke::new(stroke_width), transform, color, None, &circle);
1032 }
1033 }
1034 }
1035 }
1036
1037 fn render_angle_snap_guides(
1039 &mut self,
1040 info: &crate::renderer::AngleSnapInfo,
1041 transform: Affine,
1042 viewport_size: kurbo::Size,
1043 ) {
1044 use std::f64::consts::PI;
1045
1046 let ray_color = Color::from_rgba8(100, 100, 100, 60); let active_ray_color = Color::from_rgba8(236, 72, 153, 200); let arc_color = Color::from_rgba8(236, 72, 153, 220); let thin_stroke_width = 0.5 / self.zoom;
1053 let thick_stroke_width = 1.5 / self.zoom;
1054
1055 let start = info.start_point;
1056
1057 let inv_transform = transform.inverse();
1059 let world_top_left = inv_transform * Point::new(0.0, 0.0);
1060 let world_bottom_right = inv_transform * Point::new(viewport_size.width, viewport_size.height);
1061 let viewport_diagonal = ((world_bottom_right.x - world_top_left.x).powi(2)
1062 + (world_bottom_right.y - world_top_left.y).powi(2))
1063 .sqrt();
1064 let ray_length = viewport_diagonal;
1065
1066 let mut path = BezPath::new();
1068 for i in 0..24 {
1069 let angle_deg = i as f64 * 15.0;
1070 let angle_rad = angle_deg * PI / 180.0;
1071 let end_x = start.x + ray_length * angle_rad.cos();
1072 let end_y = start.y + ray_length * angle_rad.sin();
1073 path.move_to(start);
1074 path.line_to(Point::new(end_x, end_y));
1075 }
1076 self.scene.stroke(&Stroke::new(thin_stroke_width), transform, ray_color, None, &path);
1077
1078 if info.is_snapped {
1080 let angle_rad = info.angle_degrees * PI / 180.0;
1081 let mut active_path = BezPath::new();
1082 active_path.move_to(start);
1083 active_path.line_to(Point::new(
1084 start.x + ray_length * angle_rad.cos(),
1085 start.y + ray_length * angle_rad.sin(),
1086 ));
1087 self.scene.stroke(&Stroke::new(thick_stroke_width), transform, active_ray_color, None, &active_path);
1088
1089 let arc_radius = 30.0 / self.zoom;
1091 let segments = (info.angle_degrees.abs() / 5.0).ceil() as usize;
1092 let segments = segments.max(2).min(72); if segments > 1 {
1095 let mut arc_path = BezPath::new();
1096 let start_angle = 0.0_f64;
1097 let end_angle = info.angle_degrees * PI / 180.0;
1098
1099 let first_x = start.x + arc_radius * start_angle.cos();
1100 let first_y = start.y + arc_radius * start_angle.sin();
1101 arc_path.move_to(Point::new(first_x, first_y));
1102
1103 for i in 1..=segments {
1104 let t = i as f64 / segments as f64;
1105 let angle = start_angle + t * (end_angle - start_angle);
1106 let x = start.x + arc_radius * angle.cos();
1107 let y = start.y + arc_radius * angle.sin();
1108 arc_path.line_to(Point::new(x, y));
1109 }
1110
1111 self.scene.stroke(&Stroke::new(thick_stroke_width), transform, arc_color, None, &arc_path);
1112 }
1113
1114 let label_angle = info.angle_degrees * PI / 360.0; let label_radius = arc_radius + 15.0 / self.zoom;
1118 let _label_pos = Point::new(
1119 start.x + label_radius * label_angle.cos(),
1120 start.y + label_radius * label_angle.sin(),
1121 );
1122
1123 }
1126 }
1127
1128 fn render_selection_rect(&mut self, rect: Rect, transform: Affine) {
1131 let fill_color = Color::from_rgba8(59, 130, 246, 25);
1133 let mut path = BezPath::new();
1134 path.move_to(Point::new(rect.x0, rect.y0));
1135 path.line_to(Point::new(rect.x1, rect.y0));
1136 path.line_to(Point::new(rect.x1, rect.y1));
1137 path.line_to(Point::new(rect.x0, rect.y1));
1138 path.close_path();
1139
1140 self.scene.fill(
1141 Fill::NonZero,
1142 transform,
1143 fill_color,
1144 None,
1145 &path,
1146 );
1147
1148 let stroke_width = 1.0 / self.zoom;
1150 let dash_len = 4.0 / self.zoom;
1151 let stroke = Stroke::new(stroke_width).with_dashes(0.0, &[dash_len, dash_len]);
1152 self.scene.stroke(
1153 &stroke,
1154 transform,
1155 self.selection_color,
1156 None,
1157 &path,
1158 );
1159 }
1160}
1161
1162impl VelloRenderer {
1163 fn grid_bounds(&self, viewport: Rect, transform: Affine, grid_size: f64) -> (f64, f64, f64, f64) {
1165 let inv = transform.inverse();
1166 let world_tl = inv * Point::new(viewport.x0, viewport.y0);
1167 let world_br = inv * Point::new(viewport.x1, viewport.y1);
1168
1169 let start_x = (world_tl.x / grid_size).floor() * grid_size;
1170 let start_y = (world_tl.y / grid_size).floor() * grid_size;
1171 let end_x = (world_br.x / grid_size).ceil() * grid_size;
1172 let end_y = (world_br.y / grid_size).ceil() * grid_size;
1173
1174 (start_x, start_y, end_x, end_y)
1175 }
1176
1177 fn render_grid_lines(&mut self, viewport: Rect, transform: Affine, grid_size: f64) {
1179 let grid_color = Color::from_rgba8(200, 200, 200, 100);
1180 let stroke = Stroke::new(0.5);
1181
1182 let (start_x, start_y, end_x, end_y) = self.grid_bounds(viewport, transform, grid_size);
1183
1184 let mut x = start_x;
1186 while x <= end_x {
1187 let mut path = BezPath::new();
1188 path.move_to(Point::new(x, start_y));
1189 path.line_to(Point::new(x, end_y));
1190 self.scene.stroke(&stroke, transform, grid_color, None, &path);
1191 x += grid_size;
1192 }
1193
1194 let mut y = start_y;
1196 while y <= end_y {
1197 let mut path = BezPath::new();
1198 path.move_to(Point::new(start_x, y));
1199 path.line_to(Point::new(end_x, y));
1200 self.scene.stroke(&stroke, transform, grid_color, None, &path);
1201 y += grid_size;
1202 }
1203 }
1204
1205 fn render_grid_crosses(&mut self, viewport: Rect, transform: Affine, grid_size: f64) {
1208 let grid_color = Color::from_rgba8(180, 180, 180, 60); let stroke = Stroke::new(1.0);
1210 let cross_size = 3.0; let (start_x, start_y, end_x, end_y) = self.grid_bounds(viewport, transform, grid_size);
1213
1214 let mut path = BezPath::new();
1216
1217 let mut x = start_x;
1218 while x <= end_x {
1219 let mut y = start_y;
1220 while y <= end_y {
1221 path.move_to(Point::new(x - cross_size, y));
1223 path.line_to(Point::new(x + cross_size, y));
1224 path.move_to(Point::new(x, y - cross_size));
1226 path.line_to(Point::new(x, y + cross_size));
1227 y += grid_size;
1228 }
1229 x += grid_size;
1230 }
1231
1232 self.scene.stroke(&stroke, transform, grid_color, None, &path);
1234 }
1235
1236 fn render_grid_dots(&mut self, viewport: Rect, transform: Affine, grid_size: f64) {
1239 let grid_color = Color::from_rgba8(160, 160, 160, 70); let dot_size = 1.5; let (start_x, start_y, end_x, end_y) = self.grid_bounds(viewport, transform, grid_size);
1243
1244 let mut path = BezPath::new();
1246
1247 let mut x = start_x;
1248 while x <= end_x {
1249 let mut y = start_y;
1250 while y <= end_y {
1251 let rect = Rect::new(
1253 x - dot_size,
1254 y - dot_size,
1255 x + dot_size,
1256 y + dot_size,
1257 );
1258 path.move_to(Point::new(rect.x0, rect.y0));
1259 path.line_to(Point::new(rect.x1, rect.y0));
1260 path.line_to(Point::new(rect.x1, rect.y1));
1261 path.line_to(Point::new(rect.x0, rect.y1));
1262 path.close_path();
1263 y += grid_size;
1264 }
1265 x += grid_size;
1266 }
1267
1268 self.scene.fill(Fill::NonZero, transform, grid_color, None, &path);
1270 }
1271
1272 #[allow(dead_code)]
1273 fn render_selection_handles(&mut self, bounds: Rect, transform: Affine) {
1274 let handle_size = 8.0;
1275 let stroke = Stroke::new(2.0);
1276
1277 let mut path = BezPath::new();
1279 path.move_to(Point::new(bounds.x0, bounds.y0));
1280 path.line_to(Point::new(bounds.x1, bounds.y0));
1281 path.line_to(Point::new(bounds.x1, bounds.y1));
1282 path.line_to(Point::new(bounds.x0, bounds.y1));
1283 path.close_path();
1284
1285 self.scene.stroke(
1286 &stroke,
1287 transform,
1288 self.selection_color,
1289 None,
1290 &path,
1291 );
1292
1293 let corners = [
1295 Point::new(bounds.x0, bounds.y0),
1296 Point::new(bounds.x1, bounds.y0),
1297 Point::new(bounds.x1, bounds.y1),
1298 Point::new(bounds.x0, bounds.y1),
1299 ];
1300
1301 for corner in corners {
1302 let handle_rect = Rect::new(
1303 corner.x - handle_size / 2.0,
1304 corner.y - handle_size / 2.0,
1305 corner.x + handle_size / 2.0,
1306 corner.y + handle_size / 2.0,
1307 );
1308
1309 self.scene.fill(
1311 Fill::NonZero,
1312 transform,
1313 Color::WHITE,
1314 None,
1315 &handle_rect.to_path(0.1),
1316 );
1317
1318 self.scene.stroke(
1320 &Stroke::new(1.5),
1321 transform,
1322 self.selection_color,
1323 None,
1324 &handle_rect.to_path(0.1),
1325 );
1326 }
1327 }
1328}
1329
1330impl ShapeRenderer for VelloRenderer {
1331 fn render_shape(&mut self, shape: &Shape, transform: Affine, selected: bool) {
1332 match shape {
1334 Shape::Text(text) => {
1335 self.render_text(text, transform);
1336 }
1337 Shape::Group(group) => {
1338 for child in group.children() {
1340 self.render_shape(child, transform, false);
1342 }
1343 }
1344 Shape::Image(image) => {
1345 self.render_image(image, transform);
1346 }
1347 _ => {
1348 let path = shape.to_path();
1349 self.render_path(&path, shape.style(), transform);
1350 }
1351 }
1352
1353 if selected {
1355 self.render_shape_handles(shape, transform);
1356 }
1357 }
1358
1359 fn render_grid(&mut self, viewport: Rect, transform: Affine, grid_size: f64) {
1360 self.render_grid_lines(viewport, transform, grid_size);
1362 }
1363
1364 fn render_selection_handles(&mut self, _bounds: Rect, _transform: Affine) {
1365 }
1367}
1368
1369impl VelloRenderer {
1370 pub fn draw_cursor(&mut self, screen_pos: Point, color: Color, label: &str) {
1375 let mut path = BezPath::new();
1377 path.move_to(screen_pos); path.line_to(Point::new(screen_pos.x, screen_pos.y + 18.0)); path.line_to(Point::new(screen_pos.x + 14.0, screen_pos.y + 14.0)); path.close_path();
1382
1383 self.scene.fill(
1385 vello::peniko::Fill::NonZero,
1386 Affine::IDENTITY,
1387 color,
1388 None,
1389 &path,
1390 );
1391
1392 let stroke = Stroke::new(1.5);
1394 self.scene.stroke(&stroke, Affine::IDENTITY, Color::WHITE, None, &path);
1395 }
1396}
1397
1398#[cfg(test)]
1399mod tests {
1400 use super::*;
1401 use drafftink_core::canvas::Canvas;
1402 use drafftink_core::shapes::Rectangle;
1403
1404 #[test]
1405 fn test_renderer_creation() {
1406 let renderer = VelloRenderer::new();
1407 assert!(renderer.scene().encoding().is_empty());
1408 }
1409
1410 #[test]
1411 fn test_build_empty_scene() {
1412 let mut renderer = VelloRenderer::new();
1413 let canvas = Canvas::new();
1414 let ctx = RenderContext::new(&canvas, kurbo::Size::new(800.0, 600.0));
1415
1416 renderer.build_scene(&ctx);
1417 }
1419
1420 #[test]
1421 fn test_build_scene_with_shapes() {
1422 let mut renderer = VelloRenderer::new();
1423 let mut canvas = Canvas::new();
1424
1425 let rect = Rectangle::new(Point::new(100.0, 100.0), 200.0, 150.0);
1426 canvas.document.add_shape(Shape::Rectangle(rect));
1427
1428 let ctx = RenderContext::new(&canvas, kurbo::Size::new(800.0, 600.0));
1429 renderer.build_scene(&ctx);
1430 }
1431}