use crate::renderer::{RenderContext, Renderer, ShapeRenderer};
use crate::text_editor::TextEditState;
use kurbo::{Affine, BezPath, PathEl, Point, Rect, Shape as KurboShape, Stroke, Vec2};
use parley::layout::PositionedLayoutItem;
use parley::{FontContext, LayoutContext};
use peniko::{Brush, Color, Fill};
use drafftink_core::selection::{get_handles, Handle, HandleKind};
use drafftink_core::shapes::{Shape, ShapeStyle, ShapeTrait};
use drafftink_core::snap::{SnapTarget, SnapTargetKind};
use vello::Scene;
#[derive(Debug)]
pub struct PngRenderResult {
pub rgba_data: Vec<u8>,
pub width: u32,
pub height: u32,
}
static GELPEN_REGULAR: &[u8] = include_bytes!("../assets/GelPen.ttf");
static GELPEN_LIGHT: &[u8] = include_bytes!("../assets/GelPenLight.ttf");
static GELPEN_HEAVY: &[u8] = include_bytes!("../assets/GelPenHeavy.ttf");
static ROBOTO_LIGHT: &[u8] = include_bytes!("../assets/Roboto-Light.ttf");
static ROBOTO_REGULAR: &[u8] = include_bytes!("../assets/Roboto-Regular.ttf");
static ROBOTO_BOLD: &[u8] = include_bytes!("../assets/Roboto-Bold.ttf");
static ARCHITECTS_DAUGHTER: &[u8] = include_bytes!("../assets/ArchitectsDaughter.ttf");
pub struct VelloRenderer {
scene: Scene,
selection_color: Color,
font_cx: FontContext,
layout_cx: LayoutContext<Brush>,
zoom: f64,
image_cache: std::collections::HashMap<String, peniko::ImageData>,
}
impl Default for VelloRenderer {
fn default() -> Self {
Self::new()
}
}
fn convert_rect(rect: &parley::BoundingBox) -> Rect {
Rect::new(rect.x0, rect.y0, rect.x1, rect.y1)
}
struct SimpleRng {
state: u32,
}
impl SimpleRng {
fn new(seed: u32) -> Self {
Self { state: seed.max(1) }
}
fn next_u32(&mut self) -> u32 {
let mut x = self.state;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
self.state = x;
x
}
fn next_f64(&mut self) -> f64 {
(self.next_u32() as f64 / u32::MAX as f64) * 2.0 - 1.0
}
fn offset(&mut self, amount: f64) -> f64 {
self.next_f64() * amount
}
}
fn apply_hand_drawn_effect(path: &BezPath, roughness: f64, zoom: f64, seed: u32, stroke_index: u32) -> BezPath {
if roughness <= 0.0 {
return path.clone();
}
let scale = 1.0 / zoom.sqrt();
let max_randomness_offset = roughness * 2.0 * scale;
let bowing = roughness * 1.0;
let combined_seed = seed.wrapping_add(stroke_index.wrapping_mul(99991)); let mut rng = SimpleRng::new(combined_seed);
let mut result = BezPath::new();
let mut last_point = Point::ZERO;
for el in path.elements() {
match el {
PathEl::MoveTo(p) => {
let wobbled = Point::new(
p.x + rng.offset(max_randomness_offset),
p.y + rng.offset(max_randomness_offset),
);
result.move_to(wobbled);
last_point = *p;
}
PathEl::LineTo(p) => {
let dx = p.x - last_point.x;
let dy = p.y - last_point.y;
let len = (dx * dx + dy * dy).sqrt();
let bow_offset = bowing * roughness * len / 200.0;
let bow = rng.offset(bow_offset) * scale;
let (perp_x, perp_y) = if len > 0.001 {
(-dy / len, dx / len)
} else {
(0.0, 0.0)
};
let mid_x = (last_point.x + p.x) / 2.0 + perp_x * bow;
let mid_y = (last_point.y + p.y) / 2.0 + perp_y * bow;
let end = Point::new(
p.x + rng.offset(max_randomness_offset),
p.y + rng.offset(max_randomness_offset),
);
result.quad_to(Point::new(mid_x, mid_y), end);
last_point = *p;
}
PathEl::QuadTo(p1, p2) => {
let wobbled_p1 = Point::new(
p1.x + rng.offset(max_randomness_offset * 0.7),
p1.y + rng.offset(max_randomness_offset * 0.7),
);
let wobbled_p2 = Point::new(
p2.x + rng.offset(max_randomness_offset),
p2.y + rng.offset(max_randomness_offset),
);
result.quad_to(wobbled_p1, wobbled_p2);
last_point = *p2;
}
PathEl::CurveTo(p1, p2, p3) => {
let wobbled_p1 = Point::new(
p1.x + rng.offset(max_randomness_offset * 0.5),
p1.y + rng.offset(max_randomness_offset * 0.5),
);
let wobbled_p2 = Point::new(
p2.x + rng.offset(max_randomness_offset * 0.5),
p2.y + rng.offset(max_randomness_offset * 0.5),
);
let wobbled_p3 = Point::new(
p3.x + rng.offset(max_randomness_offset),
p3.y + rng.offset(max_randomness_offset),
);
result.curve_to(wobbled_p1, wobbled_p2, wobbled_p3);
last_point = *p3;
}
PathEl::ClosePath => {
result.close_path();
}
}
}
result
}
impl VelloRenderer {
pub fn new() -> Self {
let mut font_cx = FontContext::new();
font_cx.collection.register_fonts(
vello::peniko::Blob::new(std::sync::Arc::new(GELPEN_REGULAR)),
None,
);
font_cx.collection.register_fonts(
vello::peniko::Blob::new(std::sync::Arc::new(GELPEN_LIGHT)),
None,
);
font_cx.collection.register_fonts(
vello::peniko::Blob::new(std::sync::Arc::new(GELPEN_HEAVY)),
None,
);
font_cx.collection.register_fonts(
vello::peniko::Blob::new(std::sync::Arc::new(ROBOTO_LIGHT)),
None,
);
font_cx.collection.register_fonts(
vello::peniko::Blob::new(std::sync::Arc::new(ROBOTO_REGULAR)),
None,
);
font_cx.collection.register_fonts(
vello::peniko::Blob::new(std::sync::Arc::new(ROBOTO_BOLD)),
None,
);
font_cx.collection.register_fonts(
vello::peniko::Blob::new(std::sync::Arc::new(ARCHITECTS_DAUGHTER)),
None,
);
Self {
scene: Scene::new(),
selection_color: Color::from_rgba8(59, 130, 246, 255),
font_cx,
layout_cx: LayoutContext::new(),
zoom: 1.0,
image_cache: std::collections::HashMap::new(),
}
}
pub fn scene(&self) -> &Scene {
&self.scene
}
pub fn take_scene(&mut self) -> Scene {
std::mem::take(&mut self.scene)
}
pub fn contexts_mut(&mut self) -> (&mut FontContext, &mut LayoutContext<Brush>) {
(&mut self.font_cx, &mut self.layout_cx)
}
pub fn build_export_scene(&mut self, document: &drafftink_core::canvas::CanvasDocument, scale: f64) -> (Scene, Option<Rect>) {
self.scene.reset();
self.zoom = scale;
let bounds = document.bounds();
if bounds.is_none() {
return (std::mem::take(&mut self.scene), None);
}
let bounds = bounds.unwrap();
let padding = 20.0;
let padded_bounds = bounds.inflate(padding, padding);
let transform = Affine::scale(scale)
* Affine::translate((-padded_bounds.x0, -padded_bounds.y0));
let scaled_width = padded_bounds.width() * scale;
let scaled_height = padded_bounds.height() * scale;
let bg_rect = Rect::new(0.0, 0.0, scaled_width, scaled_height);
self.scene.fill(
Fill::NonZero,
Affine::IDENTITY,
Color::WHITE,
None,
&bg_rect,
);
for shape in document.shapes_ordered() {
self.render_shape(shape, transform, false);
}
let scaled_bounds = Rect::new(0.0, 0.0, scaled_width, scaled_height);
(std::mem::take(&mut self.scene), Some(scaled_bounds))
}
pub fn build_export_scene_selection(
&mut self,
document: &drafftink_core::canvas::CanvasDocument,
selection: &[drafftink_core::shapes::ShapeId],
scale: f64,
) -> (Scene, Option<Rect>) {
self.scene.reset();
self.zoom = scale;
if selection.is_empty() {
return (std::mem::take(&mut self.scene), None);
}
let mut min_x = f64::MAX;
let mut min_y = f64::MAX;
let mut max_x = f64::MIN;
let mut max_y = f64::MIN;
let mut shapes_to_render = Vec::new();
for &shape_id in selection {
if let Some(shape) = document.get_shape(shape_id) {
let b = shape.bounds();
min_x = min_x.min(b.x0);
min_y = min_y.min(b.y0);
max_x = max_x.max(b.x1);
max_y = max_y.max(b.y1);
shapes_to_render.push(shape);
}
}
if shapes_to_render.is_empty() {
return (std::mem::take(&mut self.scene), None);
}
let bounds = Rect::new(min_x, min_y, max_x, max_y);
let padding = 20.0;
let padded_bounds = bounds.inflate(padding, padding);
let transform = Affine::scale(scale)
* Affine::translate((-padded_bounds.x0, -padded_bounds.y0));
let scaled_width = padded_bounds.width() * scale;
let scaled_height = padded_bounds.height() * scale;
let bg_rect = Rect::new(0.0, 0.0, scaled_width, scaled_height);
self.scene.fill(
Fill::NonZero,
Affine::IDENTITY,
Color::WHITE,
None,
&bg_rect,
);
for shape in shapes_to_render {
self.render_shape(shape, transform, false);
}
let scaled_bounds = Rect::new(0.0, 0.0, scaled_width, scaled_height);
(std::mem::take(&mut self.scene), Some(scaled_bounds))
}
fn render_path(&mut self, path: &BezPath, style: &ShapeStyle, transform: Affine) {
let roughness = style.sloppiness.roughness();
let seed = style.seed;
if let Some(fill_color) = style.fill() {
let fill_path = if roughness > 0.0 {
apply_hand_drawn_effect(path, roughness * 0.3, self.zoom, seed, 0)
} else {
path.clone()
};
self.scene.fill(
Fill::NonZero,
transform,
fill_color,
None,
&fill_path,
);
}
if roughness > 0.0 {
let stroke = Stroke::new(style.stroke_width);
let path1 = apply_hand_drawn_effect(path, roughness, self.zoom, seed, 0);
self.scene.stroke(
&stroke,
transform,
style.stroke(),
None,
&path1,
);
let path2 = apply_hand_drawn_effect(path, roughness, self.zoom, seed, 1);
self.scene.stroke(
&stroke,
transform,
style.stroke(),
None,
&path2,
);
} else {
let stroke = Stroke::new(style.stroke_width);
self.scene.stroke(
&stroke,
transform,
style.stroke(),
None,
path,
);
}
}
fn render_text(&mut self, text: &drafftink_core::shapes::Text, transform: Affine) {
use parley::layout::PositionedLayoutItem;
use parley::StyleProperty;
if text.content.is_empty() {
let cursor_height = text.font_size * 1.2;
let cursor = kurbo::Line::new(
Point::new(text.position.x, text.position.y),
Point::new(text.position.x, text.position.y + cursor_height),
);
let stroke = Stroke::new(2.0);
self.scene.stroke(&stroke, transform, Color::from_rgba8(100, 100, 100, 200), None, &cursor);
return;
}
use drafftink_core::shapes::{FontFamily, FontWeight};
let style = &text.style;
let brush = Brush::Solid(style.stroke());
let font_size = text.font_size as f32;
let (font_name, parley_weight) = match (&text.font_family, &text.font_weight) {
(FontFamily::GelPen, FontWeight::Light) => ("GelPenLight", parley::FontWeight::NORMAL),
(FontFamily::GelPen, FontWeight::Regular) => ("GelPen", parley::FontWeight::NORMAL),
(FontFamily::GelPen, FontWeight::Heavy) => ("GelPenHeavy", parley::FontWeight::NORMAL),
(FontFamily::Roboto, FontWeight::Light) => ("Roboto", parley::FontWeight::LIGHT),
(FontFamily::Roboto, FontWeight::Regular) => ("Roboto", parley::FontWeight::NORMAL),
(FontFamily::Roboto, FontWeight::Heavy) => ("Roboto", parley::FontWeight::BOLD),
(FontFamily::ArchitectsDaughter, _) => ("Architects Daughter", parley::FontWeight::NORMAL),
};
let mut builder = self.layout_cx.ranged_builder(&mut self.font_cx, &text.content, 1.0, false);
builder.push_default(StyleProperty::FontSize(font_size));
builder.push_default(StyleProperty::Brush(brush.clone()));
builder.push_default(StyleProperty::FontWeight(parley_weight));
builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(
parley::FontFamily::Named(font_name.into())
)));
let mut layout = builder.build(&text.content);
layout.break_all_lines(None);
layout.align(None, parley::Alignment::Start, parley::AlignmentOptions::default());
let layout_width = layout.width() as f64;
let layout_height = layout.height() as f64;
text.set_cached_size(layout_width, layout_height);
let text_transform = transform * Affine::translate((text.position.x, text.position.y));
let mut glyph_count = 0;
for line in layout.lines() {
for item in line.items() {
let PositionedLayoutItem::GlyphRun(glyph_run) = item else {
continue;
};
let mut x = glyph_run.offset();
let y = glyph_run.baseline();
let run = glyph_run.run();
let font = run.font();
let font_size = run.font_size();
let synthesis = run.synthesis();
let glyph_xform = synthesis
.skew()
.map(|angle| Affine::skew(angle.to_radians().tan() as f64, 0.0));
let glyphs: Vec<vello::Glyph> = glyph_run.glyphs().map(|glyph| {
let gx = x + glyph.x;
let gy = y - glyph.y;
x += glyph.advance;
glyph_count += 1;
vello::Glyph {
id: glyph.id,
x: gx,
y: gy,
}
}).collect();
if !glyphs.is_empty() {
self.scene
.draw_glyphs(font)
.brush(&brush)
.hint(true)
.transform(text_transform)
.glyph_transform(glyph_xform)
.font_size(font_size)
.normalized_coords(run.normalized_coords())
.draw(Fill::NonZero, glyphs.into_iter());
}
}
}
if glyph_count == 0 {
let width = text.content.len() as f64 * text.font_size * 0.6;
let height = text.font_size * 1.2;
let rect = Rect::new(
text.position.x,
text.position.y,
text.position.x + width.max(20.0),
text.position.y + height,
);
self.scene.fill(Fill::NonZero, transform, Color::from_rgba8(255, 100, 100, 100), None, &rect);
}
}
fn render_image(&mut self, image: &drafftink_core::shapes::Image, transform: Affine) {
use std::sync::Arc;
let id_str = image.id().to_string();
let image_data = if let Some(cached) = self.image_cache.get(&id_str) {
cached.clone()
} else {
if let Some(raw_data) = image.data() {
if let Ok(decoded) = ::image::load_from_memory(&raw_data) {
let rgba = decoded.to_rgba8();
let (width, height) = rgba.dimensions();
let blob = peniko::Blob::new(Arc::new(rgba.into_vec()));
let img_data = peniko::ImageData {
data: blob,
format: peniko::ImageFormat::Rgba8,
width,
height,
alpha_type: peniko::ImageAlphaType::Alpha,
};
self.image_cache.insert(id_str.clone(), img_data.clone());
img_data
} else {
self.render_image_placeholder(image, transform, "Failed to decode");
return;
}
} else {
self.render_image_placeholder(image, transform, "No image data");
return;
}
};
let bounds = image.bounds();
let scale_x = bounds.width() / image_data.width as f64;
let scale_y = bounds.height() / image_data.height as f64;
let image_transform = transform
* Affine::translate((bounds.x0, bounds.y0))
* Affine::scale_non_uniform(scale_x, scale_y);
self.scene.draw_image(&image_data.into(), image_transform);
}
fn render_image_placeholder(&mut self, image: &drafftink_core::shapes::Image, transform: Affine, _msg: &str) {
let bounds = image.bounds();
let rect_path = bounds.to_path(0.1);
self.scene.fill(Fill::NonZero, transform, Color::from_rgba8(200, 200, 200, 255), None, &rect_path);
let stroke = Stroke::new(2.0);
let mut x_path = BezPath::new();
x_path.move_to(Point::new(bounds.x0, bounds.y0));
x_path.line_to(Point::new(bounds.x1, bounds.y1));
x_path.move_to(Point::new(bounds.x1, bounds.y0));
x_path.line_to(Point::new(bounds.x0, bounds.y1));
self.scene.stroke(&stroke, transform, Color::from_rgba8(150, 150, 150, 255), None, &x_path);
self.scene.stroke(&stroke, transform, Color::from_rgba8(100, 100, 100, 255), None, &rect_path);
}
pub fn render_text_editing(
&mut self,
text: &drafftink_core::shapes::Text,
edit_state: &mut TextEditState,
transform: Affine,
) {
use drafftink_core::shapes::{FontFamily as ShapeFontFamily, FontWeight};
let style = &text.style;
let brush = Brush::Solid(style.stroke());
let (font_name, parley_weight) = match (&text.font_family, &text.font_weight) {
(ShapeFontFamily::GelPen, FontWeight::Light) => ("GelPenLight", parley::FontWeight::NORMAL),
(ShapeFontFamily::GelPen, FontWeight::Regular) => ("GelPen", parley::FontWeight::NORMAL),
(ShapeFontFamily::GelPen, FontWeight::Heavy) => ("GelPenHeavy", parley::FontWeight::NORMAL),
(ShapeFontFamily::Roboto, FontWeight::Light) => ("Roboto", parley::FontWeight::LIGHT),
(ShapeFontFamily::Roboto, FontWeight::Regular) => ("Roboto", parley::FontWeight::NORMAL),
(ShapeFontFamily::Roboto, FontWeight::Heavy) => ("Roboto", parley::FontWeight::BOLD),
(ShapeFontFamily::ArchitectsDaughter, _) => ("Architects Daughter", parley::FontWeight::NORMAL),
};
edit_state.set_font_size(text.font_size as f32);
edit_state.set_brush(brush.clone());
{
use parley::{FontStack, FontFamily, StyleProperty};
let styles = edit_state.editor_mut().edit_styles();
styles.insert(StyleProperty::FontStack(FontStack::Single(
FontFamily::Named(font_name.into())
)));
styles.insert(StyleProperty::FontWeight(parley_weight));
}
let text_transform = transform * Affine::translate((text.position.x, text.position.y));
let layout = edit_state.editor_mut().layout(&mut self.font_cx, &mut self.layout_cx);
for line in layout.lines() {
for item in line.items() {
let PositionedLayoutItem::GlyphRun(glyph_run) = item else {
continue;
};
let glyph_style = glyph_run.style();
let mut x = glyph_run.offset();
let y = glyph_run.baseline();
let run = glyph_run.run();
let font = run.font();
let font_size = run.font_size();
let synthesis = run.synthesis();
let glyph_xform = synthesis
.skew()
.map(|angle| Affine::skew(angle.to_radians().tan() as f64, 0.0));
let glyphs: Vec<vello::Glyph> = glyph_run.glyphs().map(|glyph| {
let gx = x + glyph.x;
let gy = y - glyph.y;
x += glyph.advance;
vello::Glyph {
id: glyph.id,
x: gx,
y: gy,
}
}).collect();
if !glyphs.is_empty() {
self.scene
.draw_glyphs(font)
.brush(&glyph_style.brush)
.hint(true)
.transform(text_transform)
.glyph_transform(glyph_xform)
.font_size(font_size)
.normalized_coords(run.normalized_coords())
.draw(Fill::NonZero, glyphs.into_iter());
}
}
}
let selection_color = Color::from_rgba8(70, 130, 180, 128);
edit_state.editor().selection_geometry_with(|rect, _| {
self.scene.fill(
Fill::NonZero,
text_transform,
selection_color,
None,
&convert_rect(&rect),
);
});
if edit_state.is_cursor_visible() {
if let Some(cursor) = edit_state.editor().cursor_geometry(1.5) {
let cursor_color = Color::from_rgba8(0, 0, 0, 255);
self.scene.fill(
Fill::NonZero,
text_transform,
cursor_color,
None,
&convert_rect(&cursor),
);
} else if edit_state.text().is_empty() {
let cursor_height = text.font_size * 1.2;
let cursor_rect = Rect::new(0.0, 0.0, 1.5, cursor_height);
self.scene.fill(
Fill::NonZero,
text_transform,
Color::from_rgba8(0, 0, 0, 255),
None,
&cursor_rect,
);
}
}
}
fn render_shape_handles(&mut self, shape: &Shape, transform: Affine) {
let handles = get_handles(shape);
let handle_size = 8.0 / self.zoom;
let stroke_width = 1.0 / self.zoom;
let dash_len = 4.0 / self.zoom;
match shape {
Shape::Line(_) | Shape::Arrow(_) => {
}
_ => {
let bounds = shape.bounds();
let stroke = Stroke::new(stroke_width).with_dashes(0.0, &[dash_len, dash_len]);
let mut path = BezPath::new();
path.move_to(Point::new(bounds.x0, bounds.y0));
path.line_to(Point::new(bounds.x1, bounds.y0));
path.line_to(Point::new(bounds.x1, bounds.y1));
path.line_to(Point::new(bounds.x0, bounds.y1));
path.close_path();
self.scene.stroke(
&stroke,
transform,
self.selection_color,
None,
&path,
);
}
}
for handle in handles {
self.render_handle(&handle, transform, handle_size);
}
}
fn render_handle(&mut self, handle: &Handle, transform: Affine, size: f64) {
let pos = handle.position;
let stroke_width_thick = 2.0 / self.zoom;
let stroke_width_thin = 1.5 / self.zoom;
match handle.kind {
HandleKind::Endpoint(_) => {
let radius = size / 2.0;
let ellipse = kurbo::Ellipse::new(pos, (radius, radius), 0.0);
let path = ellipse.to_path(0.1);
self.scene.fill(
Fill::NonZero,
transform,
Color::WHITE,
None,
&path,
);
self.scene.stroke(
&Stroke::new(stroke_width_thick),
transform,
self.selection_color,
None,
&path,
);
}
HandleKind::Corner(_) | HandleKind::Edge(_) => {
let half = size / 2.0;
let rect = Rect::new(
pos.x - half,
pos.y - half,
pos.x + half,
pos.y + half,
);
let path = rect.to_path(0.1);
self.scene.fill(
Fill::NonZero,
transform,
Color::WHITE,
None,
&path,
);
self.scene.stroke(
&Stroke::new(stroke_width_thin),
transform,
self.selection_color,
None,
&path,
);
}
}
}
}
impl Renderer for VelloRenderer {
fn build_scene(&mut self, ctx: &RenderContext) {
self.scene.reset();
self.selection_color = ctx.selection_color;
self.zoom = ctx.canvas.camera.zoom;
let camera_transform = ctx.canvas.camera.transform();
use crate::renderer::GridStyle;
match ctx.grid_style {
GridStyle::None => {}
GridStyle::Lines => {
self.render_grid_lines(
Rect::new(0.0, 0.0, ctx.viewport_size.width, ctx.viewport_size.height),
camera_transform,
20.0,
);
}
GridStyle::CrossPlus => {
self.render_grid_crosses(
Rect::new(0.0, 0.0, ctx.viewport_size.width, ctx.viewport_size.height),
camera_transform,
20.0,
);
}
GridStyle::Dots => {
self.render_grid_dots(
Rect::new(0.0, 0.0, ctx.viewport_size.width, ctx.viewport_size.height),
camera_transform,
20.0,
);
}
}
for shape in ctx.canvas.document.shapes_ordered() {
if ctx.editing_shape_id == Some(shape.id()) {
continue;
}
let is_selected = ctx.canvas.is_selected(shape.id());
self.render_shape(shape, camera_transform, is_selected);
}
if let Some(preview) = ctx.canvas.tool_manager.preview_shape() {
self.render_shape(&preview, camera_transform, false);
}
if let Some(rect) = ctx.selection_rect {
self.render_selection_rect(rect, camera_transform);
}
if !ctx.nearby_snap_targets.is_empty() {
self.render_snap_targets(&ctx.nearby_snap_targets, camera_transform);
}
if let Some(snap_point) = ctx.snap_point {
self.render_snap_guides(snap_point, camera_transform, ctx.viewport_size);
}
if let Some(ref angle_info) = ctx.angle_snap_info {
self.render_angle_snap_guides(angle_info, camera_transform, ctx.viewport_size);
}
}
}
impl VelloRenderer {
fn render_snap_guides(&mut self, snap_point: Point, transform: Affine, viewport_size: kurbo::Size) {
let guide_color = Color::from_rgba8(236, 72, 153, 180);
let stroke_width = 1.0 / self.zoom;
let stroke = Stroke::new(stroke_width);
let inv_transform = transform.inverse();
let world_top_left = inv_transform * Point::new(0.0, 0.0);
let world_bottom_right = inv_transform * Point::new(viewport_size.width, viewport_size.height);
let mut h_path = BezPath::new();
h_path.move_to(Point::new(world_top_left.x, snap_point.y));
h_path.line_to(Point::new(world_bottom_right.x, snap_point.y));
self.scene.stroke(&stroke, transform, guide_color, None, &h_path);
let mut v_path = BezPath::new();
v_path.move_to(Point::new(snap_point.x, world_top_left.y));
v_path.line_to(Point::new(snap_point.x, world_bottom_right.y));
self.scene.stroke(&stroke, transform, guide_color, None, &v_path);
let circle_radius = 4.0 / self.zoom;
let circle = kurbo::Circle::new(snap_point, circle_radius);
self.scene.stroke(&Stroke::new(stroke_width * 2.0), transform, guide_color, None, &circle);
}
fn render_snap_targets(&mut self, targets: &[SnapTarget], transform: Affine) {
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;
let stroke_width = 1.0 / self.zoom;
for target in targets {
let color = match target.kind {
SnapTargetKind::Corner => corner_color,
SnapTargetKind::Midpoint => midpoint_color,
SnapTargetKind::Center => center_color,
SnapTargetKind::Edge => corner_color, };
match target.kind {
SnapTargetKind::Corner | SnapTargetKind::Edge => {
let half = size;
let rect = Rect::new(
target.point.x - half,
target.point.y - half,
target.point.x + half,
target.point.y + half,
);
self.scene.stroke(&Stroke::new(stroke_width), transform, color, None, &rect);
}
SnapTargetKind::Midpoint => {
let mut path = BezPath::new();
path.move_to(Point::new(target.point.x, target.point.y - size));
path.line_to(Point::new(target.point.x + size, target.point.y));
path.line_to(Point::new(target.point.x, target.point.y + size));
path.line_to(Point::new(target.point.x - size, target.point.y));
path.close_path();
self.scene.stroke(&Stroke::new(stroke_width), transform, color, None, &path);
}
SnapTargetKind::Center => {
let circle = kurbo::Circle::new(target.point, size);
self.scene.stroke(&Stroke::new(stroke_width), transform, color, None, &circle);
}
}
}
}
fn render_angle_snap_guides(
&mut self,
info: &crate::renderer::AngleSnapInfo,
transform: Affine,
viewport_size: kurbo::Size,
) {
use std::f64::consts::PI;
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;
let thick_stroke_width = 1.5 / self.zoom;
let start = info.start_point;
let inv_transform = transform.inverse();
let world_top_left = inv_transform * Point::new(0.0, 0.0);
let world_bottom_right = inv_transform * Point::new(viewport_size.width, viewport_size.height);
let viewport_diagonal = ((world_bottom_right.x - world_top_left.x).powi(2)
+ (world_bottom_right.y - world_top_left.y).powi(2))
.sqrt();
let ray_length = viewport_diagonal;
let mut path = BezPath::new();
for i in 0..24 {
let angle_deg = i as f64 * 15.0;
let angle_rad = angle_deg * PI / 180.0;
let end_x = start.x + ray_length * angle_rad.cos();
let end_y = start.y + ray_length * angle_rad.sin();
path.move_to(start);
path.line_to(Point::new(end_x, end_y));
}
self.scene.stroke(&Stroke::new(thin_stroke_width), transform, ray_color, None, &path);
if info.is_snapped {
let angle_rad = info.angle_degrees * PI / 180.0;
let mut active_path = BezPath::new();
active_path.move_to(start);
active_path.line_to(Point::new(
start.x + ray_length * angle_rad.cos(),
start.y + ray_length * angle_rad.sin(),
));
self.scene.stroke(&Stroke::new(thick_stroke_width), transform, active_ray_color, None, &active_path);
let arc_radius = 30.0 / self.zoom;
let segments = (info.angle_degrees.abs() / 5.0).ceil() as usize;
let segments = segments.max(2).min(72);
if segments > 1 {
let mut arc_path = BezPath::new();
let start_angle = 0.0_f64;
let end_angle = info.angle_degrees * PI / 180.0;
let first_x = start.x + arc_radius * start_angle.cos();
let first_y = start.y + arc_radius * start_angle.sin();
arc_path.move_to(Point::new(first_x, first_y));
for i in 1..=segments {
let t = i as f64 / segments as f64;
let angle = start_angle + t * (end_angle - start_angle);
let x = start.x + arc_radius * angle.cos();
let y = start.y + arc_radius * angle.sin();
arc_path.line_to(Point::new(x, y));
}
self.scene.stroke(&Stroke::new(thick_stroke_width), transform, arc_color, None, &arc_path);
}
let label_angle = info.angle_degrees * PI / 360.0; let label_radius = arc_radius + 15.0 / self.zoom;
let _label_pos = Point::new(
start.x + label_radius * label_angle.cos(),
start.y + label_radius * label_angle.sin(),
);
}
}
fn render_selection_rect(&mut self, rect: Rect, transform: Affine) {
let fill_color = Color::from_rgba8(59, 130, 246, 25);
let mut path = BezPath::new();
path.move_to(Point::new(rect.x0, rect.y0));
path.line_to(Point::new(rect.x1, rect.y0));
path.line_to(Point::new(rect.x1, rect.y1));
path.line_to(Point::new(rect.x0, rect.y1));
path.close_path();
self.scene.fill(
Fill::NonZero,
transform,
fill_color,
None,
&path,
);
let stroke_width = 1.0 / self.zoom;
let dash_len = 4.0 / self.zoom;
let stroke = Stroke::new(stroke_width).with_dashes(0.0, &[dash_len, dash_len]);
self.scene.stroke(
&stroke,
transform,
self.selection_color,
None,
&path,
);
}
}
impl VelloRenderer {
fn grid_bounds(&self, viewport: Rect, transform: Affine, grid_size: f64) -> (f64, f64, f64, f64) {
let inv = transform.inverse();
let world_tl = inv * Point::new(viewport.x0, viewport.y0);
let world_br = inv * Point::new(viewport.x1, viewport.y1);
let start_x = (world_tl.x / grid_size).floor() * grid_size;
let start_y = (world_tl.y / grid_size).floor() * grid_size;
let end_x = (world_br.x / grid_size).ceil() * grid_size;
let end_y = (world_br.y / grid_size).ceil() * grid_size;
(start_x, start_y, end_x, end_y)
}
fn render_grid_lines(&mut self, viewport: Rect, transform: Affine, grid_size: f64) {
let grid_color = Color::from_rgba8(200, 200, 200, 100);
let stroke = Stroke::new(0.5);
let (start_x, start_y, end_x, end_y) = self.grid_bounds(viewport, transform, grid_size);
let mut x = start_x;
while x <= end_x {
let mut path = BezPath::new();
path.move_to(Point::new(x, start_y));
path.line_to(Point::new(x, end_y));
self.scene.stroke(&stroke, transform, grid_color, None, &path);
x += grid_size;
}
let mut y = start_y;
while y <= end_y {
let mut path = BezPath::new();
path.move_to(Point::new(start_x, y));
path.line_to(Point::new(end_x, y));
self.scene.stroke(&stroke, transform, grid_color, None, &path);
y += grid_size;
}
}
fn render_grid_crosses(&mut self, viewport: Rect, transform: Affine, grid_size: f64) {
let grid_color = Color::from_rgba8(180, 180, 180, 60); let stroke = Stroke::new(1.0);
let cross_size = 3.0;
let (start_x, start_y, end_x, end_y) = self.grid_bounds(viewport, transform, grid_size);
let mut path = BezPath::new();
let mut x = start_x;
while x <= end_x {
let mut y = start_y;
while y <= end_y {
path.move_to(Point::new(x - cross_size, y));
path.line_to(Point::new(x + cross_size, y));
path.move_to(Point::new(x, y - cross_size));
path.line_to(Point::new(x, y + cross_size));
y += grid_size;
}
x += grid_size;
}
self.scene.stroke(&stroke, transform, grid_color, None, &path);
}
fn render_grid_dots(&mut self, viewport: Rect, transform: Affine, grid_size: f64) {
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);
let mut path = BezPath::new();
let mut x = start_x;
while x <= end_x {
let mut y = start_y;
while y <= end_y {
let rect = Rect::new(
x - dot_size,
y - dot_size,
x + dot_size,
y + dot_size,
);
path.move_to(Point::new(rect.x0, rect.y0));
path.line_to(Point::new(rect.x1, rect.y0));
path.line_to(Point::new(rect.x1, rect.y1));
path.line_to(Point::new(rect.x0, rect.y1));
path.close_path();
y += grid_size;
}
x += grid_size;
}
self.scene.fill(Fill::NonZero, transform, grid_color, None, &path);
}
#[allow(dead_code)]
fn render_selection_handles(&mut self, bounds: Rect, transform: Affine) {
let handle_size = 8.0;
let stroke = Stroke::new(2.0);
let mut path = BezPath::new();
path.move_to(Point::new(bounds.x0, bounds.y0));
path.line_to(Point::new(bounds.x1, bounds.y0));
path.line_to(Point::new(bounds.x1, bounds.y1));
path.line_to(Point::new(bounds.x0, bounds.y1));
path.close_path();
self.scene.stroke(
&stroke,
transform,
self.selection_color,
None,
&path,
);
let corners = [
Point::new(bounds.x0, bounds.y0),
Point::new(bounds.x1, bounds.y0),
Point::new(bounds.x1, bounds.y1),
Point::new(bounds.x0, bounds.y1),
];
for corner in corners {
let handle_rect = Rect::new(
corner.x - handle_size / 2.0,
corner.y - handle_size / 2.0,
corner.x + handle_size / 2.0,
corner.y + handle_size / 2.0,
);
self.scene.fill(
Fill::NonZero,
transform,
Color::WHITE,
None,
&handle_rect.to_path(0.1),
);
self.scene.stroke(
&Stroke::new(1.5),
transform,
self.selection_color,
None,
&handle_rect.to_path(0.1),
);
}
}
}
impl ShapeRenderer for VelloRenderer {
fn render_shape(&mut self, shape: &Shape, transform: Affine, selected: bool) {
match shape {
Shape::Text(text) => {
self.render_text(text, transform);
}
Shape::Group(group) => {
for child in group.children() {
self.render_shape(child, transform, false);
}
}
Shape::Image(image) => {
self.render_image(image, transform);
}
_ => {
let path = shape.to_path();
self.render_path(&path, shape.style(), transform);
}
}
if selected {
self.render_shape_handles(shape, transform);
}
}
fn render_grid(&mut self, viewport: Rect, transform: Affine, grid_size: f64) {
self.render_grid_lines(viewport, transform, grid_size);
}
fn render_selection_handles(&mut self, _bounds: Rect, _transform: Affine) {
}
}
impl VelloRenderer {
pub fn draw_cursor(&mut self, screen_pos: Point, color: Color, label: &str) {
let mut path = BezPath::new();
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();
self.scene.fill(
vello::peniko::Fill::NonZero,
Affine::IDENTITY,
color,
None,
&path,
);
let stroke = Stroke::new(1.5);
self.scene.stroke(&stroke, Affine::IDENTITY, Color::WHITE, None, &path);
}
}
#[cfg(test)]
mod tests {
use super::*;
use drafftink_core::canvas::Canvas;
use drafftink_core::shapes::Rectangle;
#[test]
fn test_renderer_creation() {
let renderer = VelloRenderer::new();
assert!(renderer.scene().encoding().is_empty());
}
#[test]
fn test_build_empty_scene() {
let mut renderer = VelloRenderer::new();
let canvas = Canvas::new();
let ctx = RenderContext::new(&canvas, kurbo::Size::new(800.0, 600.0));
renderer.build_scene(&ctx);
}
#[test]
fn test_build_scene_with_shapes() {
let mut renderer = VelloRenderer::new();
let mut canvas = Canvas::new();
let rect = Rectangle::new(Point::new(100.0, 100.0), 200.0, 150.0);
canvas.document.add_shape(Shape::Rectangle(rect));
let ctx = RenderContext::new(&canvas, kurbo::Size::new(800.0, 600.0));
renderer.build_scene(&ctx);
}
}