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 <keavon@keavon.com>
This commit is contained in:
parent
2e3e079982
commit
020f700c92
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ impl MessageHandler<DialogMessage, &PortfolioMessageHandler> 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);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ pub struct Export {
|
|||
pub scale_factor: f64,
|
||||
pub bounds: ExportBounds,
|
||||
pub artboards: HashMap<LayerId, String>,
|
||||
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()];
|
||||
|
|
|
|||
|
|
@ -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<ArtboardMessage, &FontCache> 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(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<Message>, root: &LayerDataType, mut path: Vec<LayerId>) {
|
||||
let mut image_data = Vec::new();
|
||||
fn walk_layers(data: &LayerDataType, path: &mut Vec<LayerId>, image_data: &mut Vec<FrontendImageData>) {
|
||||
/// 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<Message>, root: &LayerDataType, mut path: Vec<LayerId>) {
|
||||
fn walk_layers(data: &LayerDataType, path: &mut Vec<LayerId>, image_data: &mut Vec<FrontendImageData>, fonts: &mut HashSet<Font>) {
|
||||
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<Message>) {
|
||||
|
|
@ -1040,12 +1046,14 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
|||
let old_transform = self.graphene_document.root.transform;
|
||||
// Reset the root's transform (required to avoid any rotation by the user)
|
||||
self.graphene_document.root.transform = DAffine2::IDENTITY;
|
||||
self.graphene_document.root.cache_dirty = true;
|
||||
GrapheneDocument::mark_children_as_dirty(&mut self.graphene_document.root);
|
||||
|
||||
// Calculates the bounding box of the region to be exported
|
||||
use crate::frontend::utility_types::ExportBounds;
|
||||
let bbox = match bounds {
|
||||
crate::frontend::utility_types::ExportBounds::AllArtwork => 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<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
|||
false => 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#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}" width="{}px" height="{}">{}{}</svg>"#,
|
||||
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<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
|||
}
|
||||
RenameLayer { layer_path, new_name } => 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(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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::<Vec<_>>().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<LayerId>,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<OverlaysMessage, (bool, &FontCache, &InputPreprocessorMessag
|
|||
responses.push_back(
|
||||
FrontendMessage::UpdateDocumentOverlays {
|
||||
svg: if overlays_visible {
|
||||
self.overlays_graphene_document.render_root(ViewMode::Normal, font_cache, Some(ipp.document_bounds()))
|
||||
let render_data = RenderData::new(ViewMode::Normal, font_cache, Some(ipp.document_bounds()), false);
|
||||
self.overlays_graphene_document.render_root(render_data)
|
||||
} else {
|
||||
String::from("")
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use crate::layout::layout_message::LayoutTarget;
|
|||
use crate::layout::widgets::PropertyHolder;
|
||||
use crate::{dialog, message_prelude::*};
|
||||
|
||||
use graphene::layers::layer_info::LayerDataTypeDiscriminant;
|
||||
use graphene::layers::text_layer::{Font, FontCache};
|
||||
use graphene::Operation as DocumentOperation;
|
||||
|
||||
|
|
@ -47,11 +48,10 @@ impl PortfolioMessageHandler {
|
|||
// Uses binary search to find the index of the element where number is bigger than i
|
||||
let new_doc_title_num = doc_title_numbers.binary_search(&0).map_or_else(|e| e, |v| v) + 1;
|
||||
|
||||
let name = match new_doc_title_num {
|
||||
match new_doc_title_num {
|
||||
1 => 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<PortfolioMessage, &InputPreprocessorMessageHandler> 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<PortfolioMessage, &InputPreprocessorMessageHandler> 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<PortfolioMessage, &InputPreprocessorMessageHandler> 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(),
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ impl FileType {
|
|||
#[derive(Clone, Copy, Debug, Eq, Deserialize, PartialEq, Serialize)]
|
||||
pub enum ExportBounds {
|
||||
AllArtwork,
|
||||
Selection,
|
||||
Artboard(LayerId),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -522,6 +522,7 @@ pub struct DropdownEntryData {
|
|||
pub shortcut: Vec<String>,
|
||||
#[serde(rename = "shortcutRequiresLock")]
|
||||
pub shortcut_requires_lock: bool,
|
||||
pub disabled: bool,
|
||||
pub children: Vec<Vec<DropdownEntryData>>,
|
||||
|
||||
#[serde(skip)]
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
>
|
||||
<IconLabel v-if="entry.icon && drawIcon" :icon="entry.icon" class="entry-icon" />
|
||||
<div v-else-if="drawIcon" class="no-icon"></div>
|
||||
|
|
@ -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<Value = string> {
|
|||
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<string>): void => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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("<defs>");
|
||||
|
||||
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("</defs>");
|
||||
|
||||
|
|
@ -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<Vec<DAffine2>, 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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<glam::DAffine2>, 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<glam::DAffine2>, 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<u8>,
|
||||
#[serde(skip)]
|
||||
pub blob_url: Option<String>,
|
||||
|
|
@ -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<DAffine2>, 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<DAffine2>, 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::<String>();
|
||||
let _ = write!(
|
||||
svg,
|
||||
r#"<image width="{}" height="{}" transform="matrix({})" xlink:href="{}" />"#,
|
||||
self.dimensions.x,
|
||||
self.dimensions.y,
|
||||
svg_transform,
|
||||
self.blob_url.as_ref().unwrap_or(&String::new())
|
||||
r#"<image width="{}" height="{}" transform="matrix({})" href=""#,
|
||||
self.dimensions.x, self.dimensions.y, svg_transform,
|
||||
);
|
||||
if render_data.embed_images {
|
||||
let _ = write!(svg, "data:{};base64,{}", self.mime, base64::encode(&self.image_data));
|
||||
} else {
|
||||
let _ = write!(svg, "{}", self.blob_url.as_ref().unwrap_or(&String::new()));
|
||||
}
|
||||
let _ = svg.write_str(r#""/>"#);
|
||||
let _ = svg.write_str("</g>");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
//! Basic wrapper for [`serde`] for [`base64`] encoding
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn as_base64<T, S>(key: &T, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&base64::encode(key.as_ref()))
|
||||
}
|
||||
|
||||
pub fn from_base64<'a, D>(deserializer: D) -> Result<Vec<u8>, 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)
|
||||
}
|
||||
|
|
@ -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 {
|
|||
/// </g>"
|
||||
/// );
|
||||
/// ```
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, 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<glam::DAffine2>, 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<glam::DAffine2>, 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<glam::DAffine2>, render_data: RenderData) {
|
||||
self.inner_mut().render(svg, svg_defs, transforms, render_data)
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache) {
|
||||
|
|
@ -223,14 +260,17 @@ impl Layer {
|
|||
LayerIter { stack: vec![self] }
|
||||
}
|
||||
|
||||
pub fn render(&mut self, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, svg_defs: &mut String, font_cache: &FontCache, culling_bounds: Option<[DVec2; 2]>) -> &str {
|
||||
pub fn render(&mut self, transforms: &mut Vec<DAffine2>, 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#"<g transform="matrix("#);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use super::layer_info::LayerData;
|
||||
use super::style::{self, PathStyle, ViewMode};
|
||||
use super::style::{self, PathStyle, RenderData, ViewMode};
|
||||
use crate::intersection::{intersect_quad_bez_path, Quad};
|
||||
use crate::layers::text_layer::FontCache;
|
||||
use crate::LayerId;
|
||||
|
|
@ -32,13 +32,13 @@ pub struct ShapeLayer {
|
|||
}
|
||||
|
||||
impl LayerData for ShapeLayer {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, 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<DAffine2>, 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, "<!-- SVG shape has an invalid transform -->");
|
||||
|
|
@ -58,7 +58,7 @@ impl LayerData for ShapeLayer {
|
|||
svg,
|
||||
r#"<path d="{}" {} />"#,
|
||||
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("</g>");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<BezPath>,
|
||||
pub cached_path: Option<BezPath>,
|
||||
}
|
||||
|
||||
impl LayerData for TextLayer {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, 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<DAffine2>, 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#"<style>@font-face {{font-family: local-font;src: url({});}}")</style>"#, 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 d="{}" {} />"#,
|
||||
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("</g>");
|
||||
|
|
@ -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<Face>) -> 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<Face>) -> BezPath {
|
||||
pub fn generate_path(&self, buzz_face: Option<Face>) -> 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<Face>) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue