diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index b25e7905a5..a7b537e845 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -190,7 +190,7 @@ impl<'a> ModifyInputsContext<'a> { let fill = resolve_document_node_type("Fill").expect("Fill node does not exist").default_node_template(); let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_node_template(); let text = resolve_document_node_type("Text").expect("Text node does not exist").node_template_input_override([ - Some(NodeInput::scope("editor-api")), + Some(NodeInput::scope("font-cache")), Some(NodeInput::value(TaggedValue::String(text), false)), Some(NodeInput::value(TaggedValue::Font(font), false)), Some(NodeInput::value(TaggedValue::F64(typesetting.font_size), false)), diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 59301b2701..a85b64b797 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -1317,7 +1317,7 @@ fn static_nodes() -> Vec { implementation: DocumentNodeImplementation::ProtoNode(text::text::IDENTIFIER), call_argument: concrete!(Context), inputs: vec![ - NodeInput::scope("editor-api"), + NodeInput::scope("font-cache"), NodeInput::value(TaggedValue::String("Lorem ipsum".to_string()), false), NodeInput::value( TaggedValue::Font(Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.into(), graphene_std::consts::DEFAULT_FONT_STYLE.into())), diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 3f28944765..dc7db348d8 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -31,6 +31,7 @@ use graphene_std::subpath::BezierHandles; use graphene_std::text::Font; use graphene_std::vector::misc::HandleId; use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType}; +use std::sync::Arc; use std::vec; #[derive(ExtractField)] @@ -351,8 +352,9 @@ impl MessageHandler> for Portfolio data, } => { let font = Font::new(font_family, font_style); - - self.persistent_data.font_cache.insert(font, preview_url, data); + let mut font_cache = self.persistent_data.font_cache.as_ref().clone(); + font_cache.insert(font, preview_url, data); + self.persistent_data.font_cache = Arc::new(font_cache); self.executor.update_font_cache(self.persistent_data.font_cache.clone()); for document_id in self.document_ids.iter() { let node_to_inspect = self.node_to_inspect(); diff --git a/editor/src/messages/portfolio/utility_types.rs b/editor/src/messages/portfolio/utility_types.rs index 3edc26d9e0..7ef9ec4c38 100644 --- a/editor/src/messages/portfolio/utility_types.rs +++ b/editor/src/messages/portfolio/utility_types.rs @@ -1,8 +1,9 @@ use graphene_std::text::FontCache; +use std::sync::Arc; #[derive(Debug, Default)] pub struct PersistentData { - pub font_cache: FontCache, + pub font_cache: Arc, pub use_vello: bool, } diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 84b0975ac4..b279203635 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -13,6 +13,7 @@ use graphene_std::transform::Footprint; use graphene_std::vector::Vector; use graphene_std::wasm_application_io::RenderOutputType; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; +use std::sync::Arc; mod runtime_io; pub use runtime_io::NodeRuntimeIO; @@ -89,7 +90,7 @@ impl NodeGraphExecutor { execution_id } - pub fn update_font_cache(&self, font_cache: FontCache) { + pub fn update_font_cache(&self, font_cache: Arc) { self.runtime_io.send(GraphRuntimeRequest::FontCacheUpdate(font_cache)).expect("Failed to send font cache update"); } diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 217e5ad900..77e54e0f89 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -40,6 +40,8 @@ pub struct NodeRuntime { update_thumbnails: bool, editor_api: Arc, + font_cache: Arc, + node_graph_errors: GraphErrors, monitor_nodes: Vec>, @@ -60,7 +62,7 @@ pub struct NodeRuntime { pub enum GraphRuntimeRequest { GraphUpdate(GraphUpdate), ExecutionRequest(ExecutionRequest), - FontCacheUpdate(FontCache), + FontCacheUpdate(Arc), EditorPreferencesUpdate(EditorPreferences), } @@ -113,7 +115,6 @@ impl NodeRuntime { update_thumbnails: true, editor_api: WasmEditorApi { - font_cache: FontCache::default(), editor_preferences: Box::new(EditorPreferences::default()), node_graph_message_sender: Box::new(InternalNodeGraphUpdateSender(sender)), @@ -121,6 +122,8 @@ impl NodeRuntime { } .into(), + font_cache: Arc::new(FontCache::default()), + node_graph_errors: Vec::new(), monitor_nodes: Vec::new(), @@ -139,7 +142,6 @@ impl NodeRuntime { application_io: Some(WasmApplicationIo::new().await.into()), #[cfg(any(test, not(target_family = "wasm")))] application_io: Some(WasmApplicationIo::new_offscreen().await.into()), - font_cache: self.editor_api.font_cache.clone(), node_graph_message_sender: Box::new(self.sender.clone()), editor_preferences: Box::new(self.editor_preferences.clone()), } @@ -163,13 +165,7 @@ impl NodeRuntime { for request in requests { match request { GraphRuntimeRequest::FontCacheUpdate(font_cache) => { - self.editor_api = WasmEditorApi { - font_cache, - application_io: self.editor_api.application_io.clone(), - node_graph_message_sender: Box::new(self.sender.clone()), - editor_preferences: Box::new(self.editor_preferences.clone()), - } - .into(); + self.font_cache = font_cache; if let Some(graph) = self.old_graph.clone() { // We ignore this result as compilation errors should have been reported in an earlier iteration let _ = self.update_network(graph).await; @@ -178,7 +174,6 @@ impl NodeRuntime { GraphRuntimeRequest::EditorPreferencesUpdate(preferences) => { self.editor_preferences = preferences.clone(); self.editor_api = WasmEditorApi { - font_cache: self.editor_api.font_cache.clone(), application_io: self.editor_api.application_io.clone(), node_graph_message_sender: Box::new(self.sender.clone()), editor_preferences: Box::new(preferences), @@ -240,7 +235,7 @@ impl NodeRuntime { async fn update_network(&mut self, mut graph: NodeNetwork) -> Result { preprocessor::expand_network(&mut graph, &self.substitutions); - let scoped_network = wrap_network_in_scope(graph, self.editor_api.clone()); + let scoped_network = wrap_network_in_scope(graph, self.editor_api.clone(), self.font_cache.clone()); // We assume only one output assert_eq!(scoped_network.exports.len(), 1, "Graph with multiple outputs not yet handled"); @@ -408,7 +403,6 @@ pub async fn replace_application_io(application_io: WasmApplicationIo) { let mut node_runtime = NODE_RUNTIME.lock(); if let Some(node_runtime) = &mut *node_runtime { node_runtime.editor_api = WasmEditorApi { - font_cache: node_runtime.editor_api.font_cache.clone(), application_io: Some(application_io.into()), node_graph_message_sender: Box::new(node_runtime.sender.clone()), editor_preferences: Box::new(node_runtime.editor_preferences.clone()), diff --git a/node-graph/gapplication-io/src/lib.rs b/node-graph/gapplication-io/src/lib.rs index b9d072d2c0..9c91ddc0d9 100644 --- a/node-graph/gapplication-io/src/lib.rs +++ b/node-graph/gapplication-io/src/lib.rs @@ -1,6 +1,5 @@ use dyn_any::{DynAny, StaticType, StaticTypeSized}; use glam::{DAffine2, UVec2}; -use graphene_core::text::FontCache; use graphene_core::transform::Footprint; use graphene_core::vector::style::ViewMode; use std::fmt::Debug; @@ -262,8 +261,6 @@ impl GetEditorPreferences for DummyPreferences { } pub struct EditorApi { - /// Font data (for rendering text) made available to the graph through the [`WasmEditorApi`]. - pub font_cache: FontCache, /// Gives access to APIs like a rendering surface (native window handle or HTML5 canvas) and WGPU (which becomes WebGPU on web). pub application_io: Option>, pub node_graph_message_sender: Box, @@ -276,7 +273,6 @@ impl Eq for EditorApi {} impl Default for EditorApi { fn default() -> Self { Self { - font_cache: FontCache::default(), application_io: None, node_graph_message_sender: Box::new(Logger), editor_preferences: Box::new(DummyPreferences), @@ -286,7 +282,6 @@ impl Default for EditorApi { impl Hash for EditorApi { fn hash(&self, state: &mut H) { - self.font_cache.hash(state); self.application_io.as_ref().map_or(0, |io| io.as_ref() as *const _ as usize).hash(state); (self.node_graph_message_sender.as_ref() as *const dyn NodeGraphUpdateSender).hash(state); (self.editor_preferences.as_ref() as *const dyn GetEditorPreferences).hash(state); @@ -295,8 +290,7 @@ impl Hash for EditorApi { impl PartialEq for EditorApi { fn eq(&self, other: &Self) -> bool { - self.font_cache == other.font_cache - && self.application_io.as_ref().map_or(0, |io| addr_of!(io) as usize) == other.application_io.as_ref().map_or(0, |io| addr_of!(io) as usize) + self.application_io.as_ref().map_or(0, |io| addr_of!(io) as usize) == other.application_io.as_ref().map_or(0, |io| addr_of!(io) as usize) && std::ptr::eq(self.node_graph_message_sender.as_ref() as *const _, other.node_graph_message_sender.as_ref() as *const _) && std::ptr::eq(self.editor_preferences.as_ref() as *const _, other.editor_preferences.as_ref() as *const _) } @@ -304,7 +298,7 @@ impl PartialEq for EditorApi { impl Debug for EditorApi { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("EditorApi").field("font_cache", &self.font_cache).finish() + f.debug_struct("EditorApi").finish() } } diff --git a/node-graph/gcore/src/consts.rs b/node-graph/gcore/src/consts.rs index 505dc81ccd..bd79ad78a8 100644 --- a/node-graph/gcore/src/consts.rs +++ b/node-graph/gcore/src/consts.rs @@ -7,3 +7,5 @@ pub const LAYER_OUTLINE_STROKE_WEIGHT: f64 = 0.5; // Fonts pub const DEFAULT_FONT_FAMILY: &str = "Cabin"; pub const DEFAULT_FONT_STYLE: &str = "Regular (400)"; +pub const SOURCE_SANS_PRO_FAMILY: &str = "Source Sans Pro"; +pub const SOURCE_SANS_PRO_STYLE: &str = "Regular (400)"; diff --git a/node-graph/gcore/src/text/font_cache.rs b/node-graph/gcore/src/text/font_cache.rs index 37a8bfc505..e2d9870a5d 100644 --- a/node-graph/gcore/src/text/font_cache.rs +++ b/node-graph/gcore/src/text/font_cache.rs @@ -1,6 +1,8 @@ use dyn_any::DynAny; use std::collections::HashMap; +use crate::consts::{SOURCE_SANS_PRO_FAMILY, SOURCE_SANS_PRO_STYLE}; + /// A font type (storing font family and font style and an optional preview URL) #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Hash, PartialEq, Eq, DynAny, specta::Type)] pub struct Font { @@ -20,7 +22,7 @@ impl Default for Font { } } /// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`) -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq, DynAny)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, DynAny)] pub struct FontCache { /// Actual font file data used for rendering a font font_file_data: HashMap>, @@ -28,6 +30,22 @@ pub struct FontCache { preview_urls: HashMap, } +impl Default for FontCache { + fn default() -> Self { + let font = Font::new(SOURCE_SANS_PRO_FAMILY.to_string(), SOURCE_SANS_PRO_STYLE.to_string()); + // Load Source Sans Pro font data + // TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo. + // TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size. + const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf"); + let mut font_file_data = HashMap::new(); + font_file_data.insert(font, FONT_DATA.to_vec()); + Self { + font_file_data, + preview_urls: HashMap::new(), + } + } +} + impl FontCache { /// Returns the font family name if the font is cached, otherwise returns the fallback font family name if that is cached pub fn resolve_font<'a>(&'a self, font: &'a Font) -> Option<&'a Font> { @@ -60,6 +78,15 @@ impl FontCache { pub fn get_preview_url(&self, font: &Font) -> Option<&String> { self.preview_urls.get(font) } + + /// Get the default font (Source Sans Pro) which is loaded from disk + pub fn source_sans_pro(&self) -> &Vec { + self.get(&Font { + font_family: SOURCE_SANS_PRO_FAMILY.to_string(), + font_style: SOURCE_SANS_PRO_STYLE.to_string(), + }) + .expect("Source sans pro must be added to font cache") + } } impl std::hash::Hash for FontCache { diff --git a/node-graph/gcore/src/text/source-sans-pro-regular.ttf b/node-graph/gcore/src/text/source-sans-pro-regular.ttf new file mode 100644 index 0000000000..ffe27865aa Binary files /dev/null and b/node-graph/gcore/src/text/source-sans-pro-regular.ttf differ diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 6d404462d5..1af121e8ed 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -11,6 +11,7 @@ use graphene_brush::brush_stroke::BrushStroke; use graphene_core::raster::Image; use graphene_core::raster_types::{CPU, Raster}; use graphene_core::table::Table; +use graphene_core::text::FontCache; use graphene_core::transform::ReferencePoint; use graphene_core::uuid::NodeId; use graphene_core::vector::Vector; @@ -38,7 +39,9 @@ macro_rules! tagged_value { RenderOutput(RenderOutput), SurfaceFrame(SurfaceFrame), #[serde(skip)] - EditorApi(Arc) + EditorApi(Arc), + #[serde(skip)] + FontCache(Arc), } // We must manually implement hashing because some values are floats and so do not reproducibly hash (see FakeHash below) @@ -52,6 +55,7 @@ macro_rules! tagged_value { Self::RenderOutput(x) => x.hash(state), Self::SurfaceFrame(x) => x.hash(state), Self::EditorApi(x) => x.hash(state), + Self::FontCache(x) => x.hash(state), } } } @@ -64,6 +68,7 @@ macro_rules! tagged_value { Self::RenderOutput(x) => Box::new(x), Self::SurfaceFrame(x) => Box::new(x), Self::EditorApi(x) => Box::new(x), + Self::FontCache(x) => Box::new(x), } } /// Converts to a Arc @@ -74,6 +79,7 @@ macro_rules! tagged_value { Self::RenderOutput(x) => Arc::new(x), Self::SurfaceFrame(x) => Arc::new(x), Self::EditorApi(x) => Arc::new(x), + Self::FontCache(x) => Arc::new(x), } } /// Creates a graphene_core::Type::Concrete(TypeDescriptor { .. }) with the type of the value inside the tagged value @@ -83,7 +89,8 @@ macro_rules! tagged_value { $( Self::$identifier(_) => concrete!($ty), )* Self::RenderOutput(_) => concrete!(RenderOutput), Self::SurfaceFrame(_) => concrete!(SurfaceFrame), - Self::EditorApi(_) => concrete!(&WasmEditorApi) + Self::EditorApi(_) => concrete!(&WasmEditorApi), + Self::FontCache(_) => concrete!(Arc), } } /// Attempts to downcast the dynamic type to a tagged value @@ -97,7 +104,6 @@ macro_rules! tagged_value { x if x == TypeId::of::() => Ok(TaggedValue::RenderOutput(*downcast(input).unwrap())), x if x == TypeId::of::() => Ok(TaggedValue::SurfaceFrame(*downcast(input).unwrap())), - _ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))), } } diff --git a/node-graph/graphene-cli/src/main.rs b/node-graph/graphene-cli/src/main.rs index 39de026cd9..2860eb4edd 100644 --- a/node-graph/graphene-cli/src/main.rs +++ b/node-graph/graphene-cli/src/main.rs @@ -90,7 +90,6 @@ async fn main() -> Result<(), Box> { let preferences = EditorPreferences { use_vello: true }; let editor_api = Arc::new(WasmEditorApi { - font_cache: FontCache::default(), application_io: Some(application_io.into()), node_graph_message_sender: Box::new(UpdateLogger {}), editor_preferences: Box::new(preferences), @@ -184,7 +183,9 @@ fn compile_graph(document_string: String, editor_api: Arc) -> Res let substitutions = preprocessor::generate_node_substitutions(); preprocessor::expand_network(&mut network, &substitutions); - let wrapped_network = wrap_network_in_scope(network.clone(), editor_api); + let font_cache = Arc::new(FontCache::default()); + + let wrapped_network = wrap_network_in_scope(network.clone(), editor_api, font_cache); let compiler = Compiler {}; compiler.compile_single(wrapped_network).map_err(|x| x.into()) diff --git a/node-graph/gstd/src/text.rs b/node-graph/gstd/src/text.rs index d1a094cc73..08633bea22 100644 --- a/node-graph/gstd/src/text.rs +++ b/node-graph/gstd/src/text.rs @@ -1,11 +1,12 @@ -use graph_craft::wasm_application_io::WasmEditorApi; +use std::sync::Arc; + pub use graphene_core::text::*; use graphene_core::{Ctx, table::Table, vector::Vector}; #[node_macro::node(category(""))] fn text<'i: 'n>( _: impl Ctx, - editor: &'i WasmEditorApi, + font_cache: Arc, text: String, font_name: Font, #[unit(" px")] @@ -38,7 +39,7 @@ fn text<'i: 'n>( align, }; - let font_data = editor.font_cache.get(&font_name).map(|f| load_font(f)); + let font_data = font_cache.get(&font_name).map(|f| load_font(f)); to_path(&text, font_data, typesetting, per_glyph_instances) } diff --git a/node-graph/interpreted-executor/benches/benchmark_util.rs b/node-graph/interpreted-executor/benches/benchmark_util.rs index b2c686c222..0e3ba38ea1 100644 --- a/node-graph/interpreted-executor/benches/benchmark_util.rs +++ b/node-graph/interpreted-executor/benches/benchmark_util.rs @@ -4,13 +4,15 @@ use futures::executor::block_on; use graph_craft::proto::ProtoNetwork; use graph_craft::util::{DEMO_ART, compile, load_from_name}; use graphene_std::application_io::EditorApi; +use graphene_std::text::FontCache; use interpreted_executor::dynamic_executor::DynamicExecutor; use interpreted_executor::util::wrap_network_in_scope; pub fn setup_network(name: &str) -> (DynamicExecutor, ProtoNetwork) { let network = load_from_name(name); let editor_api = std::sync::Arc::new(EditorApi::default()); - let network = wrap_network_in_scope(network, editor_api); + let font_cache = std::sync::Arc::new(FontCache::default()); + let network = wrap_network_in_scope(network, editor_api, font_cache); let proto_network = compile(network); let executor = block_on(DynamicExecutor::new(proto_network.clone())).unwrap(); (executor, proto_network) diff --git a/node-graph/interpreted-executor/src/util.rs b/node-graph/interpreted-executor/src/util.rs index d5c4d0a90b..9825257a2a 100644 --- a/node-graph/interpreted-executor/src/util.rs +++ b/node-graph/interpreted-executor/src/util.rs @@ -5,10 +5,11 @@ use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput, use graph_craft::generic; use graph_craft::wasm_application_io::WasmEditorApi; use graphene_std::Context; +use graphene_std::text::FontCache; use graphene_std::uuid::NodeId; use std::sync::Arc; -pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc) -> NodeNetwork { +pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc, font_cache: Arc) -> NodeNetwork { network.generate_node_paths(&[]); let inner_network = DocumentNode { @@ -72,12 +73,22 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc))), + ] + .into_iter() + .collect(), // TODO(TrueDoctor): check if it makes sense to set `generated` to `true` generated: false, }