From 020f700c92521a2e8920b014a06c8c1ce09d7aa8 Mon Sep 17 00:00:00 2001 From: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Thu, 30 Jun 2022 02:18:01 +0100 Subject: [PATCH] Image and text bug fixes (#685) * Image and text bugfixes * Mark only the required layer types as dirty * Fix doctest * Disable selection if empty * Cleanup naming * Simplify cache deleting on export * Minor css style change * Nit Co-authored-by: Keavon Chambers --- Cargo.lock | 7 +++ editor/src/dialog/dialog_message_handler.rs | 1 + editor/src/dialog/dialogs/export_dialog.rs | 13 ++-- .../src/document/artboard_message_handler.rs | 5 +- .../src/document/document_message_handler.rs | 46 ++++++++------ editor/src/document/layer_panel.rs | 42 ++----------- .../src/document/overlays_message_handler.rs | 5 +- .../src/document/portfolio_message_handler.rs | 15 ++--- .../properties_panel_message_handler.rs | 3 +- editor/src/frontend/utility_types.rs | 1 + editor/src/layout/widgets.rs | 1 + .../components/floating-menus/MenuList.vue | 25 ++++++-- frontend/src/state-providers/portfolio.ts | 2 +- graphene/Cargo.toml | 1 + graphene/src/document.rs | 31 ++++++++-- graphene/src/layers/folder_layer.rs | 6 +- graphene/src/layers/image_layer.rs | 22 ++++--- .../src/layers/image_layer/base64_serde.rs | 21 +++++++ graphene/src/layers/layer_info.rs | 60 +++++++++++++++---- graphene/src/layers/shape_layer.rs | 8 +-- graphene/src/layers/style/mod.rs | 21 +++++++ graphene/src/layers/text_layer.rs | 35 +++++------ 22 files changed, 243 insertions(+), 128 deletions(-) create mode 100644 graphene/src/layers/image_layer/base64_serde.rs diff --git a/Cargo.lock b/Cargo.lock index 5e6c0847..f6f9767c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,6 +34,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + [[package]] name = "bezier-rs" version = "0.0.0" @@ -154,6 +160,7 @@ dependencies = [ name = "graphite-graphene" version = "0.0.0" dependencies = [ + "base64", "glam", "kurbo", "log", diff --git a/editor/src/dialog/dialog_message_handler.rs b/editor/src/dialog/dialog_message_handler.rs index f3b13e41..ff592c22 100644 --- a/editor/src/dialog/dialog_message_handler.rs +++ b/editor/src/dialog/dialog_message_handler.rs @@ -79,6 +79,7 @@ impl MessageHandler for DialogMessageHa file_name: portfolio.active_document().name.clone(), scale_factor: 1., artboards, + has_selection: portfolio.active_document().selected_layers().next().is_some(), ..Default::default() }; self.export_dialog.register_properties(responses, LayoutTarget::DialogDetails); diff --git a/editor/src/dialog/dialogs/export_dialog.rs b/editor/src/dialog/dialogs/export_dialog.rs index e792992f..39ec9eb8 100644 --- a/editor/src/dialog/dialogs/export_dialog.rs +++ b/editor/src/dialog/dialogs/export_dialog.rs @@ -15,6 +15,7 @@ pub struct Export { pub scale_factor: f64, pub bounds: ExportBounds, pub artboards: HashMap, + pub has_selection: bool, } impl PropertyHolder for Export { @@ -60,15 +61,19 @@ impl PropertyHolder for Export { })), ]; - let artboards = self.artboards.iter().map(|(&val, name)| (ExportBounds::Artboard(val), name.to_string())); - let mut export_area_options = vec![(ExportBounds::AllArtwork, "All Artwork".to_string())]; + let artboards = self.artboards.iter().map(|(&val, name)| (ExportBounds::Artboard(val), name.to_string(), false)); + let mut export_area_options = vec![ + (ExportBounds::AllArtwork, "All Artwork".to_string(), false), + (ExportBounds::Selection, "Selection".to_string(), !self.has_selection), + ]; export_area_options.extend(artboards); - let index = export_area_options.iter().position(|(val, _)| val == &self.bounds).unwrap(); + let index = export_area_options.iter().position(|(val, _, _)| val == &self.bounds).unwrap(); let entries = vec![export_area_options .into_iter() - .map(|(val, name)| DropdownEntryData { + .map(|(val, name, disabled)| DropdownEntryData { label: name, on_update: WidgetCallback::new(move |_| ExportDialogUpdate::ExportBounds(val).into()), + disabled, ..Default::default() }) .collect()]; diff --git a/editor/src/document/artboard_message_handler.rs b/editor/src/document/artboard_message_handler.rs index b4ca9886..044aed1b 100644 --- a/editor/src/document/artboard_message_handler.rs +++ b/editor/src/document/artboard_message_handler.rs @@ -2,7 +2,7 @@ use crate::message_prelude::*; use graphene::color::Color; use graphene::document::Document as GrapheneDocument; -use graphene::layers::style::{self, Fill, ViewMode}; +use graphene::layers::style::{self, Fill, RenderData, ViewMode}; use graphene::layers::text_layer::FontCache; use graphene::DocumentResponse; use graphene::Operation as DocumentOperation; @@ -85,9 +85,10 @@ impl MessageHandler for ArtboardMessageHandler { .into(), ) } else { + let render_data = RenderData::new(ViewMode::Normal, font_cache, None, false); responses.push_back( FrontendMessage::UpdateDocumentArtboards { - svg: self.artboards_graphene_document.render_root(ViewMode::Normal, font_cache, None), + svg: self.artboards_graphene_document.render_root(render_data), } .into(), ); diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index cc5e4488..ef40f5b7 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -1,5 +1,5 @@ use super::clipboards::Clipboard; -use super::layer_panel::{layer_panel_entry, LayerDataTypeDiscriminant, LayerMetadata, LayerPanelEntry, RawBuffer}; +use super::layer_panel::{layer_panel_entry, LayerMetadata, LayerPanelEntry, RawBuffer}; use super::properties_panel_message_handler::PropertiesPanelMessageHandlerData; use super::utility_types::{AlignAggregate, AlignAxis, DocumentSave, FlipAxis}; use super::utility_types::{DocumentMode, TargetDocument}; @@ -21,16 +21,15 @@ use graphene::color::Color; use graphene::document::Document as GrapheneDocument; use graphene::layers::blend_mode::BlendMode; use graphene::layers::folder_layer::FolderLayer; -use graphene::layers::layer_info::LayerDataType; -use graphene::layers::style::{Fill, ViewMode}; -use graphene::layers::text_layer::FontCache; +use graphene::layers::layer_info::{LayerDataType, LayerDataTypeDiscriminant}; +use graphene::layers::style::{Fill, RenderData, ViewMode}; +use graphene::layers::text_layer::{Font, FontCache}; use graphene::{DocumentError, DocumentResponse, LayerId, Operation as DocumentOperation}; use glam::{DAffine2, DVec2}; use log::warn; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::collections::VecDeque; +use std::collections::{HashMap, HashSet, VecDeque}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DocumentMessageHandler { @@ -491,18 +490,20 @@ impl DocumentMessageHandler { path } - /// Creates the blob URLs for the image data in the document - pub fn load_image_data(&self, responses: &mut VecDeque, root: &LayerDataType, mut path: Vec) { - let mut image_data = Vec::new(); - fn walk_layers(data: &LayerDataType, path: &mut Vec, image_data: &mut Vec) { + /// Loads layer resources such as creating the blob URLs for the images and loading all of the fonts in the document + pub fn load_layer_resources(&self, responses: &mut VecDeque, root: &LayerDataType, mut path: Vec) { + fn walk_layers(data: &LayerDataType, path: &mut Vec, image_data: &mut Vec, fonts: &mut HashSet) { match data { LayerDataType::Folder(f) => { for (id, layer) in f.layer_ids.iter().zip(f.layers().iter()) { path.push(*id); - walk_layers(&layer.data, path, image_data); + walk_layers(&layer.data, path, image_data, fonts); path.pop(); } } + LayerDataType::Text(txt) => { + fonts.insert(txt.font.clone()); + } LayerDataType::Image(img) => image_data.push(FrontendImageData { path: path.clone(), image_data: img.image_data.clone(), @@ -512,10 +513,15 @@ impl DocumentMessageHandler { } } - walk_layers(root, &mut path, &mut image_data); + let mut image_data = Vec::new(); + let mut fonts = HashSet::new(); + walk_layers(root, &mut path, &mut image_data, &mut fonts); if !image_data.is_empty() { responses.push_front(FrontendMessage::UpdateImageData { image_data }.into()); } + for font in fonts { + responses.push_front(FrontendMessage::TriggerFontLoad { font, is_default: false }.into()); + } } pub fn update_document_widgets(&self, responses: &mut VecDeque) { @@ -1040,12 +1046,14 @@ impl MessageHandler self.all_layer_bounds(font_cache), - crate::frontend::utility_types::ExportBounds::Artboard(id) => self + ExportBounds::AllArtwork => self.all_layer_bounds(font_cache), + ExportBounds::Selection => self.selected_visible_layers_bounding_box(font_cache), + ExportBounds::Artboard(id) => self .artboard_message_handler .artboards_graphene_document .layer(&[id]) @@ -1061,14 +1069,15 @@ impl MessageHandler file_name + file_suffix, }; - let rendered = self.graphene_document.render_root(self.view_mode, font_cache, None); + let render_data = RenderData::new(self.view_mode, font_cache, None, true); + let rendered = self.graphene_document.render_root(render_data); let document = format!( r#"{}{}"#, bbox[0].x, bbox[0].y, size.x, size.y, size.x, size.y, "\n", rendered ); self.graphene_document.root.transform = old_transform; - self.graphene_document.root.cache_dirty = true; + GrapheneDocument::mark_children_as_dirty(&mut self.graphene_document.root); if file_type == FileType::Svg { responses.push_back(FrontendMessage::TriggerFileDownload { document, name }.into()); @@ -1218,9 +1227,10 @@ impl MessageHandler responses.push_back(DocumentOperation::RenameLayer { layer_path, new_name }.into()), RenderDocument => { + let render_data = RenderData::new(self.view_mode, font_cache, Some(ipp.document_bounds()), false); responses.push_back( FrontendMessage::UpdateDocumentArtwork { - svg: self.graphene_document.render_root(self.view_mode, font_cache, Some(ipp.document_bounds())), + svg: self.graphene_document.render_root(render_data), } .into(), ); diff --git a/editor/src/document/layer_panel.rs b/editor/src/document/layer_panel.rs index b96cb5c7..15943ab3 100644 --- a/editor/src/document/layer_panel.rs +++ b/editor/src/document/layer_panel.rs @@ -1,12 +1,11 @@ -use graphene::layers::layer_info::{Layer, LayerData, LayerDataType}; -use graphene::layers::style::ViewMode; +use graphene::layers::layer_info::{Layer, LayerData, LayerDataTypeDiscriminant}; +use graphene::layers::style::{RenderData, ViewMode}; use graphene::layers::text_layer::FontCache; use graphene::LayerId; use glam::{DAffine2, DVec2}; use serde::ser::SerializeStruct; use serde::{Deserialize, Serialize}; -use std::fmt; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Copy)] pub struct LayerMetadata { @@ -27,7 +26,8 @@ pub fn layer_panel_entry(layer_metadata: &LayerMetadata, transform: DAffine2, la let mut thumbnail = String::new(); let mut svg_defs = String::new(); - layer.data.clone().render(&mut thumbnail, &mut svg_defs, &mut vec![transform], ViewMode::Normal, font_cache, None); + let render_data = RenderData::new(ViewMode::Normal, font_cache, None, false); + layer.data.clone().render(&mut thumbnail, &mut svg_defs, &mut vec![transform], render_data); let transform = transform.to_cols_array().iter().map(ToString::to_string).collect::>().join(","); let thumbnail = if let [(x_min, y_min), (x_max, y_max)] = arr.as_slice() { format!( @@ -90,37 +90,3 @@ pub struct LayerPanelEntry { pub path: Vec, pub thumbnail: String, } - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub enum LayerDataTypeDiscriminant { - Folder, - Shape, - Text, - Image, -} - -impl fmt::Display for LayerDataTypeDiscriminant { - fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - let name = match self { - LayerDataTypeDiscriminant::Folder => "Folder", - LayerDataTypeDiscriminant::Shape => "Shape", - LayerDataTypeDiscriminant::Text => "Text", - LayerDataTypeDiscriminant::Image => "Image", - }; - - formatter.write_str(name) - } -} - -impl From<&LayerDataType> for LayerDataTypeDiscriminant { - fn from(data: &LayerDataType) -> Self { - use LayerDataType::*; - - match data { - Folder(_) => LayerDataTypeDiscriminant::Folder, - Shape(_) => LayerDataTypeDiscriminant::Shape, - Text(_) => LayerDataTypeDiscriminant::Text, - Image(_) => LayerDataTypeDiscriminant::Image, - } - } -} diff --git a/editor/src/document/overlays_message_handler.rs b/editor/src/document/overlays_message_handler.rs index cd4adc33..b0a33d61 100644 --- a/editor/src/document/overlays_message_handler.rs +++ b/editor/src/document/overlays_message_handler.rs @@ -2,7 +2,7 @@ use crate::input::InputPreprocessorMessageHandler; use crate::message_prelude::*; use graphene::document::Document as GrapheneDocument; -use graphene::layers::style::ViewMode; +use graphene::layers::style::{RenderData, ViewMode}; use graphene::layers::text_layer::FontCache; #[derive(Debug, Clone, Default)] @@ -32,7 +32,8 @@ impl MessageHandler DEFAULT_DOCUMENT_NAME.to_string(), _ => format!("{} {}", DEFAULT_DOCUMENT_NAME, new_doc_title_num), - }; - name + } } // TODO Fix how this doesn't preserve tab order upon loading new document from *File > Load* @@ -81,7 +81,7 @@ impl PortfolioMessageHandler { ); new_document.update_layer_tree_options_bar_widgets(responses, &self.font_cache); - new_document.load_image_data(responses, &new_document.graphene_document.root.data, Vec::new()); + new_document.load_layer_resources(responses, &new_document.graphene_document.root.data, Vec::new()); self.documents.insert(document_id, new_document); @@ -287,7 +287,8 @@ impl MessageHandler for Port is_default, } => { self.font_cache.insert(Font::new(font_family, font_style), preview_url, data, is_default); - responses.push_back(DocumentMessage::DirtyRenderDocument.into()); + self.active_document_mut().graphene_document.mark_all_layers_of_type_as_dirty(LayerDataTypeDiscriminant::Text); + responses.push_back(DocumentMessage::RenderDocument.into()); } LoadFont { font, is_default } => { if !self.font_cache.loaded_font(&font) { @@ -387,7 +388,7 @@ impl MessageHandler for Port } .into(), ); - self.active_document().load_image_data(responses, &entry.layer.data, destination_path.clone()); + self.active_document().load_layer_resources(responses, &entry.layer.data, destination_path.clone()); responses.push_front( DocumentOperation::InsertLayer { layer: entry.layer.clone(), @@ -428,7 +429,7 @@ impl MessageHandler for Port } .into(), ); - self.active_document().load_image_data(responses, &entry.layer.data, destination_path.clone()); + self.active_document().load_layer_resources(responses, &entry.layer.data, destination_path.clone()); responses.push_front( DocumentOperation::InsertLayer { layer: entry.layer.clone(), diff --git a/editor/src/document/properties_panel_message_handler.rs b/editor/src/document/properties_panel_message_handler.rs index 7b07cd96..0470c927 100644 --- a/editor/src/document/properties_panel_message_handler.rs +++ b/editor/src/document/properties_panel_message_handler.rs @@ -1,4 +1,3 @@ -use super::layer_panel::LayerDataTypeDiscriminant; use super::utility_types::TargetDocument; use crate::document::properties_panel_message::TransformOp; use crate::layout::layout_message::LayoutTarget; @@ -10,7 +9,7 @@ use crate::message_prelude::*; use graphene::color::Color; use graphene::document::Document as GrapheneDocument; -use graphene::layers::layer_info::{Layer, LayerDataType}; +use graphene::layers::layer_info::{Layer, LayerDataType, LayerDataTypeDiscriminant}; use graphene::layers::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke}; use graphene::layers::text_layer::{FontCache, TextLayer}; use graphene::{LayerId, Operation}; diff --git a/editor/src/frontend/utility_types.rs b/editor/src/frontend/utility_types.rs index 4da11123..618e2dca 100644 --- a/editor/src/frontend/utility_types.rs +++ b/editor/src/frontend/utility_types.rs @@ -61,6 +61,7 @@ impl FileType { #[derive(Clone, Copy, Debug, Eq, Deserialize, PartialEq, Serialize)] pub enum ExportBounds { AllArtwork, + Selection, Artboard(LayerId), } diff --git a/editor/src/layout/widgets.rs b/editor/src/layout/widgets.rs index de8b04fd..3e5adfc2 100644 --- a/editor/src/layout/widgets.rs +++ b/editor/src/layout/widgets.rs @@ -522,6 +522,7 @@ pub struct DropdownEntryData { pub shortcut: Vec, #[serde(rename = "shortcutRequiresLock")] pub shortcut_requires_lock: bool, + pub disabled: bool, pub children: Vec>, #[serde(skip)] diff --git a/frontend/src/components/floating-menus/MenuList.vue b/frontend/src/components/floating-menus/MenuList.vue index f50ec30d..2ad8b426 100644 --- a/frontend/src/components/floating-menus/MenuList.vue +++ b/frontend/src/components/floating-menus/MenuList.vue @@ -20,11 +20,11 @@ v-for="(entry, entryIndex) in virtualScrollingEntryHeight ? section.slice(virtualScrollingStartIndex, virtualScrollingEndIndex) : section" :key="entryIndex + (virtualScrollingEntryHeight ? virtualScrollingStartIndex : 0)" class="row" - :class="{ open: isEntryOpen(entry), active: entry.label === highlighted?.label }" + :class="{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: entry.disabled }" :style="{ height: virtualScrollingEntryHeight || '20px' }" - @click="() => onEntryClick(entry)" - @pointerenter="() => onEntryPointerEnter(entry)" - @pointerleave="() => onEntryPointerLeave(entry)" + @click="() => !entry.disabled && onEntryClick(entry)" + @pointerenter="() => !entry.disabled && onEntryPointerEnter(entry)" + @pointerleave="() => !entry.disabled && onEntryPointerLeave(entry)" >
@@ -141,6 +141,20 @@ color: var(--color-f-white); } } + + &.disabled { + &:hover { + background: none; + } + + span { + color: var(--color-8-uppergray); + } + + svg { + fill: var(--color-8-uppergray); + } + } } } } @@ -168,6 +182,7 @@ interface MenuListEntryData { font?: URL; shortcut?: string[]; shortcutRequiresLock?: boolean; + disabled?: boolean; action?: () => void; children?: SectionsOfMenuListEntries; } @@ -256,7 +271,7 @@ const MenuList = defineComponent({ if (this.interactive) this.highlighted = this.activeEntry; const menuOpen = this.isOpen; - const flatEntries = this.entries.flat(); + const flatEntries = this.entries.flat().filter((entry) => !entry.disabled); const openChild = flatEntries.findIndex((entry) => entry.children?.length && entry.ref?.isOpen); const openSubmenu = (highlighted: MenuListEntry): void => { diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index 1c58ea56..1cc5e77b 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -40,7 +40,7 @@ export function createPortfolioState(editor: Editor) { if (!context) return; // Fill the canvas with white if jpeg (does not support transparency and defaults to black) - if (triggerRasterDownload.mime.endsWith("jpg")) { + if (triggerRasterDownload.mime.endsWith("jpeg")) { context.fillStyle = "white"; context.fillRect(0, 0, triggerRasterDownload.size.x, triggerRasterDownload.size.y); } diff --git a/graphene/Cargo.toml b/graphene/Cargo.toml index 4ceb21e9..c7223f00 100644 --- a/graphene/Cargo.toml +++ b/graphene/Cargo.toml @@ -17,6 +17,7 @@ kurbo = { git = "https://github.com/linebender/kurbo.git", features = [ "serde", ] } serde = { version = "1.0", features = ["derive"] } +base64 = "0.13" glam = { version = "0.17", features = ["serde"] } # Font rendering diff --git a/graphene/src/document.rs b/graphene/src/document.rs index 7304500b..ab0fe734 100644 --- a/graphene/src/document.rs +++ b/graphene/src/document.rs @@ -3,9 +3,9 @@ use crate::intersection::Quad; use crate::layers; use crate::layers::folder_layer::FolderLayer; use crate::layers::image_layer::ImageLayer; -use crate::layers::layer_info::{Layer, LayerData, LayerDataType}; +use crate::layers::layer_info::{Layer, LayerData, LayerDataType, LayerDataTypeDiscriminant}; use crate::layers::shape_layer::ShapeLayer; -use crate::layers::style::ViewMode; +use crate::layers::style::RenderData; use crate::layers::text_layer::{Font, FontCache, TextLayer}; use crate::{DocumentError, DocumentResponse, Operation}; @@ -42,10 +42,10 @@ impl Default for Document { impl Document { /// Wrapper around render, that returns the whole document as a Response. - pub fn render_root(&mut self, mode: ViewMode, font_cache: &FontCache, culling_bounds: Option<[DVec2; 2]>) -> String { + pub fn render_root(&mut self, render_data: RenderData) -> String { let mut svg_defs = String::from(""); - self.root.render(&mut vec![], mode, &mut svg_defs, font_cache, culling_bounds); + self.root.render(&mut vec![], &mut svg_defs, render_data); svg_defs.push_str(""); @@ -375,6 +375,27 @@ impl Document { Ok(()) } + /// Marks all decendants of the specified [Layer] of a specific [LayerDataType] as dirty + fn mark_layers_of_type_as_dirty(root: &mut Layer, data_type: LayerDataTypeDiscriminant) -> bool { + if let LayerDataType::Folder(folder) = &mut root.data { + let mut dirty = false; + for layer in folder.layers_mut() { + dirty = Self::mark_layers_of_type_as_dirty(layer, data_type) || dirty; + } + root.cache_dirty = dirty; + } + if LayerDataTypeDiscriminant::from(&root.data) == data_type { + root.cache_dirty = true; + } + + root.cache_dirty + } + + /// Marks all layers in the [Document] of a specific [LayerDataType] as dirty + pub fn mark_all_layers_of_type_as_dirty(&mut self, data_type: LayerDataTypeDiscriminant) -> bool { + Self::mark_layers_of_type_as_dirty(&mut self.root, data_type) + } + pub fn transforms(&self, path: &[LayerId]) -> Result, DocumentError> { let mut root = &self.root; let mut transforms = vec![self.root.transform]; @@ -697,7 +718,7 @@ impl Document { text.font = Font::new(font_family, font_style); text.size = size; - text.regenerate_path(text.load_face(font_cache)); + text.cached_path = Some(text.generate_path(text.load_face(font_cache))); self.mark_as_dirty(&path)?; Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) } diff --git a/graphene/src/layers/folder_layer.rs b/graphene/src/layers/folder_layer.rs index 6bbdd0b1..19eba77b 100644 --- a/graphene/src/layers/folder_layer.rs +++ b/graphene/src/layers/folder_layer.rs @@ -1,5 +1,5 @@ use super::layer_info::{Layer, LayerData, LayerDataType}; -use super::style::ViewMode; +use super::style::RenderData; use crate::intersection::Quad; use crate::layers::text_layer::FontCache; use crate::{DocumentError, LayerId}; @@ -22,9 +22,9 @@ pub struct FolderLayer { } impl LayerData for FolderLayer { - fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec, view_mode: ViewMode, font_cache: &FontCache, culling_bounds: Option<[DVec2; 2]>) { + fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec, render_data: RenderData) { for layer in &mut self.layers { - let _ = writeln!(svg, "{}", layer.render(transforms, view_mode, svg_defs, font_cache, culling_bounds)); + let _ = writeln!(svg, "{}", layer.render(transforms, svg_defs, render_data)); } } diff --git a/graphene/src/layers/image_layer.rs b/graphene/src/layers/image_layer.rs index df023da6..180ec77f 100644 --- a/graphene/src/layers/image_layer.rs +++ b/graphene/src/layers/image_layer.rs @@ -1,5 +1,5 @@ use super::layer_info::LayerData; -use super::style::ViewMode; +use super::style::{RenderData, ViewMode}; use crate::intersection::{intersect_quad_bez_path, Quad}; use crate::layers::text_layer::FontCache; use crate::LayerId; @@ -9,6 +9,8 @@ use kurbo::{Affine, BezPath, Shape as KurboShape}; use serde::{Deserialize, Serialize}; use std::fmt::Write; +mod base64_serde; + fn glam_to_kurbo(transform: DAffine2) -> Affine { Affine::new(transform.to_cols_array()) } @@ -16,6 +18,7 @@ fn glam_to_kurbo(transform: DAffine2) -> Affine { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ImageLayer { pub mime: String, + #[serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64")] pub image_data: Vec, #[serde(skip)] pub blob_url: Option, @@ -24,8 +27,8 @@ pub struct ImageLayer { } impl LayerData for ImageLayer { - fn render(&mut self, svg: &mut String, _svg_defs: &mut String, transforms: &mut Vec, view_mode: ViewMode, _font_cache: &FontCache, _culling_bounds: Option<[DVec2; 2]>) { - let transform = self.transform(transforms, view_mode); + fn render(&mut self, svg: &mut String, _svg_defs: &mut String, transforms: &mut Vec, render_data: RenderData) { + let transform = self.transform(transforms, render_data.view_mode); let inverse = transform.inverse(); if !inverse.is_finite() { @@ -47,12 +50,15 @@ impl LayerData for ImageLayer { .collect::(); let _ = write!( svg, - r#""#, - self.dimensions.x, - self.dimensions.y, - svg_transform, - self.blob_url.as_ref().unwrap_or(&String::new()) + r#""#); let _ = svg.write_str(""); } diff --git a/graphene/src/layers/image_layer/base64_serde.rs b/graphene/src/layers/image_layer/base64_serde.rs new file mode 100644 index 00000000..b131822d --- /dev/null +++ b/graphene/src/layers/image_layer/base64_serde.rs @@ -0,0 +1,21 @@ +//! Basic wrapper for [`serde`] for [`base64`] encoding + +use serde::{Deserialize, Deserializer, Serializer}; + +pub fn as_base64(key: &T, serializer: S) -> Result +where + T: AsRef<[u8]>, + S: Serializer, +{ + serializer.serialize_str(&base64::encode(key.as_ref())) +} + +pub fn from_base64<'a, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'a>, +{ + use serde::de::Error; + String::deserialize(deserializer) + .and_then(|string| base64::decode(&string).map_err(|err| Error::custom(err.to_string()))) + .map_err(serde::de::Error::custom) +} diff --git a/graphene/src/layers/layer_info.rs b/graphene/src/layers/layer_info.rs index 2b605fd4..3b5a5348 100644 --- a/graphene/src/layers/layer_info.rs +++ b/graphene/src/layers/layer_info.rs @@ -2,13 +2,14 @@ use super::blend_mode::BlendMode; use super::folder_layer::FolderLayer; use super::image_layer::ImageLayer; use super::shape_layer::ShapeLayer; -use super::style::{PathStyle, ViewMode}; +use super::style::{PathStyle, RenderData}; use super::text_layer::TextLayer; use crate::intersection::Quad; use crate::layers::text_layer::FontCache; use crate::DocumentError; use crate::LayerId; +use core::fmt; use glam::{DAffine2, DMat2, DVec2}; use serde::{Deserialize, Serialize}; use std::fmt::Write; @@ -46,6 +47,40 @@ impl LayerDataType { } } +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum LayerDataTypeDiscriminant { + Folder, + Shape, + Text, + Image, +} + +impl fmt::Display for LayerDataTypeDiscriminant { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + let name = match self { + LayerDataTypeDiscriminant::Folder => "Folder", + LayerDataTypeDiscriminant::Shape => "Shape", + LayerDataTypeDiscriminant::Text => "Text", + LayerDataTypeDiscriminant::Image => "Image", + }; + + formatter.write_str(name) + } +} + +impl From<&LayerDataType> for LayerDataTypeDiscriminant { + fn from(data: &LayerDataType) -> Self { + use LayerDataType::*; + + match data { + Folder(_) => LayerDataTypeDiscriminant::Folder, + Shape(_) => LayerDataTypeDiscriminant::Shape, + Text(_) => LayerDataTypeDiscriminant::Text, + Image(_) => LayerDataTypeDiscriminant::Image, + } + } +} + /// Defines shared behavior for every layer type. pub trait LayerData { /// Render the layer as an SVG tag to a given string. @@ -53,7 +88,7 @@ pub trait LayerData { /// # Example /// ``` /// # use graphite_graphene::layers::shape_layer::ShapeLayer; - /// # use graphite_graphene::layers::style::{Fill, PathStyle, ViewMode}; + /// # use graphite_graphene::layers::style::{Fill, PathStyle, ViewMode, RenderData}; /// # use graphite_graphene::layers::layer_info::LayerData; /// # use std::collections::HashMap; /// @@ -61,7 +96,9 @@ pub trait LayerData { /// let mut svg = String::new(); /// /// // Render the shape without any transforms, in normal view mode - /// shape.render(&mut svg, &mut String::new(), &mut vec![], ViewMode::Normal, &Default::default(), None); + /// # let font_cache = Default::default(); + /// let render_data = RenderData::new(ViewMode::Normal, &font_cache, None, false); + /// shape.render(&mut svg, &mut String::new(), &mut vec![], render_data); /// /// assert_eq!( /// svg, @@ -70,7 +107,7 @@ pub trait LayerData { /// " /// ); /// ``` - fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec, view_mode: ViewMode, font_cache: &FontCache, culling_bounds: Option<[DVec2; 2]>); + fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec, render_data: RenderData); /// Determine the layers within this layer that intersect a given quad. /// # Example @@ -117,8 +154,8 @@ pub trait LayerData { } impl LayerData for LayerDataType { - fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec, view_mode: ViewMode, font_cache: &FontCache, viewport_bounds: Option<[DVec2; 2]>) { - self.inner_mut().render(svg, svg_defs, transforms, view_mode, font_cache, viewport_bounds) + fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec, render_data: RenderData) { + self.inner_mut().render(svg, svg_defs, transforms, render_data) } fn intersects_quad(&self, quad: Quad, path: &mut Vec, intersections: &mut Vec>, font_cache: &FontCache) { @@ -223,14 +260,17 @@ impl Layer { LayerIter { stack: vec![self] } } - pub fn render(&mut self, transforms: &mut Vec, view_mode: ViewMode, svg_defs: &mut String, font_cache: &FontCache, culling_bounds: Option<[DVec2; 2]>) -> &str { + pub fn render(&mut self, transforms: &mut Vec, svg_defs: &mut String, render_data: RenderData) -> &str { if !self.visible { return ""; } transforms.push(self.transform); - if let Some(viewport_bounds) = culling_bounds { - if let Some(bounding_box) = self.data.bounding_box(transforms.iter().cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY), font_cache) { + if let Some(viewport_bounds) = render_data.culling_bounds { + if let Some(bounding_box) = self + .data + .bounding_box(transforms.iter().cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY), render_data.font_cache) + { let is_overlapping = viewport_bounds[0].x < bounding_box[1].x && bounding_box[0].x < viewport_bounds[1].x && viewport_bounds[0].y < bounding_box[1].y && bounding_box[0].y < viewport_bounds[1].y; if !is_overlapping { @@ -245,7 +285,7 @@ impl Layer { if self.cache_dirty { self.thumbnail_cache.clear(); self.svg_defs_cache.clear(); - self.data.render(&mut self.thumbnail_cache, &mut self.svg_defs_cache, transforms, view_mode, font_cache, culling_bounds); + self.data.render(&mut self.thumbnail_cache, &mut self.svg_defs_cache, transforms, render_data); self.cache.clear(); let _ = writeln!(self.cache, r#", view_mode: ViewMode, _font_cache: &FontCache, _culling_bounds: Option<[DVec2; 2]>) { + fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec, render_data: RenderData) { let mut path = self.path.clone(); let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box(); let layer_bounds = [(x0, y0).into(), (x1, y1).into()]; - let transform = self.transform(transforms, view_mode); + let transform = self.transform(transforms, render_data.view_mode); let inverse = transform.inverse(); if !inverse.is_finite() { let _ = write!(svg, ""); @@ -58,7 +58,7 @@ impl LayerData for ShapeLayer { svg, r#""#, path.to_svg(), - self.style.render(view_mode, svg_defs, transform, layer_bounds, transformed_bounds) + self.style.render(render_data.view_mode, svg_defs, transform, layer_bounds, transformed_bounds) ); let _ = svg.write_str(""); } diff --git a/graphene/src/layers/style/mod.rs b/graphene/src/layers/style/mod.rs index 5d446715..e6052dc9 100644 --- a/graphene/src/layers/style/mod.rs +++ b/graphene/src/layers/style/mod.rs @@ -1,5 +1,6 @@ //! Contains stylistic options for SVG elements. +use super::text_layer::FontCache; use crate::color::Color; use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT}; @@ -36,6 +37,26 @@ impl Default for ViewMode { } } +/// Contains metadata for rendering the document as an svg +#[derive(Debug, Clone, Copy)] +pub struct RenderData<'a> { + pub view_mode: ViewMode, + pub font_cache: &'a FontCache, + pub culling_bounds: Option<[DVec2; 2]>, + pub embed_images: bool, +} + +impl<'a> RenderData<'a> { + pub fn new(view_mode: ViewMode, font_cache: &'a FontCache, culling_bounds: Option<[DVec2; 2]>, embed_images: bool) -> Self { + Self { + view_mode, + font_cache, + culling_bounds, + embed_images, + } + } +} + #[derive(PartialEq, Eq, Clone, Copy, Debug, Hash, Serialize, Deserialize)] pub enum GradientType { Linear, diff --git a/graphene/src/layers/text_layer.rs b/graphene/src/layers/text_layer.rs index 1829866c..2f436ed5 100644 --- a/graphene/src/layers/text_layer.rs +++ b/graphene/src/layers/text_layer.rs @@ -1,5 +1,5 @@ use super::layer_info::LayerData; -use super::style::{PathStyle, ViewMode}; +use super::style::{PathStyle, RenderData, ViewMode}; use crate::intersection::{intersect_quad_bez_path, Quad}; use crate::LayerId; pub use font_cache::{Font, FontCache}; @@ -33,12 +33,12 @@ pub struct TextLayer { #[serde(skip)] pub editable: bool, #[serde(skip)] - cached_path: Option, + pub cached_path: Option, } impl LayerData for TextLayer { - fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec, view_mode: ViewMode, font_cache: &FontCache, _culling_bounds: Option<[DVec2; 2]>) { - let transform = self.transform(transforms, view_mode); + fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec, render_data: RenderData) { + let transform = self.transform(transforms, render_data.view_mode); let inverse = transform.inverse(); if !inverse.is_finite() { @@ -53,8 +53,8 @@ impl LayerData for TextLayer { let _ = svg.write_str(r#")">"#); if self.editable { - let font = font_cache.resolve_font(&self.font); - if let Some(url) = font.and_then(|font| font_cache.get_preview_url(font)) { + let font = render_data.font_cache.resolve_font(&self.font); + if let Some(url) = font.and_then(|font| render_data.font_cache.get_preview_url(font)) { let _ = write!(svg, r#""#, url); } @@ -70,7 +70,7 @@ impl LayerData for TextLayer { font.map(|_| r#" style="font-family: local-font;""#).unwrap_or_default() ); } else { - let buzz_face = self.load_face(font_cache); + let buzz_face = self.load_face(render_data.font_cache); let mut path = self.to_bez_path(buzz_face); @@ -86,7 +86,7 @@ impl LayerData for TextLayer { svg, r#""#, path.to_svg(), - self.path_style.render(view_mode, svg_defs, transform, bounds, transformed_bounds) + self.path_style.render(render_data.view_mode, svg_defs, transform, bounds, transformed_bounds) ); } let _ = svg.write_str(""); @@ -139,7 +139,7 @@ impl TextLayer { cached_path: None, }; - new.regenerate_path(new.load_face(font_cache)); + new.cached_path = Some(new.generate_path(new.load_face(font_cache))); new } @@ -147,8 +147,10 @@ impl TextLayer { /// Converts to a [BezPath], populating the cache if necessary. #[inline] pub fn to_bez_path(&mut self, buzz_face: Option) -> BezPath { - if self.cached_path.is_none() { - self.regenerate_path(buzz_face); + if self.cached_path.as_ref().filter(|x| !x.is_empty()).is_none() { + let path = self.generate_path(buzz_face); + self.cached_path = Some(path.clone()); + return path; } self.cached_path.clone().unwrap() } @@ -158,11 +160,11 @@ impl TextLayer { pub fn to_bez_path_nonmut(&self, font_cache: &FontCache) -> BezPath { let buzz_face = self.load_face(font_cache); - self.cached_path.clone().unwrap_or_else(|| self.generate_path(buzz_face)) + self.cached_path.clone().filter(|x| !x.is_empty()).unwrap_or_else(|| self.generate_path(buzz_face)) } #[inline] - fn generate_path(&self, buzz_face: Option) -> BezPath { + pub fn generate_path(&self, buzz_face: Option) -> BezPath { to_kurbo::to_kurbo(&self.text, buzz_face, self.size, self.line_width) } @@ -172,15 +174,10 @@ impl TextLayer { Rect::new(0., 0., far.x, far.y) } - /// Populate the cache. - pub fn regenerate_path(&mut self, buzz_face: Option) { - self.cached_path = Some(self.generate_path(buzz_face)); - } - pub fn update_text(&mut self, text: String, font_cache: &FontCache) { let buzz_face = self.load_face(font_cache); self.text = text; - self.regenerate_path(buzz_face); + self.cached_path = Some(self.generate_path(buzz_face)); } }