use crate::raster::{Image, ImageFrame}; use crate::uuid::{generate_uuid, ManipulatorGroupId}; use crate::{vector::VectorData, Artboard, Color, GraphicElementData, GraphicGroup}; use base64::Engine; use bezier_rs::Subpath; use image::ImageEncoder; pub use quad::Quad; use glam::{DAffine2, DVec2}; mod quad; /// Represents a clickable target for the layer #[derive(Clone, Debug)] pub struct ClickTarget { pub subpath: bezier_rs::Subpath, pub stroke_width: f64, } impl ClickTarget { /// Does the click target intersect the rectangle pub fn intersect_rectangle(&self, document_quad: Quad, layer_transform: DAffine2) -> bool { let quad = layer_transform.inverse() * document_quad; // Check if outlines intersect if self .subpath .iter() .any(|path_segment| quad.bezier_lines().any(|line| !path_segment.intersections(&line, None, None).is_empty())) { return true; } // Check if selection is entirely within the shape if self.subpath.closed() && self.subpath.contains_point(quad.center()) { return true; } // Check if shape is entirely within selection self.subpath .manipulator_groups() .first() .map(|group| group.anchor) .map(|shape_point| quad.contains(shape_point)) .unwrap_or_default() } /// Does the click target intersect the point (accounting for stroke size) pub fn intersect_point(&self, point: DVec2, layer_transform: DAffine2) -> bool { // Allows for selecting lines // TODO: actual intersection of stroke let inflated_quad = Quad::from_box([point - DVec2::splat(self.stroke_width / 2.), point + DVec2::splat(self.stroke_width / 2.)]); self.intersect_rectangle(inflated_quad, layer_transform) } } /// Mutable state used whilst rendering to an SVG pub struct SvgRender { pub svg: SvgSegmentList, pub svg_defs: String, pub transform: DAffine2, pub image_data: Vec<(u64, Image)>, indent: usize, } impl SvgRender { pub fn new() -> Self { Self { svg: SvgSegmentList::default(), svg_defs: String::new(), transform: DAffine2::IDENTITY, image_data: Vec::new(), indent: 0, } } pub fn indent(&mut self) { self.svg.push("\n"); self.svg.push("\t".repeat(self.indent)); } /// Add an outer `` tag with a `viewBox` and the `` pub fn format_svg(&mut self, bounds_min: DVec2, bounds_max: DVec2) { let (x, y) = bounds_min.into(); let (size_x, size_y) = (bounds_max - bounds_min).into(); let defs = &self.svg_defs; let svg_header = format!(r#"{defs}"#,); self.svg.insert(0, svg_header.into()); self.svg.push(""); } pub fn leaf_tag(&mut self, name: impl Into, attributes: impl FnOnce(&mut SvgRenderAttrs)) { self.indent(); self.svg.push("<"); self.svg.push(name); attributes(&mut SvgRenderAttrs(self)); self.svg.push("/>"); } pub fn parent_tag(&mut self, name: impl Into, attributes: impl FnOnce(&mut SvgRenderAttrs), inner: impl FnOnce(&mut Self)) { let name = name.into(); self.indent(); self.svg.push("<"); self.svg.push(name.clone()); attributes(&mut SvgRenderAttrs(self)); self.svg.push(">"); let length = self.svg.len(); self.indent += 1; inner(self); self.indent -= 1; if self.svg.len() != length { self.indent(); self.svg.push(""); } else { self.svg.pop(); self.svg.push("/>"); } } } impl Default for SvgRender { fn default() -> Self { Self::new() } } pub enum ImageRenderMode { BlobUrl, Canvas, Base64, } /// Static state used whilst rendering pub struct RenderParams { pub view_mode: crate::vector::style::ViewMode, pub image_render_mode: ImageRenderMode, pub culling_bounds: Option<[DVec2; 2]>, pub thumbnail: bool, } impl RenderParams { pub fn new(view_mode: crate::vector::style::ViewMode, image_render_mode: ImageRenderMode, culling_bounds: Option<[DVec2; 2]>, thumbnail: bool) -> Self { Self { view_mode, image_render_mode, culling_bounds, thumbnail, } } } pub fn format_transform_matrix(transform: DAffine2) -> String { use std::fmt::Write; let mut result = "matrix(".to_string(); let cols = transform.to_cols_array(); for (index, item) in cols.iter().enumerate() { write!(result, "{}", item).unwrap(); if index != cols.len() - 1 { result.push_str(", "); } } result.push(')'); result } pub trait GraphicElementRendered { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams); fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]>; fn add_click_targets(&self, click_targets: &mut Vec); } impl GraphicElementRendered for GraphicGroup { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { self.iter().for_each(|element| element.graphic_element_data.render_svg(render, render_params)) } fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { self.iter().filter_map(|element| element.graphic_element_data.bounding_box(transform)).reduce(Quad::combine_bounds) } fn add_click_targets(&self, _click_targets: &mut Vec) {} } impl GraphicElementRendered for VectorData { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { let layer_bounds = self.bounding_box().unwrap_or_default(); let transformed_bounds = self.bounding_box_with_transform(render.transform).unwrap_or_default(); let mut path = String::new(); for subpath in &self.subpaths { let _ = subpath.subpath_to_svg(&mut path, self.transform * render.transform); } render.leaf_tag("path", |attributes| { attributes.push("class", "vector-data"); attributes.push("d", path); let render = &mut attributes.0; let style = self.style.render(render_params.view_mode, &mut render.svg_defs, render.transform, layer_bounds, transformed_bounds); attributes.push_val(style); }); } fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { self.bounding_box_with_transform(self.transform * transform) } fn add_click_targets(&self, click_targets: &mut Vec) { let stroke_width = self.style.stroke().as_ref().map_or(0., crate::vector::style::Stroke::weight); let update_closed = |mut subpath: bezier_rs::Subpath| { subpath.set_closed(self.style.fill().is_some()); subpath }; click_targets.extend(self.subpaths.iter().cloned().map(update_closed).map(|subpath| ClickTarget { stroke_width, subpath })) } } impl GraphicElementRendered for Artboard { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { // Background render.leaf_tag("rect", |attributes| { attributes.push("class", "artboard-bg"); attributes.push("fill", format!("#{}", self.background.rgba_hex())); attributes.push("x", self.location.x.min(self.location.x + self.dimensions.x).to_string()); attributes.push("y", self.location.y.min(self.location.y + self.dimensions.y).to_string()); attributes.push("width", self.dimensions.x.abs().to_string()); attributes.push("height", self.dimensions.y.abs().to_string()); }); // Label render.parent_tag( "text", |attributes| { attributes.push("class", "artboard-label"); attributes.push("fill", "white"); attributes.push("x", (self.location.x.min(self.location.x + self.dimensions.x)).to_string()); attributes.push("y", (self.location.y.min(self.location.y + self.dimensions.y) - 4).to_string()); attributes.push("font-size", "14px"); }, |render| { render.svg.push("Artboard"); }, ); // Contents group render.parent_tag( "g", |attributes| { attributes.push("class", "artboard"); if self.clip { let id = format!("artboard-{}", generate_uuid()); let selector = format!("url(#{id})"); use std::fmt::Write; write!( &mut attributes.0.svg_defs, r##""##, self.location.x, self.location.y, self.dimensions.x, self.dimensions.y ) .unwrap(); attributes.push("clip-path", selector); } }, |render| { // Contents self.graphic_group.render_svg(render, render_params); }, ); } fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { let artboard_bounds = (transform * Quad::from_box([self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()])).bounding_box(); if self.clip { Some(artboard_bounds) } else { [self.graphic_group.bounding_box(transform), Some(artboard_bounds)].into_iter().flatten().reduce(Quad::combine_bounds) } } fn add_click_targets(&self, click_targets: &mut Vec) { let subpath = Subpath::new_rect(self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()); click_targets.push(ClickTarget { stroke_width: 0., subpath }); } } impl GraphicElementRendered for ImageFrame { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { let transform: String = format_transform_matrix(self.transform * render.transform); let uuid = generate_uuid(); match render_params.image_render_mode { ImageRenderMode::BlobUrl => { render.leaf_tag("image", move |attributes| { attributes.push("width", 1.to_string()); attributes.push("height", 1.to_string()); attributes.push("preserveAspectRatio", "none"); attributes.push("transform", transform); attributes.push("href", SvgSegment::BlobUrl(uuid)) }); render.image_data.push((uuid, self.image.clone())) } ImageRenderMode::Base64 => { let image = &self.image; if image.data.is_empty() { return; } let (flat_data, _, _) = image.clone().into_flat_u8(); let mut output = Vec::new(); let encoder = image::codecs::png::PngEncoder::new(&mut output); encoder .write_image(&flat_data, image.width, image.height, image::ColorType::Rgba8) .expect("failed to encode image as png"); let preamble = "data:image/png;base64,"; let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4); base64_string.push_str(preamble); base64::engine::general_purpose::STANDARD.encode_string(output, &mut base64_string); render.leaf_tag("image", |attributes| { attributes.push("width", 1.to_string()); attributes.push("height", 1.to_string()); attributes.push("preserveAspectRatio", "none"); attributes.push("transform", transform); attributes.push("href", base64_string) }); } ImageRenderMode::Canvas => { todo!("Canvas rendering is not yet implemented") } } } fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { let transform = self.transform * transform; (transform.matrix2 != glam::DMat2::ZERO).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box()) } fn add_click_targets(&self, click_targets: &mut Vec) { let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE); click_targets.push(ClickTarget { subpath, stroke_width: 0. }); } } impl GraphicElementRendered for GraphicElementData { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { match self { GraphicElementData::VectorShape(vector_data) => vector_data.render_svg(render, render_params), GraphicElementData::ImageFrame(image_frame) => image_frame.render_svg(render, render_params), GraphicElementData::Text(_) => todo!("Render a text GraphicElementData"), GraphicElementData::GraphicGroup(graphic_group) => graphic_group.render_svg(render, render_params), GraphicElementData::Artboard(artboard) => artboard.render_svg(render, render_params), } } fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { match self { GraphicElementData::VectorShape(vector_data) => GraphicElementRendered::bounding_box(&**vector_data, transform), GraphicElementData::ImageFrame(image_frame) => image_frame.bounding_box(transform), GraphicElementData::Text(_) => todo!("Bounds of a text GraphicElementData"), GraphicElementData::GraphicGroup(graphic_group) => graphic_group.bounding_box(transform), GraphicElementData::Artboard(artboard) => artboard.bounding_box(transform), } } fn add_click_targets(&self, click_targets: &mut Vec) { match self { GraphicElementData::VectorShape(vector_data) => vector_data.add_click_targets(click_targets), GraphicElementData::ImageFrame(image_frame) => image_frame.add_click_targets(click_targets), GraphicElementData::Text(_) => todo!("click target for text GraphicElementData"), GraphicElementData::GraphicGroup(graphic_group) => graphic_group.add_click_targets(click_targets), GraphicElementData::Artboard(artboard) => artboard.add_click_targets(click_targets), } } } /// A segment of an svg string to allow for embedding blob urls #[derive(Debug, Clone, PartialEq, Eq)] pub enum SvgSegment { Slice(&'static str), String(String), BlobUrl(u64), } impl From for SvgSegment { fn from(value: String) -> Self { Self::String(value) } } impl From<&'static str> for SvgSegment { fn from(value: &'static str) -> Self { Self::Slice(value) } } /// A list of [`SvgSegment`]s. /// /// Can be modified with `list.push("hello".into())`. Use `list.to_string()` to convert the segments into one string. #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct SvgSegmentList(Vec); impl core::ops::Deref for SvgSegmentList { type Target = Vec; fn deref(&self) -> &Self::Target { &self.0 } } impl core::ops::DerefMut for SvgSegmentList { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl core::fmt::Display for SvgSegmentList { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { for segment in self.iter() { f.write_str(match segment { SvgSegment::Slice(x) => x, SvgSegment::String(x) => x, SvgSegment::BlobUrl(_) => "", })?; } Ok(()) } } pub struct SvgRenderAttrs<'a>(&'a mut SvgRender); impl<'a> SvgRenderAttrs<'a> { pub fn push_complex(&mut self, name: impl Into, value: impl FnOnce(&mut SvgRender)) { self.0.svg.push(" "); self.0.svg.push(name); self.0.svg.push("=\""); value(self.0); self.0.svg.push("\""); } pub fn push(&mut self, name: impl Into, value: impl Into) { self.push_complex(name, move |renderer| renderer.svg.push(value)); } pub fn push_val(&mut self, value: impl Into) { self.0.svg.push(value); } } impl SvgSegmentList { pub fn push(&mut self, value: impl Into) { self.0.push(value.into()); } }