diff --git a/Cargo.lock b/Cargo.lock index e9aae5ad5f..0b69c5e32b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1640,6 +1640,7 @@ dependencies = [ "num-traits", "once_cell", "rand_chacha 0.3.1", + "rustybuzz", "serde", "specta", "spin 0.9.8", diff --git a/document-legacy/src/document.rs b/document-legacy/src/document.rs index 37a9adbe78..a180df1e6e 100644 --- a/document-legacy/src/document.rs +++ b/document-legacy/src/document.rs @@ -5,7 +5,6 @@ use crate::layers::layer_info::{Layer, LayerData, LayerDataType, LayerDataTypeDi use crate::layers::nodegraph_layer::{CachedOutputData, NodeGraphFrameLayer}; use crate::layers::shape_layer::ShapeLayer; use crate::layers::style::RenderData; -use crate::layers::text_layer::{Font, TextLayer}; use crate::{DocumentError, DocumentResponse, Operation}; use glam::{DAffine2, DVec2}; @@ -430,30 +429,6 @@ 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; - if let LayerDataType::Text(text) = &mut root.data { - text.cached_path = None; - } - } - - 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]; @@ -539,25 +514,6 @@ impl Document { Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) } - Operation::AddText { - path, - insert_index, - transform, - text, - style, - size, - font_name, - font_style, - } => { - let font = Font::new(font_name, font_style); - let layer_text = TextLayer::new(text, style, size, font, render_data); - let layer_data = LayerDataType::Text(layer_text); - let layer = Layer::new(layer_data, transform); - - self.set_layer(&path, layer, insert_index)?; - - Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) - } Operation::AddNodeGraphFrame { path, insert_index, @@ -576,30 +532,6 @@ impl Document { } Some(vec![LayerChanged { path: layer_path.clone() }]) } - Operation::SetTextEditability { path, editable } => { - self.layer_mut(&path)?.as_text_mut()?.editable = editable; - self.mark_as_dirty(&path)?; - Some(vec![DocumentChanged]) - } - Operation::SetTextContent { path, new_text } => { - // Not using Document::layer_mut is necessary because we also need to borrow the font cache - let mut current_folder = &mut self.root; - - let (layer_path, id) = split_path(&path)?; - for id in layer_path { - current_folder = current_folder.as_folder_mut()?.layer_mut(*id).ok_or_else(|| DocumentError::LayerNotFound(layer_path.into()))?; - } - current_folder - .as_folder_mut()? - .layer_mut(id) - .ok_or_else(|| DocumentError::LayerNotFound(path.clone()))? - .as_text_mut()? - .update_text(new_text, render_data); - - self.mark_as_dirty(&path)?; - - Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat()) - } Operation::AddNgon { path, insert_index, @@ -736,22 +668,6 @@ impl Document { return Err(DocumentError::IndexOutOfBounds); } } - Operation::ModifyFont { path, font_family, font_style, size } => { - // Not using Document::layer_mut is necessary because we also need to borrow the font cache - let mut current_folder = &mut self.root; - let (folder_path, id) = split_path(&path)?; - for id in folder_path { - current_folder = current_folder.as_folder_mut()?.layer_mut(*id).ok_or_else(|| DocumentError::LayerNotFound(folder_path.into()))?; - } - let layer_mut = current_folder.as_folder_mut()?.layer_mut(id).ok_or_else(|| DocumentError::LayerNotFound(folder_path.into()))?; - let text = layer_mut.as_text_mut()?; - - text.font = Font::new(font_family, font_style); - text.size = size; - text.cached_path = Some(text.generate_path(text.load_face(render_data))); - self.mark_as_dirty(&path)?; - Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) - } Operation::RenameLayer { layer_path: path, new_name: name } => { self.layer_mut(&path)?.name = Some(name); Some(vec![LayerChanged { path }]) @@ -791,7 +707,9 @@ impl Document { let layer = self.layer_mut(&path).expect("Clearing node graph image for invalid layer"); match &mut layer.data { LayerDataType::NodeGraphFrame(node_graph) => { - node_graph.cached_output_data = CachedOutputData::None; + if matches!(node_graph.cached_output_data, CachedOutputData::BlobURL(_)) { + node_graph.cached_output_data = CachedOutputData::None; + } } e => panic!("Incorrectly trying to clear the blob URL for layer of type {}", LayerDataTypeDiscriminant::from(&*e)), } @@ -984,7 +902,6 @@ impl Document { let layer = self.layer_mut(&path)?; match &mut layer.data { LayerDataType::Shape(s) => s.style = style, - LayerDataType::Text(text) => text.path_style = style, _ => return Err(DocumentError::NotShape), } self.mark_as_dirty(&path)?; diff --git a/document-legacy/src/layers/layer_info.rs b/document-legacy/src/layers/layer_info.rs index 7c1fa4fe7d..d4d0de599f 100644 --- a/document-legacy/src/layers/layer_info.rs +++ b/document-legacy/src/layers/layer_info.rs @@ -3,7 +3,6 @@ use super::folder_layer::FolderLayer; use super::nodegraph_layer::NodeGraphFrameLayer; use super::shape_layer::ShapeLayer; use super::style::{PathStyle, RenderData}; -use super::text_layer::TextLayer; use crate::intersection::Quad; use crate::DocumentError; use crate::LayerId; @@ -23,8 +22,6 @@ pub enum LayerDataType { Folder(FolderLayer), /// A layer that wraps a [ShapeLayer] struct. Shape(ShapeLayer), - /// A layer that wraps a [TextLayer] struct. - Text(TextLayer), /// A layer that wraps an [NodeGraphFrameLayer] struct. NodeGraphFrame(NodeGraphFrameLayer), } @@ -34,7 +31,6 @@ impl LayerDataType { match self { LayerDataType::Shape(s) => s, LayerDataType::Folder(f) => f, - LayerDataType::Text(t) => t, LayerDataType::NodeGraphFrame(n) => n, } } @@ -43,7 +39,6 @@ impl LayerDataType { match self { LayerDataType::Shape(s) => s, LayerDataType::Folder(f) => f, - LayerDataType::Text(t) => t, LayerDataType::NodeGraphFrame(n) => n, } } @@ -75,7 +70,6 @@ impl From<&LayerDataType> for LayerDataTypeDiscriminant { match data { Folder(_) => LayerDataTypeDiscriminant::Folder, Shape(_) => LayerDataTypeDiscriminant::Shape, - Text(_) => LayerDataTypeDiscriminant::Text, NodeGraphFrame(_) => LayerDataTypeDiscriminant::NodeGraphFrame, } } @@ -459,24 +453,6 @@ impl Layer { } } - /// Get a mutable reference to the Text element wrapped by the layer. - /// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Text`. - pub fn as_text_mut(&mut self) -> Result<&mut TextLayer, DocumentError> { - match &mut self.data { - LayerDataType::Text(t) => Ok(t), - _ => Err(DocumentError::NotText), - } - } - - /// Get a reference to the Text element wrapped by the layer. - /// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Text`. - pub fn as_text(&self) -> Result<&TextLayer, DocumentError> { - match &self.data { - LayerDataType::Text(t) => Ok(t), - _ => Err(DocumentError::NotText), - } - } - /// Get a mutable reference to the NodeNetwork /// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::NodeGraphFrame`. pub fn as_node_graph_mut(&mut self) -> Result<&mut graph_craft::document::NodeNetwork, DocumentError> { @@ -505,7 +481,6 @@ impl Layer { pub fn style(&self) -> Result<&PathStyle, DocumentError> { match &self.data { LayerDataType::Shape(s) => Ok(&s.style), - LayerDataType::Text(t) => Ok(&t.path_style), LayerDataType::NodeGraphFrame(t) => t.as_vector_data().map(|vector| &vector.style).ok_or(DocumentError::NotShape), _ => Err(DocumentError::NotShape), } @@ -514,7 +489,6 @@ impl Layer { pub fn style_mut(&mut self) -> Result<&mut PathStyle, DocumentError> { match &mut self.data { LayerDataType::Shape(s) => Ok(&mut s.style), - LayerDataType::Text(t) => Ok(&mut t.path_style), _ => Err(DocumentError::NotShape), } } @@ -551,12 +525,6 @@ impl From for Layer { } } -impl From for Layer { - fn from(from: TextLayer) -> Layer { - Layer::new(LayerDataType::Text(from), DAffine2::IDENTITY.to_cols_array()) - } -} - impl<'a> IntoIterator for &'a Layer { type Item = &'a Layer; type IntoIter = LayerIter<'a>; diff --git a/document-legacy/src/layers/mod.rs b/document-legacy/src/layers/mod.rs index abd54f1414..e403f0acb9 100644 --- a/document-legacy/src/layers/mod.rs +++ b/document-legacy/src/layers/mod.rs @@ -4,7 +4,6 @@ //! There are currently these different types of layers: //! * [Folder layers](folder_layer::FolderLayer), which encapsulate sub-layers //! * [Shape layers](shape_layer::ShapeLayer), which contain generic SVG [``](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path)s -//! * [Text layers](text_layer::TextLayer), which contain a description of laid out text //! * [Node Graph layers](nodegraph_layer::NodegraphLayer), which contain a node graph frame //! //! Refer to the module-level documentation for detailed information on each layer. @@ -23,10 +22,9 @@ pub mod folder_layer; pub mod layer_info; /// Contains the [NodegraphLayer](nodegraph_layer::NodegraphLayer) type that contains a node graph. pub mod nodegraph_layer; +// TODO: Remove shape layers after rewriting the overlay system /// Contains the [ShapeLayer](shape_layer::ShapeLayer) type, a generic SVG element defined using Bezier paths. pub mod shape_layer; -/// Contains the [TextLayer](text_layer::TextLayer) type. -pub mod text_layer; mod render_data; pub use render_data::RenderData; diff --git a/document-legacy/src/layers/render_data.rs b/document-legacy/src/layers/render_data.rs index 804303fe41..ac19ddc86a 100644 --- a/document-legacy/src/layers/render_data.rs +++ b/document-legacy/src/layers/render_data.rs @@ -1,5 +1,5 @@ use super::style::ViewMode; -use super::text_layer::FontCache; +use graphene_std::text::FontCache; use glam::DVec2; diff --git a/document-legacy/src/layers/text_layer.rs b/document-legacy/src/layers/text_layer.rs deleted file mode 100644 index 2c7a51842b..0000000000 --- a/document-legacy/src/layers/text_layer.rs +++ /dev/null @@ -1,179 +0,0 @@ -use super::layer_info::LayerData; -use super::style::{PathStyle, RenderData, ViewMode}; -use crate::intersection::{intersect_quad_bez_path, Quad}; -use crate::LayerId; -pub use font_cache::{Font, FontCache}; - -use graphene_std::vector::subpath::Subpath; - -use glam::{DAffine2, DMat2, DVec2}; -use rustybuzz::Face; -use serde::{Deserialize, Serialize}; -use std::fmt::Write; - -mod font_cache; -mod to_path; - -/// A line, or multiple lines, of text drawn in the document. -/// Like [ShapeLayers](super::shape_layer::ShapeLayer), [TextLayer] are rendered as -/// [``s](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path). -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, specta::Type)] -pub struct TextLayer { - /// The string of text, encompassing one or multiple lines. - pub text: String, - /// Fill color and stroke used to render the text. - pub path_style: PathStyle, - /// Font size in pixels. - pub size: f64, - pub line_width: Option, - pub font: Font, - #[serde(skip)] - pub editable: bool, - #[serde(skip)] - pub cached_path: Option, -} - -impl LayerData for TextLayer { - fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec, render_data: &RenderData) -> bool { - let transform = self.transform(transforms, render_data.view_mode); - let inverse = transform.inverse(); - - if !inverse.is_finite() { - let _ = write!(svg, ""); - return false; - } - - let _ = writeln!(svg, r#""#); - - if self.editable { - 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); - } - - let _ = write!( - svg, - r#""#, - transform - .to_cols_array() - .iter() - .enumerate() - .map(|(i, entry)| { entry.to_string() + if i == 5 { "" } else { "," } }) - .collect::(), - font.map(|_| r#" style="font-family: local-font;""#).unwrap_or_default() - ); - } else { - let buzz_face = self.load_face(render_data); - - let mut path = self.to_subpath(buzz_face); - - let bounds = path.bounding_box().unwrap_or_default(); - - path.apply_affine(transform); - - let transformed_bounds = path.bounding_box().unwrap_or_default(); - - let _ = write!( - svg, - r#""#, - path.to_svg(), - self.path_style.render(render_data.view_mode, svg_defs, transform, bounds, transformed_bounds) - ); - } - let _ = svg.write_str(""); - - false - } - - fn bounding_box(&self, transform: glam::DAffine2, render_data: &RenderData) -> Option<[DVec2; 2]> { - let buzz_face = Some(self.load_face(render_data)?); - - if transform.matrix2 == DMat2::ZERO { - return None; - } - - Some((transform * self.bounding_box(&self.text, buzz_face)).bounding_box()) - } - - fn intersects_quad(&self, quad: Quad, path: &mut Vec, intersections: &mut Vec>, render_data: &RenderData) { - let buzz_face = self.load_face(render_data); - - if intersect_quad_bez_path(quad, &self.bounding_box(&self.text, buzz_face).path(), true) { - intersections.push(path.clone()); - } - } -} - -impl TextLayer { - pub fn load_face<'a>(&self, render_data: &'a RenderData) -> Option> { - render_data.font_cache.get(&self.font).map(|data| rustybuzz::Face::from_slice(data, 0).expect("Loading font failed")) - } - - pub fn transform(&self, transforms: &[DAffine2], mode: ViewMode) -> DAffine2 { - let start = match mode { - ViewMode::Outline => 0, - _ => (transforms.len() as i32 - 1).max(0) as usize, - }; - transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY) - } - - pub fn new(text: String, style: PathStyle, size: f64, font: Font, render_data: &RenderData) -> Self { - let mut new = Self { - text, - path_style: style, - size, - line_width: None, - font, - editable: false, - cached_path: None, - }; - - new.cached_path = Some(new.generate_path(new.load_face(render_data))); - - new - } - - /// Converts to a [Subpath], populating the cache if necessary. - #[inline] - pub fn to_subpath(&mut self, buzz_face: Option) -> Subpath { - if self.cached_path.as_ref().filter(|subpath| !subpath.manipulator_groups().is_empty()).is_none() { - let path = self.generate_path(buzz_face); - self.cached_path = Some(path.clone()); - return path; - } - self.cached_path.clone().unwrap() - } - - /// Converts to a [Subpath], without populating the cache. - #[inline] - pub fn to_subpath_nonmut(&self, render_data: &RenderData) -> Subpath { - let buzz_face = self.load_face(render_data); - - self.cached_path - .clone() - .filter(|subpath| !subpath.manipulator_groups().is_empty()) - .unwrap_or_else(|| self.generate_path(buzz_face)) - } - - #[inline] - pub fn generate_path(&self, buzz_face: Option) -> Subpath { - to_path::to_path(&self.text, buzz_face, self.size, self.line_width) - } - - #[inline] - pub fn bounding_box(&self, text: &str, buzz_face: Option) -> Quad { - let far = to_path::bounding_box(text, buzz_face, self.size, self.line_width); - Quad::from_box([DVec2::ZERO, far]) - } - - pub fn update_text(&mut self, text: String, render_data: &RenderData) { - let buzz_face = self.load_face(render_data); - - self.text = text; - self.cached_path = Some(self.generate_path(buzz_face)); - } -} diff --git a/document-legacy/src/operation.rs b/document-legacy/src/operation.rs index 8e847810a2..7b81dd36e4 100644 --- a/document-legacy/src/operation.rs +++ b/document-legacy/src/operation.rs @@ -36,16 +36,6 @@ pub enum Operation { transform: [f64; 6], style: style::PathStyle, }, - AddText { - path: Vec, - insert_index: isize, - transform: [f64; 6], - style: style::PathStyle, - text: String, - size: f64, - font_name: String, - font_style: String, - }, AddNodeGraphFrame { path: Vec, insert_index: isize, @@ -68,14 +58,6 @@ pub enum Operation { layer_path: Vec, pivot: (f64, f64), }, - SetTextEditability { - path: Vec, - editable: bool, - }, - SetTextContent { - path: Vec, - new_text: String, - }, AddPolyline { path: Vec, insert_index: isize, @@ -128,12 +110,6 @@ pub enum Operation { DuplicateLayer { path: Vec, }, - ModifyFont { - path: Vec, - font_family: String, - size: f64, - font_style: String, - }, MoveSelectedManipulatorPoints { layer_path: Vec, delta: (f64, f64), diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index a8bd204c34..769e3628af 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -2,7 +2,7 @@ use crate::consts::{DEFAULT_FONT_FAMILY, DEFAULT_FONT_STYLE}; use crate::messages::debug::utility_types::MessageLoggingVerbosity; use crate::messages::prelude::*; -use document_legacy::layers::text_layer::Font; +use graphene_core::text::Font; #[derive(Debug, Default)] pub struct Dispatcher { diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 7b09d5805e..fa0cf7398a 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -7,11 +7,11 @@ use crate::messages::portfolio::document::utility_types::layer_panel::{JsRawBuff use crate::messages::prelude::*; use crate::messages::tool::utility_types::HintData; -use document_legacy::layers::text_layer::Font; use document_legacy::LayerId; use graph_craft::document::NodeId; use graph_craft::imaginate_input::*; use graphene_core::raster::color::Color; +use graphene_core::text::Font; use serde::{Deserialize, Serialize}; @@ -37,6 +37,11 @@ pub enum FrontendMessage { #[serde(rename = "fontSize")] font_size: f64, color: Color, + url: String, + transform: [f64; 6], + }, + DisplayEditableTextboxTransform { + transform: [f64; 6], }, DisplayRemoveEditableTextbox, diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index 8b74ad3e82..13261c3acb 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -5,9 +5,9 @@ use crate::messages::layout::utility_types::layout_widget::{DiffUpdate, Widget}; use crate::messages::layout::utility_types::layout_widget::{Layout, WidgetLayout}; use crate::messages::prelude::*; -use document_legacy::layers::text_layer::Font; use document_legacy::LayerId; use graphene_core::raster::color::Color; +use graphene_core::text::Font; use serde_json::Value; use std::ops::Not; diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 37099a479a..e202c7bc36 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -176,10 +176,6 @@ pub enum DocumentMessage { SetSnapping { snap: bool, }, - SetTextboxEditability { - path: Vec, - editable: bool, - }, SetViewMode { view_mode: ViewMode, }, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 833047ce87..d92c2b1ad8 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -29,13 +29,12 @@ use document_legacy::layers::blend_mode::BlendMode; use document_legacy::layers::folder_layer::FolderLayer; use document_legacy::layers::layer_info::{LayerDataType, LayerDataTypeDiscriminant}; use document_legacy::layers::nodegraph_layer::CachedOutputData; -use document_legacy::layers::style::{Fill, RenderData, ViewMode}; -use document_legacy::layers::text_layer::Font; +use document_legacy::layers::style::{RenderData, ViewMode}; use document_legacy::{DocumentError, DocumentResponse, LayerId, Operation as DocumentOperation}; -use graph_craft::document::NodeId; -use graph_craft::{concrete, Type, TypeDescriptor}; -use graphene_core::raster::{Color, ImageFrame}; -use graphene_core::Cow; +use graph_craft::document::value::TaggedValue; +use graph_craft::document::{NodeId, NodeInput}; +use graphene_core::raster::ImageFrame; +use graphene_core::text::Font; use glam::{DAffine2, DVec2}; use serde::{Deserialize, Serialize}; @@ -904,24 +903,6 @@ impl MessageHandler { self.snapping_enabled = snap; } - SetTextboxEditability { path, editable } => { - let text = self.document_legacy.layer(&path).unwrap().as_text().unwrap(); - responses.push_back(DocumentOperation::SetTextEditability { path, editable }.into()); - if editable { - let color = if let Fill::Solid(solid_color) = text.path_style.fill() { *solid_color } else { Color::BLACK }; - responses.push_back( - FrontendMessage::DisplayEditableTextbox { - text: text.text.clone(), - line_width: text.line_width, - font_size: text.size, - color, - } - .into(), - ); - } else { - responses.push_back(FrontendMessage::DisplayRemoveEditableTextbox.into()); - } - } SetViewMode { view_mode } => { self.view_mode = view_mode; responses.push_front(DocumentMessage::DirtyRenderDocument.into()); @@ -1055,37 +1036,41 @@ impl DocumentMessageHandler { return None; }; - // Find the primary input type of the node graph - let primary_input_type = node_network.input_types().next().clone(); - let response = match primary_input_type { - // Only calclate the frame if the primary input is an image - Some(ty) if ty == concrete!(ImageFrame) => { - // Calculate the size of the region to be exported - let old_transforms = self.remove_document_transform(); - let transform = self.document_legacy.multiply_transforms(&layer_path).unwrap(); - let size = DVec2::new(transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length()); + // Check if we use the "Input Frame" node. + // TODO: Remove once rasterization is moved into a node. + let input_frame = node_network.nodes.iter().find(|(_, node)| node.name == "Input Frame"); + let input_node_id = input_frame.map(|(&id, _)| id); + let primary_input_type = input_node_id.filter(|&target_node_id| node_network.connected_to_output(target_node_id, true)); - let svg = self.render_document(size, transform.inverse(), persistent_data, DocumentRenderMode::OnlyBelowLayerInFolder(&layer_path)); - self.restore_document_transform(old_transforms); + // Only calculate the frame if the primary input is an image + let response = if primary_input_type.is_some() { + // Calculate the size of the region to be exported + let old_transforms = self.remove_document_transform(); + let transform = self.document_legacy.multiply_transforms(&layer_path).unwrap(); + let size = DVec2::new(transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length()); - FrontendMessage::TriggerNodeGraphFrameGenerate { - document_id, - layer_path, - svg, - size, - imaginate_node, - } - .into() + let svg = self.render_document(size, transform.inverse(), persistent_data, DocumentRenderMode::OnlyBelowLayerInFolder(&layer_path)); + self.restore_document_transform(old_transforms); + + FrontendMessage::TriggerNodeGraphFrameGenerate { + document_id, + layer_path, + svg, + size, + imaginate_node, } - // Skip processing under node graph frame input if not connected - _ => PortfolioMessage::ProcessNodeGraphFrame { + .into() + } + // Skip processing under node graph frame input if not connected + else { + PortfolioMessage::ProcessNodeGraphFrame { document_id, layer_path, image_data: Default::default(), size: (0, 0), imaginate_node, } - .into(), + .into() }; Some(response) } @@ -1622,13 +1607,21 @@ impl DocumentMessageHandler { path.pop(); } } - LayerDataType::Text(text) => { - fonts.insert(text.font.clone()); - } LayerDataType::NodeGraphFrame(node_graph_frame) => { if node_graph_frame.cached_output_data == CachedOutputData::None { responses.add(DocumentMessage::NodeGraphFrameGenerate { layer_path: path.clone() }); } + for node in node_graph_frame.network.nodes.values() { + for input in &node.inputs { + if let NodeInput::Value { + tagged_value: TaggedValue::Font(font), + .. + } = input + { + fonts.insert(font.clone()); + } + } + } } _ => {} } diff --git a/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs index 4e46b30c12..56b5227a70 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs @@ -15,7 +15,11 @@ pub enum GraphOperationMessage { layer: LayerIdentifier, fill: Fill, }, - + UpdateBounds { + layer: LayerIdentifier, + old_bounds: [DVec2; 2], + new_bounds: [DVec2; 2], + }, StrokeSet { layer: LayerIdentifier, stroke: Stroke, diff --git a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs index 2267b0557d..c18ee2eaef 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs @@ -157,7 +157,27 @@ impl<'a> ModifyInputsContext<'a> { }); } + fn update_bounds(&mut self, [old_bounds_min, old_bounds_max]: [DVec2; 2], [new_bounds_min, new_bounds_max]: [DVec2; 2]) { + self.modify_inputs("Transform", false, |inputs| { + let layer_transform = transform_utils::get_current_transform(inputs); + let normalized_pivot = transform_utils::get_current_normalized_pivot(inputs); + + let old_layerspace_pivot = (old_bounds_max - old_bounds_min) * normalized_pivot + old_bounds_min; + let new_layerspace_pivot = (new_bounds_max - new_bounds_min) * normalized_pivot + new_bounds_min; + let new_pivot_transform = DAffine2::from_translation(new_layerspace_pivot); + let old_pivot_transform = DAffine2::from_translation(old_layerspace_pivot); + + let transform = new_pivot_transform.inverse() * old_pivot_transform * layer_transform * old_pivot_transform.inverse() * new_pivot_transform; + transform_utils::update_transform(inputs, transform); + }); + } + fn vector_modify(&mut self, modification: VectorDataModification) { + // TODO: Allow modifying a graph with a "Text" node. + if self.network.nodes.values().any(|node| node.name == "Text") { + return; + } + let [mut old_bounds_min, mut old_bounds_max] = [DVec2::ZERO, DVec2::ONE]; let [mut new_bounds_min, mut new_bounds_max] = [DVec2::ZERO, DVec2::ONE]; @@ -186,18 +206,7 @@ impl<'a> ModifyInputsContext<'a> { [new_bounds_min, new_bounds_max] = transform_utils::nonzero_subpath_bounds(subpaths); }); - self.modify_inputs("Transform", false, |inputs| { - let layer_transform = transform_utils::get_current_transform(inputs); - let normalized_pivot = transform_utils::get_current_normalized_pivot(inputs); - - let old_layerspace_pivot = (old_bounds_max - old_bounds_min) * normalized_pivot + old_bounds_min; - let new_layerspace_pivot = (new_bounds_max - new_bounds_min) * normalized_pivot + new_bounds_min; - let new_pivot_transform = DAffine2::from_translation(new_layerspace_pivot); - let old_pivot_transform = DAffine2::from_translation(old_layerspace_pivot); - - let transform = new_pivot_transform.inverse() * old_pivot_transform * layer_transform * old_pivot_transform.inverse() * new_pivot_transform; - transform_utils::update_transform(inputs, transform); - }); + self.update_bounds([old_bounds_min, old_bounds_max], [new_bounds_min, new_bounds_max]); } } @@ -211,7 +220,11 @@ impl MessageHandler { + if let Some(mut modify_inputs) = ModifyInputsContext::new(&layer, document, node_graph, responses) { + modify_inputs.update_bounds(old_bounds, new_bounds); + } + } GraphOperationMessage::StrokeSet { layer, stroke } => { if let Some(mut modify_inputs) = ModifyInputsContext::new(&layer, document, node_graph, responses) { modify_inputs.stroke_set(stroke); diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs index dba7b3b9f2..5357c7cc72 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs @@ -1,4 +1,5 @@ use super::{node_properties, FrontendGraphDataType, FrontendNodeType}; +use crate::consts::{DEFAULT_FONT_FAMILY, DEFAULT_FONT_STYLE}; use crate::messages::layout::utility_types::layout_widget::LayoutGroup; use crate::node_graph_executor::NodeGraphExecutor; @@ -8,6 +9,7 @@ use graph_craft::document::*; use graph_craft::imaginate_input::ImaginateSamplingMethod; use graph_craft::NodeIdentifier; use graphene_core::raster::{BlendMode, Color, Image, ImageFrame, LuminanceCalculation, RedGreenBlue, RelativeAbsolute, SelectiveColorChoice}; +use graphene_core::text::Font; use graphene_core::vector::VectorData; use graphene_core::*; @@ -160,8 +162,8 @@ fn static_nodes() -> Vec { outputs: vec![NodeOutput::new(0, 0), NodeOutput::new(1, 0)], nodes: [DocumentNode { name: "Identity".to_string(), - inputs: vec![NodeInput::Network(concrete!(ImageFrame))], - implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode")), + inputs: vec![NodeInput::Network(concrete!(EditorApi))], + implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ExtractImageFrame")), metadata: Default::default(), }] .into_iter() @@ -193,7 +195,7 @@ fn static_nodes() -> Vec { nodes: [ DocumentNode { name: "SetNode".to_string(), - inputs: vec![NodeInput::Network(concrete!(ImageFrame))], + inputs: vec![NodeInput::Network(concrete!(EditorApi))], implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::SomeNode")), metadata: Default::default(), }, @@ -226,7 +228,7 @@ fn static_nodes() -> Vec { inputs: vec![DocumentInputType { name: "In", data_type: FrontendGraphDataType::Raster, - default: NodeInput::value(TaggedValue::ImageFrame(ImageFrame::empty()), true), + default: NodeInput::value(TaggedValue::EditorApi(EditorApi::empty()), true), }], outputs: vec![ DocumentOutputType { @@ -828,6 +830,19 @@ fn static_nodes() -> Vec { outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], properties: node_properties::no_properties, }, + DocumentNodeType { + name: "Text", + category: "Vector", + identifier: NodeImplementation::proto("graphene_core::text::TextGenerator<_, _, _>"), + inputs: vec![ + DocumentInputType::none(), + DocumentInputType::value("Text", TaggedValue::String("hello world".to_string()), false), + DocumentInputType::value("Font", TaggedValue::Font(Font::new(DEFAULT_FONT_FAMILY.into(), DEFAULT_FONT_STYLE.into())), false), + DocumentInputType::value("Size", TaggedValue::F64(24.), false), + ], + outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], + properties: node_properties::node_section_font, + }, DocumentNodeType { name: "Transform", category: "Transform", @@ -1048,23 +1063,23 @@ pub fn wrap_network_in_scope(network: NodeNetwork) -> NodeNetwork { } pub fn new_image_network(output_offset: i32, output_node_id: NodeId) -> NodeNetwork { - NodeNetwork { + let mut network = NodeNetwork { inputs: vec![0], - outputs: vec![NodeOutput::new(1, 0)], - nodes: [ - resolve_document_node_type("Input Frame") - .expect("Input Frame node does not exist") - .to_document_node_default_inputs([], DocumentNodeMetadata::position((8, 4))), - resolve_document_node_type("Output") - .expect("Output node does not exist") - .to_document_node([NodeInput::node(output_node_id, 0)], DocumentNodeMetadata::position((output_offset + 8, 4))), - ] - .into_iter() - .enumerate() - .map(|(id, node)| (id as NodeId, node)) - .collect(), ..Default::default() - } + }; + network.push_node( + resolve_document_node_type("Input Frame") + .expect("Input Frame node does not exist") + .to_document_node_default_inputs([], DocumentNodeMetadata::position((8, 4))), + false, + ); + network.push_node( + resolve_document_node_type("Output") + .expect("Output node does not exist") + .to_document_node([NodeInput::node(output_node_id, 0)], DocumentNodeMetadata::position((output_offset + 8, 4))), + false, + ); + network } pub fn new_vector_network(subpaths: Vec>) -> NodeNetwork { @@ -1074,27 +1089,48 @@ pub fn new_vector_network(subpaths: Vec NodeNetwork { + let text_generator = resolve_document_node_type("Text").expect("Text node does not exist"); + let transform = resolve_document_node_type("Transform").expect("Transform node does not exist"); + let fill = resolve_document_node_type("Fill").expect("Fill node does not exist"); + let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist"); + let output = resolve_document_node_type("Output").expect("Output node does not exist"); + + let mut network = NodeNetwork { + inputs: vec![0], + ..Default::default() + }; + network.push_node( + text_generator.to_document_node( + [ + NodeInput::Network(concrete!(graphene_core::EditorApi)), + NodeInput::value(TaggedValue::String(text.clone()), false), + NodeInput::value(TaggedValue::Font(font.clone()), false), + NodeInput::value(TaggedValue::F64(size), false), + ], + DocumentNodeMetadata::position((0, 4)), + ), + false, + ); + network.push_node(transform.to_document_node_default_inputs([None], Default::default()), true); + network.push_node(fill.to_document_node_default_inputs([None], Default::default()), true); + network.push_node(stroke.to_document_node_default_inputs([None], Default::default()), true); + network.push_node(output.to_document_node_default_inputs([None], Default::default()), true); + network } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index 7c9b208b72..d762771419 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -9,7 +9,9 @@ use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, NodeId, NodeInput}; use graph_craft::imaginate_input::*; use graphene_core::raster::{BlendMode, Color, LuminanceCalculation, RedGreenBlue, RelativeAbsolute, SelectiveColorChoice}; +use graphene_core::text::Font; use graphene_core::vector::style::{FillType, GradientType, LineCap, LineJoin}; +use graphene_core::EditorApi; use super::document_node_types::NodePropertiesContext; use super::{FrontendGraphDataType, IMAGINATE_NODE}; @@ -155,6 +157,38 @@ fn vec_f32_input(document_node: &DocumentNode, node_id: NodeId, index: usize, na } widgets } +fn font_inputs(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> (Vec, Option>) { + let mut first_widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); + let mut second_widgets = None; + + let from_font_input = |font: &FontInput| TaggedValue::Font(Font::new(font.font_family.clone(), font.font_style.clone())); + + if let NodeInput::Value { + tagged_value: TaggedValue::Font(font), + exposed: false, + } = &document_node.inputs[index] + { + first_widgets.extend_from_slice(&[ + WidgetHolder::unrelated_separator(), + FontInput::new(font.font_family.clone(), font.font_style.clone()) + .on_update(update_value(from_font_input, node_id, index)) + .widget_holder(), + ]); + second_widgets = Some(vec![ + TextLabel::new("").widget_holder(), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), + WidgetHolder::unrelated_separator(), + FontInput::new(font.font_family.clone(), font.font_style.clone()) + .is_style_picker(true) + .on_update(update_value(from_font_input, node_id, index)) + .widget_holder(), + ]); + } + (first_widgets, second_widgets) +} fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, number_props: NumberInput, blank_assist: bool) -> Vec { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Number, blank_assist); @@ -847,6 +881,19 @@ pub fn transform_properties(document_node: &DocumentNode, node_id: NodeId, _cont vec![translation, rotation, scale] } +pub fn node_section_font(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let text = text_area_widget(document_node, node_id, 1, "Text", true); + let (font, style) = font_inputs(document_node, node_id, 2, "Font", true); + let size = number_widget(document_node, node_id, 3, "Size", NumberInput::default().unit(" px").min(1.), true); + + let mut result = vec![LayoutGroup::Row { widgets: text }, LayoutGroup::Row { widgets: font }]; + if let Some(style) = style { + result.push(LayoutGroup::Row { widgets: style }); + } + result.push(LayoutGroup::Row { widgets: size }); + result +} + pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { let imaginate_node = [context.nested_path, &[node_id]].concat(); let layer_path = context.layer_path.to_vec(); @@ -1098,9 +1145,9 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte }; // Create the input to the graph using an empty image - let image_frame = std::borrow::Cow::Owned(graphene_core::raster::ImageFrame { - image: graphene_core::raster::Image::empty(), - transform: glam::DAffine2::IDENTITY, + let image_frame = std::borrow::Cow::Owned(EditorApi { + image_frame: None, + font_cache: Some(&context.persistent_data.font_cache), }); // Compute the transform input to the node graph frame let image_frame: graphene_core::raster::ImageFrame = context.executor.compute_input(context.network, &imaginate_node, 0, image_frame).unwrap_or_default(); diff --git a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message.rs b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message.rs index 03c4948569..ad823a84a8 100644 --- a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message.rs +++ b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message.rs @@ -19,11 +19,9 @@ pub enum PropertiesPanelMessage { Deactivate, Init, ModifyFill { fill: Fill }, - ModifyFont { font_family: String, font_style: String, size: f64 }, ModifyName { name: String }, ModifyPreserveAspect { preserve_aspect: bool }, ModifyStroke { stroke: Stroke }, - ModifyText { new_text: String }, ModifyTransform { value: f64, transform_op: TransformOp }, ResendActiveProperties, SetActiveLayers { paths: Vec>, document: TargetDocument }, diff --git a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs index 1e40772a30..037c527f73 100644 --- a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs @@ -94,12 +94,6 @@ impl<'a> MessageHandler { - let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - - self.create_document_operation(Operation::ModifyFont { path, font_family, font_style, size }, true, responses); - responses.push_back(ResendActiveProperties.into()); - } ModifyTransform { value, transform_op } => { let (path, target_document) = self.active_selection.as_ref().expect("Received update for properties panel with no active layer"); let layer = get_document(*target_document).layer(path).unwrap(); @@ -124,10 +118,6 @@ impl<'a> MessageHandler { - let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - self.create_document_operation(Operation::SetTextContent { path, new_text }, true, responses); - } SetPivot { new_position } => { let (layer, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); let position: Option = new_position.into(); diff --git a/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs b/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs index 88af51c79a..e29072e17a 100644 --- a/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs +++ b/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs @@ -4,7 +4,7 @@ use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, use crate::messages::layout::utility_types::misc::LayoutTarget; use crate::messages::layout::utility_types::widgets::assist_widgets::PivotAssist; use crate::messages::layout::utility_types::widgets::button_widgets::{IconButton, PopoverButton, TextButton}; -use crate::messages::layout::utility_types::widgets::input_widgets::{CheckboxInput, ColorInput, FontInput, NumberInput, NumberInputMode, RadioEntryData, RadioInput, TextAreaInput, TextInput}; +use crate::messages::layout::utility_types::widgets::input_widgets::{CheckboxInput, ColorInput, NumberInput, NumberInputMode, RadioEntryData, RadioInput, TextInput}; use crate::messages::layout::utility_types::widgets::label_widgets::{IconLabel, TextLabel}; use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::prelude::*; @@ -13,7 +13,6 @@ use crate::node_graph_executor::NodeGraphExecutor; use document_legacy::document::Document; use document_legacy::layers::layer_info::{Layer, LayerDataType, LayerDataTypeDiscriminant}; use document_legacy::layers::style::{Fill, Gradient, GradientType, LineCap, LineJoin, RenderData, Stroke, ViewMode}; -use document_legacy::layers::text_layer::TextLayer; use graphene_core::raster::color::Color; use glam::{DAffine2, DVec2}; @@ -265,11 +264,6 @@ pub fn register_artwork_layer_properties( tooltip: "Shape".into(), ..Default::default() })), - LayerDataType::Text(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { - icon: "NodeText".into(), - tooltip: "Text".into(), - ..Default::default() - })), LayerDataType::NodeGraphFrame(_) => WidgetHolder::new(Widget::IconLabel(IconLabel { icon: "Layer".into(), tooltip: "Layer".into(), @@ -311,14 +305,6 @@ pub fn register_artwork_layer_properties( vec![node_section_transform(layer, persistent_data), node_section_stroke(&shape.style.stroke().unwrap_or_default())] } } - LayerDataType::Text(text) => { - vec![ - node_section_transform(layer, persistent_data), - node_section_font(text), - node_section_fill(text.path_style.fill()).expect("Text should have fill"), - node_section_stroke(&text.path_style.stroke().unwrap_or_default()), - ] - } LayerDataType::NodeGraphFrame(node_graph_frame) => { let mut properties_sections = Vec::new(); @@ -522,115 +508,6 @@ fn node_section_transform(layer: &Layer, persistent_data: &PersistentData) -> La } } -fn node_section_font(layer: &TextLayer) -> LayoutGroup { - let font = layer.font.clone(); - let size = layer.size; - LayoutGroup::Section { - name: "Font".into(), - layout: vec![ - LayoutGroup::Row { - widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Text".into(), - ..TextLabel::default() - })), - WidgetHolder::unrelated_separator(), - WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, - WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. - WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. - WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::TextAreaInput(TextAreaInput { - value: layer.text.clone(), - on_update: WidgetCallback::new(|text_area: &TextAreaInput| PropertiesPanelMessage::ModifyText { new_text: text_area.value.clone() }.into()), - ..Default::default() - })), - ], - }, - LayoutGroup::Row { - widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Font".into(), - ..TextLabel::default() - })), - WidgetHolder::unrelated_separator(), - WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, - WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. - WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. - WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::FontInput(FontInput { - is_style_picker: false, - font_family: layer.font.font_family.clone(), - font_style: layer.font.font_style.clone(), - on_update: WidgetCallback::new(move |font_input: &FontInput| { - PropertiesPanelMessage::ModifyFont { - font_family: font_input.font_family.clone(), - font_style: font_input.font_style.clone(), - size, - } - .into() - }), - ..Default::default() - })), - ], - }, - LayoutGroup::Row { - widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Style".into(), - ..TextLabel::default() - })), - WidgetHolder::unrelated_separator(), - WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, - WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. - WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. - WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::FontInput(FontInput { - is_style_picker: true, - font_family: layer.font.font_family.clone(), - font_style: layer.font.font_style.clone(), - on_update: WidgetCallback::new(move |font_input: &FontInput| { - PropertiesPanelMessage::ModifyFont { - font_family: font_input.font_family.clone(), - font_style: font_input.font_style.clone(), - size, - } - .into() - }), - ..Default::default() - })), - ], - }, - LayoutGroup::Row { - widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Size".into(), - ..TextLabel::default() - })), - WidgetHolder::unrelated_separator(), - WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, - WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. - WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. - WidgetHolder::unrelated_separator(), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some(layer.size), - min: Some(1.), - unit: " px".into(), - on_update: WidgetCallback::new(move |number_input: &NumberInput| { - PropertiesPanelMessage::ModifyFont { - font_family: font.font_family.clone(), - font_style: font.font_style.clone(), - size: number_input.value.unwrap(), - } - .into() - }), - ..Default::default() - })), - ], - }, - ], - } -} - fn node_gradient_type(gradient: &Gradient) -> LayoutGroup { let selected_index = match gradient.gradient_type { GradientType::Linear => 0, diff --git a/editor/src/messages/portfolio/document/utility_types/transformation.rs b/editor/src/messages/portfolio/document/utility_types/transformation.rs index bf40d4a1a2..4a939e1239 100644 --- a/editor/src/messages/portfolio/document/utility_types/transformation.rs +++ b/editor/src/messages/portfolio/document/utility_types/transformation.rs @@ -327,7 +327,6 @@ impl<'a> Selected<'a> { continue; }; let Some(vector_data) = layer.as_vector_data() else { - warn!("Didn't find a vectordata for {:?}", layer); continue; }; let get_manipulator_point_position = |point_id: ManipulatorPointId| { diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index bae83bfae0..e4de83e5fb 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -2,10 +2,10 @@ use super::utility_types::ImaginateServerStatus; use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; use crate::messages::prelude::*; -use document_legacy::layers::text_layer::Font; use document_legacy::LayerId; use graph_craft::document::NodeId; use graph_craft::imaginate_input::ImaginateStatus; +use graphene_core::text::Font; use serde::{Deserialize, Serialize}; diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index edc4319ff6..2a77b9dafa 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -12,12 +12,13 @@ use crate::messages::prelude::*; use crate::messages::tool::utility_types::{HintData, HintGroup}; use crate::node_graph_executor::NodeGraphExecutor; -use document_legacy::layers::layer_info::LayerDataTypeDiscriminant; +use document_legacy::layers::layer_info::LayerDataType; use document_legacy::layers::style::RenderData; -use document_legacy::layers::text_layer::Font; use document_legacy::Operation as DocumentOperation; use graph_craft::document::value::TaggedValue; +use graph_craft::document::NodeInput; use graphene_core::raster::Image; +use graphene_core::text::Font; #[derive(Debug, Clone, Default)] pub struct PortfolioMessageHandler { @@ -206,13 +207,15 @@ impl MessageHandler { - self.persistent_data.font_cache.insert(Font::new(font_family, font_style), preview_url, data, is_default); + let font = Font::new(font_family, font_style); if let Some(document) = self.active_document_mut() { - document.document_legacy.mark_all_layers_of_type_as_dirty(LayerDataTypeDiscriminant::Text); + Self::uploaded_new_font(document, &font, responses); responses.push_back(DocumentMessage::RenderDocument.into()); responses.push_back(BroadcastEvent::DocumentIsDirty.into()); } + + self.persistent_data.font_cache.insert(font, preview_url, data, is_default); } PortfolioMessage::ImaginateCheckServerStatus => { self.persistent_data.imaginate_server_status = ImaginateServerStatus::Checking; @@ -688,4 +691,31 @@ impl PortfolioMessageHandler { fn document_index(&self, document_id: u64) -> usize { self.document_ids.iter().position(|id| id == &document_id).expect("Active document is missing from document ids") } + + fn uploaded_new_font(document: &mut DocumentMessageHandler, target_font: &Font, responses: &mut VecDeque) { + let mut stack = vec![(&document.document_legacy.root, Vec::new())]; + + while let Some((layer, layer_path)) = stack.pop() { + match &layer.data { + LayerDataType::Folder(folder) => stack.extend(folder.layers.iter().zip(folder.layer_ids.iter().map(|id| { + let mut x = layer_path.clone(); + x.push(*id); + x + }))), + LayerDataType::NodeGraphFrame(graph_frame) => { + let input_is_font = |input: &NodeInput| { + let NodeInput::Value { tagged_value: TaggedValue::Font(font), .. } = input else { + return false; + }; + font == target_font + }; + let should_rerender = graph_frame.network.nodes.values().any(|node| node.inputs.iter().any(input_is_font)); + if should_rerender { + responses.add(DocumentMessage::NodeGraphFrameGenerate { layer_path }); + } + } + _ => {} + } + } + } } diff --git a/editor/src/messages/portfolio/utility_types.rs b/editor/src/messages/portfolio/utility_types.rs index cc3a111d59..657ebd7b54 100644 --- a/editor/src/messages/portfolio/utility_types.rs +++ b/editor/src/messages/portfolio/utility_types.rs @@ -1,4 +1,4 @@ -use document_legacy::layers::text_layer::FontCache; +use graphene_std::text::FontCache; use serde::{Deserialize, Serialize}; diff --git a/editor/src/messages/tool/common_functionality/path_outline.rs b/editor/src/messages/tool/common_functionality/path_outline.rs index 2218aa49d7..aaa9b5988e 100644 --- a/editor/src/messages/tool/common_functionality/path_outline.rs +++ b/editor/src/messages/tool/common_functionality/path_outline.rs @@ -34,7 +34,6 @@ impl PathOutline { // Get the bezpath from the shape or text let subpath = match &document_layer.data { LayerDataType::Shape(layer_shape) => Some(layer_shape.shape.clone()), - LayerDataType::Text(text) => Some(text.to_subpath_nonmut(render_data)), LayerDataType::NodeGraphFrame(frame) => frame.as_vector_data().map(|vector_data| Subpath::from_bezier_crate(&vector_data.subpaths)), _ => document_layer.aabb_for_transform(DAffine2::IDENTITY, render_data).map(|[p1, p2]| Subpath::new_rect(p1, p2)), }?; diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index beab8495ea..433b874719 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -863,9 +863,11 @@ impl Fsm for SelectToolFsmState { // Check that only one layer is selected if selected_layers.next().is_none() { if let Ok(layer) = document.document_legacy.layer(layer_path) { - if let LayerDataType::Text(_) = layer.data { - responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }.into()); - responses.push_back(TextToolMessage::EditSelected.into()); + if let Ok(network) = layer.as_node_graph() { + if network.nodes.values().any(|node| node.name == "Text") { + responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }.into()); + responses.push_back(TextToolMessage::EditSelected.into()); + } } } } @@ -1249,15 +1251,16 @@ fn edit_layer_shallowest_manipulation(document: &DocumentMessageHandler, interse fn edit_layer_deepest_manipulation(intersect: &Layer, responses: &mut VecDeque) { match &intersect.data { - LayerDataType::Text(_) => { - responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }.into()); - responses.push_back(TextToolMessage::Interact.into()); - } LayerDataType::Shape(_) => { responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }.into()); } - LayerDataType::NodeGraphFrame(frame) if frame.as_vector_data().is_some() => { - responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }.into()); + LayerDataType::NodeGraphFrame(graph_frame) if graph_frame.as_vector_data().is_some() => { + if graph_frame.network.nodes.values().any(|node| node.name == "Text") { + responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }.into()); + responses.push_back(TextToolMessage::EditSelected.into()); + } else { + responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }.into()); + } } _ => {} } diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 9c85a3a2ee..3eea602e74 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -5,17 +5,22 @@ use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMot use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; use crate::messages::layout::utility_types::widgets::input_widgets::{FontInput, NumberInput}; +use crate::messages::portfolio::document::node_graph::new_text_network; use crate::messages::prelude::*; use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; use document_legacy::intersection::Quad; -use document_legacy::layers::layer_info::LayerDataType; +use document_legacy::layers::layer_info::Layer; use document_legacy::layers::style::{self, Fill, RenderData, Stroke}; use document_legacy::LayerId; use document_legacy::Operation; use glam::{DAffine2, DVec2}; +use graph_craft::document::value::TaggedValue; +use graph_craft::document::{DocumentNode, NodeId, NodeInput, NodeNetwork}; +use graphene_core::text::{load_face, Font}; +use graphene_core::Color; use serde::{Deserialize, Serialize}; #[derive(Default)] @@ -166,7 +171,7 @@ impl ToolTransition for TextTool { EventToMessageMap { document_dirty: Some(TextToolMessage::DocumentIsDirty.into()), tool_abort: Some(TextToolMessage::Abort.into()), - selection_changed: None, + selection_changed: Some(TextToolMessage::DocumentIsDirty.into()), } } } @@ -177,17 +182,175 @@ enum TextToolFsmState { Ready, Editing, } +#[derive(Clone, Debug)] +pub struct EditingText { + text: String, + font: Font, + font_size: f64, + color: Color, + transform: DAffine2, +} + #[derive(Clone, Debug, Default)] struct TextToolData { layer_path: Vec, overlays: Vec>, + editing_text: Option, + new_text: String, } impl TextToolData { /// Set the editing state of the currently modifying layer - fn set_editing(&self, editable: bool, responses: &mut VecDeque) { + fn set_editing(&self, editable: bool, render_data: &RenderData, responses: &mut VecDeque) { let path = self.layer_path.clone(); - responses.push_back(DocumentMessage::SetTextboxEditability { path, editable }.into()); + responses.add(Operation::SetLayerVisibility { path, visible: !editable }); + + if let Some(editing_text) = self.editing_text.as_ref().filter(|_| editable) { + responses.add(FrontendMessage::DisplayEditableTextbox { + text: editing_text.text.clone(), + line_width: None, + font_size: editing_text.font_size, + color: editing_text.color, + url: render_data.font_cache.get_preview_url(&editing_text.font).cloned().unwrap_or_default(), + transform: editing_text.transform.to_cols_array(), + }); + } else { + responses.add(FrontendMessage::DisplayRemoveEditableTextbox); + } + } + + fn load_layer_text_node(&mut self, document: &DocumentMessageHandler) -> Option<()> { + let transform = document.document_legacy.multiply_transforms(&self.layer_path).ok()?; + let layer = document.document_legacy.layer(&self.layer_path).ok()?; + let color = layer + .style() + .ok() + .map_or(Color::BLACK, |style| if let Fill::Solid(solid_color) = style.fill() { *solid_color } else { Color::BLACK }); + + let network = get_network(&self.layer_path, document)?; + let node_id = get_text_node_id(network)?; + let node = network.nodes.get(&node_id)?; + + let (text, font, font_size) = Self::extract_text_node_inputs(node)?; + self.editing_text = Some(EditingText { + text: text.clone(), + font: font.clone(), + font_size, + color, + transform, + }); + self.new_text = text.clone(); + Some(()) + } + + fn start_editing_layer(&mut self, layer_path: &[LayerId], tool_state: TextToolFsmState, document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque) { + if tool_state == TextToolFsmState::Editing { + self.set_editing(false, render_data, responses); + } + + self.layer_path = layer_path.into(); + self.load_layer_text_node(document); + + responses.add(DocumentMessage::StartTransaction); + + self.set_editing(true, render_data, responses); + + let replacement_selected_layers = vec![self.layer_path.clone()]; + responses.add(DocumentMessage::SetSelectedLayers { replacement_selected_layers }); + } + + fn extract_text_node_inputs(node: &DocumentNode) -> Option<(&String, &Font, f64)> { + let NodeInput::Value { tagged_value: TaggedValue::String(text), .. } = &node.inputs[1] else { return None; }; + let NodeInput::Value { tagged_value: TaggedValue::Font(font), .. } = &node.inputs[2] else { return None; }; + let NodeInput::Value { tagged_value: TaggedValue::F64(font_size), .. } = &node.inputs[3] else { return None; }; + Some((text, font, *font_size)) + } + + fn interact(&mut self, state: TextToolFsmState, mouse: DVec2, document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque) -> TextToolFsmState { + let tolerance = DVec2::splat(SELECTION_TOLERANCE); + let quad = Quad::from_box([mouse - tolerance, mouse + tolerance]); + + // Check if the user has selected an existing text layer + if let Some(clicked_text_layer_path) = document.document_legacy.intersects_quad_root(quad, render_data).last().filter(|l| is_text_layer(document, l)) { + self.start_editing_layer(clicked_text_layer_path, state, document, render_data, responses); + + TextToolFsmState::Editing + } + // Create new text + else if let Some(editing_text) = self.editing_text.as_ref().filter(|_| state == TextToolFsmState::Ready) { + responses.add(DocumentMessage::StartTransaction); + + let network = new_text_network(String::new(), editing_text.font.clone(), editing_text.font_size as f64); + + responses.add(Operation::AddNodeGraphFrame { + path: self.layer_path.clone(), + insert_index: -1, + transform: DAffine2::ZERO.to_cols_array(), + network, + }); + responses.add(GraphOperationMessage::FillSet { + layer: self.layer_path.clone(), + fill: Fill::solid(editing_text.color), + }); + responses.add(GraphOperationMessage::TransformSet { + layer: self.layer_path.clone(), + transform: editing_text.transform, + transform_in: TransformIn::Viewport, + skip_rerender: true, + }); + + self.set_editing(true, render_data, responses); + + let replacement_selected_layers = vec![self.layer_path.clone()]; + + responses.add(DocumentMessage::SetSelectedLayers { replacement_selected_layers }); + + TextToolFsmState::Editing + } else { + // Removing old text as editable + self.set_editing(false, render_data, responses); + + resize_overlays(&mut self.overlays, responses, 0); + + TextToolFsmState::Ready + } + } + + pub fn update_bounds_overlay(&mut self, document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque) -> Option<()> { + resize_overlays(&mut self.overlays, responses, 1); + + let editing_text = self.editing_text.as_ref()?; + let buzz_face = render_data.font_cache.get(&editing_text.font).map(|data| load_face(&data)); + let far = graphene_core::text::bounding_box(&self.new_text, buzz_face, editing_text.font_size, None); + let quad = Quad::from_box([DVec2::ZERO, far]); + + let transformed_quad = document.document_legacy.multiply_transforms(&self.layer_path).ok()? * quad; + let bounds = transformed_quad.bounding_box(); + + let operation = Operation::SetLayerTransformInViewport { + path: self.overlays[0].clone(), + transform: transform_from_box(bounds[0], bounds[1]), + }; + responses.add(DocumentMessage::Overlays(operation.into())); + Some(()) + } + + fn get_bounds(&self, text: &str, render_data: &RenderData) -> Option<[DVec2; 2]> { + let editing_text = self.editing_text.as_ref()?; + let buzz_face = render_data.font_cache.get(&editing_text.font).map(|data| load_face(&data)); + let subpaths = graphene_core::text::to_path(&text, buzz_face, editing_text.font_size, None); + let bounds = subpaths.iter().filter_map(|subpath| subpath.bounding_box()); + let combined_bounds = bounds.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])]).unwrap_or_default(); + Some(combined_bounds) + } + + fn fix_text_bounds(&self, new_text: &str, _document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque) -> Option<()> { + let layer = self.layer_path.clone(); + let old_bounds = self.get_bounds(&self.editing_text.as_ref()?.text, render_data)?; + let new_bounds = self.get_bounds(new_text, render_data)?; + responses.add(GraphOperationMessage::UpdateBounds { layer, old_bounds, new_bounds }); + + Some(()) } } @@ -198,7 +361,7 @@ fn transform_from_box(pos1: DVec2, pos2: DVec2) -> [f64; 6] { fn resize_overlays(overlays: &mut Vec>, responses: &mut VecDeque, newlen: usize) { while overlays.len() > newlen { let operation = Operation::DeleteLayer { path: overlays.pop().unwrap() }; - responses.push_back(DocumentMessage::Overlays(operation.into()).into()); + responses.add(DocumentMessage::Overlays(operation.into())); } while overlays.len() < newlen { let path = vec![generate_uuid()]; @@ -210,206 +373,152 @@ fn resize_overlays(overlays: &mut Vec>, responses: &mut VecDeque, render_data: &RenderData) { - let visible_text_layers = document.selected_visible_text_layers().collect::>(); - resize_overlays(&mut tool_data.overlays, responses, visible_text_layers.len()); - - let bounds = visible_text_layers - .into_iter() - .zip(&tool_data.overlays) - .filter_map(|(layer_path, overlay_path)| { - document - .document_legacy - .layer(layer_path) - .unwrap() - .aabb_for_transform(document.document_legacy.multiply_transforms(layer_path).unwrap(), render_data) - .map(|bounds| (bounds, overlay_path)) - }) - .collect::>(); + let get_bounds = |layer: &Layer, path: &[LayerId], document: &DocumentMessageHandler, render_data: &RenderData| { + let node_graph = layer.as_node_graph().ok()?; + let node_id = get_text_node_id(node_graph)?; + let document_node = node_graph.nodes.get(&node_id)?; + let (text, font, font_size) = TextToolData::extract_text_node_inputs(document_node)?; + let buzz_face = render_data.font_cache.get(font).map(|data| load_face(&data)); + let far = graphene_core::text::bounding_box(&text, buzz_face, font_size, None); + let quad = Quad::from_box([DVec2::ZERO, far]); + let multiplied = document.document_legacy.multiply_transforms(path).ok()? * quad; + Some(multiplied.bounding_box()) + }; + let bounds = document.selected_layers().filter_map(|path| match document.document_legacy.layer(path) { + Ok(layer) => get_bounds(layer, path, document, render_data), + Err(_) => None, + }); + let bounds = bounds.collect::>(); let new_len = bounds.len(); - for (bounds, overlay_path) in bounds { + for (bounds, overlay_path) in bounds.iter().zip(&tool_data.overlays) { let operation = Operation::SetLayerTransformInViewport { path: overlay_path.to_vec(), transform: transform_from_box(bounds[0], bounds[1]), }; - responses.push_back(DocumentMessage::Overlays(operation.into()).into()); + responses.add(DocumentMessage::Overlays(operation.into())); } resize_overlays(&mut tool_data.overlays, responses, new_len); } -fn set_edit_layer(layer_path: &[LayerId], tool_state: TextToolFsmState, tool_data: &mut TextToolData, responses: &mut VecDeque) { - if tool_state == TextToolFsmState::Editing { - tool_data.set_editing(false, responses); +fn get_network<'a>(layer_path: &[LayerId], document: &'a DocumentMessageHandler) -> Option<&'a NodeNetwork> { + let layer = document.document_legacy.layer(layer_path).ok()?; + layer.as_node_graph().ok() +} + +fn get_text_node_id(network: &NodeNetwork) -> Option { + network.nodes.iter().find(|(_, node)| node.name == "Text").map(|(&id, _)| id) +} + +fn is_text_layer(document: &DocumentMessageHandler, layer_path: &[LayerId]) -> bool { + let Some(network) = get_network(layer_path, document) else { return false; }; + get_text_node_id(network).is_some() +} + +fn can_edit_selected(document: &DocumentMessageHandler) -> Option> { + let mut selected_layers = document.selected_layers(); + + let layer_path = selected_layers.next()?.to_vec(); + // Check that only one layer is selected + if selected_layers.next().is_some() { + return None; } - tool_data.layer_path = layer_path.into(); + if !is_text_layer(document, &layer_path) { + return None; + } - responses.push_back(DocumentMessage::StartTransaction.into()); - - tool_data.set_editing(true, responses); - - let replacement_selected_layers = vec![tool_data.layer_path.clone()]; - responses.push_back(DocumentMessage::SetSelectedLayers { replacement_selected_layers }.into()); + Some(layer_path) } impl Fsm for TextToolFsmState { type ToolData = TextToolData; type ToolOptions = TextOptions; - fn transition( - self, - event: ToolMessage, - tool_data: &mut Self::ToolData, - ToolActionHandlerData { + fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, transition_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque) -> Self { + let ToolActionHandlerData { document, global_tool_data, input, render_data, .. - }: &mut ToolActionHandlerData, - tool_options: &Self::ToolOptions, - responses: &mut VecDeque, - ) -> Self { - use TextToolFsmState::*; - use TextToolMessage::*; - + } = transition_data; if let ToolMessage::Text(event) = event { match (self, event) { - (state, DocumentIsDirty) => { + (TextToolFsmState::Editing, TextToolMessage::DocumentIsDirty) => { + responses.add(FrontendMessage::DisplayEditableTextboxTransform { + transform: document.document_legacy.multiply_transforms(&tool_data.layer_path).ok().unwrap_or_default().to_cols_array(), + }); + tool_data.update_bounds_overlay(document, render_data, responses); + TextToolFsmState::Editing + } + (state, TextToolMessage::DocumentIsDirty) => { update_overlays(document, tool_data, responses, render_data); state } - (state, Interact) => { - let mouse_pos = input.mouse.position; - let tolerance = DVec2::splat(SELECTION_TOLERANCE); - let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]); + (state, TextToolMessage::Interact) => { + tool_data.editing_text = Some(EditingText { + text: String::new(), + transform: DAffine2::from_translation(input.mouse.position), + font_size: tool_options.font_size as f64, + font: Font::new(tool_options.font_name.clone(), tool_options.font_style.clone()), + color: global_tool_data.primary_color, + }); + tool_data.new_text = String::new(); + tool_data.layer_path = document.get_path_for_new_layer(); - // Check if the user has selected an existing text layer - let new_state = if let Some(clicked_text_layer_path) = document - .document_legacy - .intersects_quad_root(quad, render_data) - .last() - .filter(|l| document.document_legacy.layer(l).map(|l| l.as_text().is_ok()).unwrap_or(false)) - { - set_edit_layer(clicked_text_layer_path, state, tool_data, responses); - - Editing - } - // Create new text - else if state == TextToolFsmState::Ready { - responses.push_back(DocumentMessage::StartTransaction.into()); - - let transform = DAffine2::from_translation(input.mouse.position); - let font_size = tool_options.font_size; - let font_name = tool_options.font_name.clone(); - let font_style = tool_options.font_style.clone(); - tool_data.layer_path = document.get_path_for_new_layer(); - - responses.push_back( - Operation::AddText { - path: tool_data.layer_path.clone(), - transform: DAffine2::ZERO.to_cols_array(), - insert_index: -1, - text: String::new(), - style: style::PathStyle::new(None, Fill::solid(global_tool_data.primary_color)), - size: font_size as f64, - font_name, - font_style, - } - .into(), - ); - responses.push_back( - GraphOperationMessage::TransformSet { - layer: tool_data.layer_path.clone(), - transform, - transform_in: TransformIn::Viewport, - skip_rerender: false, - } - .into(), - ); - - tool_data.set_editing(true, responses); - - let replacement_selected_layers = vec![tool_data.layer_path.clone()]; - - responses.push_back(DocumentMessage::SetSelectedLayers { replacement_selected_layers }.into()); - - Editing - } else { - // Removing old text as editable - tool_data.set_editing(false, responses); - - resize_overlays(&mut tool_data.overlays, responses, 0); - - Ready - }; - - new_state + tool_data.interact(state, input.mouse.position, document, &render_data, responses) } - (state, EditSelected) => { - let mut selected_layers = document.selected_layers(); - - if let Some(layer_path) = selected_layers.next() { - // Check that only one layer is selected - if selected_layers.next().is_none() { - if let Ok(layer) = document.document_legacy.layer(layer_path) { - if let LayerDataType::Text(_) = layer.data { - set_edit_layer(layer_path, state, tool_data, responses); - - return Editing; - } - } - } + (state, TextToolMessage::EditSelected) => { + if let Some(layer_path) = can_edit_selected(document) { + tool_data.start_editing_layer(&layer_path, state, document, render_data, responses); + return TextToolFsmState::Editing; } state } - (state, Abort) => { + (state, TextToolMessage::Abort) => { if state == TextToolFsmState::Editing { - tool_data.set_editing(false, responses); + tool_data.set_editing(false, render_data, responses); } resize_overlays(&mut tool_data.overlays, responses, 0); - Ready + TextToolFsmState::Ready } - (Editing, CommitText) => { - responses.push_back(FrontendMessage::TriggerTextCommit.into()); + (TextToolFsmState::Editing, TextToolMessage::CommitText) => { + responses.add(FrontendMessage::TriggerTextCommit); - Editing + TextToolFsmState::Editing } - (Editing, TextChange { new_text }) => { - let path = tool_data.layer_path.clone(); - responses.push_back(Operation::SetTextContent { path, new_text }.into()); + (TextToolFsmState::Editing, TextToolMessage::TextChange { new_text }) => { + let layer_path = tool_data.layer_path.clone(); + let network = get_network(&layer_path, document).unwrap(); + tool_data.fix_text_bounds(&new_text, document, render_data, responses); + responses.add(NodeGraphMessage::SetQualifiedInputValue { + layer_path, + node_path: vec![get_text_node_id(network).unwrap()], + input_index: 1, + value: TaggedValue::String(new_text), + }); - tool_data.set_editing(false, responses); + tool_data.set_editing(false, render_data, responses); resize_overlays(&mut tool_data.overlays, responses, 0); - Ready + TextToolFsmState::Ready } - (Editing, UpdateBounds { new_text }) => { - resize_overlays(&mut tool_data.overlays, responses, 1); - let text = document.document_legacy.layer(&tool_data.layer_path).unwrap().as_text().unwrap(); - let quad = text.bounding_box(&new_text, text.load_face(render_data)); - - let transformed_quad = document.document_legacy.multiply_transforms(&tool_data.layer_path).unwrap() * quad; - let bounds = transformed_quad.bounding_box(); - - let operation = Operation::SetLayerTransformInViewport { - path: tool_data.overlays[0].clone(), - transform: transform_from_box(bounds[0], bounds[1]), - }; - responses.push_back(DocumentMessage::Overlays(operation.into()).into()); - - Editing + (TextToolFsmState::Editing, TextToolMessage::UpdateBounds { new_text }) => { + tool_data.new_text = new_text; + tool_data.update_bounds_overlay(document, render_data, responses); + TextToolFsmState::Editing } _ => self, } @@ -427,10 +536,10 @@ impl Fsm for TextToolFsmState { ])]), }; - responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into()); + responses.add(FrontendMessage::UpdateInputHints { hint_data }); } fn update_cursor(&self, responses: &mut VecDeque) { - responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Text }.into()); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Text }); } } diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index acc7c5a192..6c965bdcc0 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -12,7 +12,7 @@ use graph_craft::executor::Compiler; use graph_craft::{concrete, Type, TypeDescriptor}; use graphene_core::raster::{Image, ImageFrame}; use graphene_core::vector::VectorData; -use graphene_core::Color; +use graphene_core::{Color, EditorApi}; use interpreted_executor::executor::DynamicExecutor; use glam::{DAffine2, DVec2}; @@ -25,7 +25,7 @@ pub struct NodeGraphExecutor { impl NodeGraphExecutor { /// Execute the network by flattening it and creating a borrow stack. - fn execute_network<'a>(&'a mut self, network: NodeNetwork, image_frame: ImageFrame) -> Result, String> { + fn execute_network<'a>(&'a mut self, network: NodeNetwork, editor_api: EditorApi<'a>) -> Result, String> { let mut scoped_network = wrap_network_in_scope(network); scoped_network.duplicate_outputs(&mut generate_uuid); @@ -45,14 +45,14 @@ impl NodeGraphExecutor { use graph_craft::executor::Executor; match self.executor.input_type() { - Some(t) if t == concrete!(ImageFrame) => self.executor.execute(image_frame.into_dyn()).map_err(|e| e.to_string()), + Some(t) if t == concrete!(EditorApi) => self.executor.execute(editor_api.into_dyn()).map_err(|e| e.to_string()), Some(t) if t == concrete!(()) => self.executor.execute(().into_dyn()).map_err(|e| e.to_string()), _ => Err("Invalid input type".to_string()), } } /// Computes an input for a node in the graph - pub fn compute_input(&mut self, old_network: &NodeNetwork, node_path: &[NodeId], mut input_index: usize, image_frame: Cow>) -> Result { + pub fn compute_input(&mut self, old_network: &NodeNetwork, node_path: &[NodeId], mut input_index: usize, editor_api: Cow>) -> Result { let mut network = old_network.clone(); // Adjust the output of the graph so we find the relevant output 'outer: for end in (0..node_path.len()).rev() { @@ -88,7 +88,7 @@ impl NodeGraphExecutor { } } - let boxed = self.execute_network(network, image_frame.into_owned())?; + let boxed = self.execute_network(network, editor_api.into_owned())?; dyn_any::downcast::(boxed).map(|v| *v) } @@ -119,7 +119,7 @@ impl NodeGraphExecutor { imaginate_node: Vec, (document, document_id): (&mut DocumentMessageHandler, u64), layer_path: Vec, - image_frame: ImageFrame, + editor_api: EditorApi<'_>, (preferences, persistent_data): (&PreferencesMessageHandler, &PersistentData), ) -> Result { use crate::messages::portfolio::document::node_graph::IMAGINATE_NODE; @@ -131,31 +131,31 @@ impl NodeGraphExecutor { let layer = document.document_legacy.layer(&layer_path).map_err(|e| format!("No layer: {e:?}"))?; let transform = layer.transform; - let resolution: Option = self.compute_input(&network, &imaginate_node, get("Resolution"), Cow::Borrowed(&image_frame))?; + let resolution: Option = self.compute_input(&network, &imaginate_node, get("Resolution"), Cow::Borrowed(&editor_api))?; let resolution = resolution.unwrap_or_else(|| { let (x, y) = pick_safe_imaginate_resolution((transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length())); DVec2::new(x as f64, y as f64) }); let parameters = ImaginateGenerationParameters { - seed: self.compute_input::(&network, &imaginate_node, get("Seed"), Cow::Borrowed(&image_frame))? as u64, + seed: self.compute_input::(&network, &imaginate_node, get("Seed"), Cow::Borrowed(&editor_api))? as u64, resolution: resolution.as_uvec2().into(), - samples: self.compute_input::(&network, &imaginate_node, get("Samples"), Cow::Borrowed(&image_frame))? as u32, + samples: self.compute_input::(&network, &imaginate_node, get("Samples"), Cow::Borrowed(&editor_api))? as u32, sampling_method: self - .compute_input::(&network, &imaginate_node, get("Sampling Method"), Cow::Borrowed(&image_frame))? + .compute_input::(&network, &imaginate_node, get("Sampling Method"), Cow::Borrowed(&editor_api))? .api_value() .to_string(), - text_guidance: self.compute_input(&network, &imaginate_node, get("Prompt Guidance"), Cow::Borrowed(&image_frame))?, - text_prompt: self.compute_input(&network, &imaginate_node, get("Prompt"), Cow::Borrowed(&image_frame))?, - negative_prompt: self.compute_input(&network, &imaginate_node, get("Negative Prompt"), Cow::Borrowed(&image_frame))?, - image_creativity: Some(self.compute_input::(&network, &imaginate_node, get("Image Creativity"), Cow::Borrowed(&image_frame))? / 100.), - restore_faces: self.compute_input(&network, &imaginate_node, get("Improve Faces"), Cow::Borrowed(&image_frame))?, - tiling: self.compute_input(&network, &imaginate_node, get("Tiling"), Cow::Borrowed(&image_frame))?, + text_guidance: self.compute_input(&network, &imaginate_node, get("Prompt Guidance"), Cow::Borrowed(&editor_api))?, + text_prompt: self.compute_input(&network, &imaginate_node, get("Prompt"), Cow::Borrowed(&editor_api))?, + negative_prompt: self.compute_input(&network, &imaginate_node, get("Negative Prompt"), Cow::Borrowed(&editor_api))?, + image_creativity: Some(self.compute_input::(&network, &imaginate_node, get("Image Creativity"), Cow::Borrowed(&editor_api))? / 100.), + restore_faces: self.compute_input(&network, &imaginate_node, get("Improve Faces"), Cow::Borrowed(&editor_api))?, + tiling: self.compute_input(&network, &imaginate_node, get("Tiling"), Cow::Borrowed(&editor_api))?, }; - let use_base_image = self.compute_input::(&network, &imaginate_node, get("Adapt Input Image"), Cow::Borrowed(&image_frame))?; + let use_base_image = self.compute_input::(&network, &imaginate_node, get("Adapt Input Image"), Cow::Borrowed(&editor_api))?; let input_image_frame: Option> = if use_base_image { - Some(self.compute_input::>(&network, &imaginate_node, get("Input Image"), Cow::Borrowed(&image_frame))?) + Some(self.compute_input::>(&network, &imaginate_node, get("Input Image"), Cow::Borrowed(&editor_api))?) } else { None }; @@ -176,7 +176,7 @@ impl NodeGraphExecutor { }; let mask_image = if let Some(transform) = image_transform { - let mask_path: Option> = self.compute_input(&network, &imaginate_node, get("Masking Layer"), Cow::Borrowed(&image_frame))?; + let mask_path: Option> = self.compute_input(&network, &imaginate_node, get("Masking Layer"), Cow::Borrowed(&editor_api))?; // Calculate the size of the node graph frame let size = DVec2::new(transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length()); @@ -208,13 +208,13 @@ impl NodeGraphExecutor { parameters: Box::new(parameters), base_image: base_image.map(Box::new), mask_image: mask_image.map(Box::new), - mask_paint_mode: if self.compute_input::(&network, &imaginate_node, get("Inpaint"), Cow::Borrowed(&image_frame))? { + mask_paint_mode: if self.compute_input::(&network, &imaginate_node, get("Inpaint"), Cow::Borrowed(&editor_api))? { ImaginateMaskPaintMode::Inpaint } else { ImaginateMaskPaintMode::Outpaint }, - mask_blur_px: self.compute_input::(&network, &imaginate_node, get("Mask Blur"), Cow::Borrowed(&image_frame))? as u32, - imaginate_mask_starting_fill: self.compute_input(&network, &imaginate_node, get("Mask Starting Fill"), Cow::Borrowed(&image_frame))?, + mask_blur_px: self.compute_input::(&network, &imaginate_node, get("Mask Blur"), Cow::Borrowed(&editor_api))? as u32, + imaginate_mask_starting_fill: self.compute_input(&network, &imaginate_node, get("Mask Starting Fill"), Cow::Borrowed(&editor_api))?, hostname: preferences.imaginate_server_hostname.clone(), refresh_frequency: preferences.imaginate_refresh_frequency, document_id, @@ -244,6 +244,10 @@ impl NodeGraphExecutor { // Construct the input image frame let transform = DAffine2::IDENTITY; let image_frame = ImageFrame { image, transform }; + let editor_api = EditorApi { + image_frame: Some(&image_frame), + font_cache: Some(&persistent_data.1.font_cache), + }; let node_graph_frame = match &layer.data { LayerDataType::NodeGraphFrame(frame) => Ok(frame), @@ -253,11 +257,11 @@ impl NodeGraphExecutor { // Special execution path for generating Imaginate (as generation requires IO from outside node graph) if let Some(imaginate_node) = imaginate_node { - responses.push_back(self.generate_imaginate(network, imaginate_node, (document, document_id), layer_path, image_frame, persistent_data)?); + responses.push_back(self.generate_imaginate(network, imaginate_node, (document, document_id), layer_path, editor_api, persistent_data)?); return Ok(()); } // Execute the node graph - let boxed_node_graph_output = self.execute_network(network, image_frame)?; + let boxed_node_graph_output = self.execute_network(network, editor_api)?; // Check if the output is vector data if core::any::TypeId::of::() == DynAny::type_id(boxed_node_graph_output.as_ref()) { diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 3a12e02cc4..7cedf724b8 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -7,6 +7,7 @@ type MouseCursorIcon, type XY, DisplayEditableTextbox, + DisplayEditableTextboxTransform, DisplayRemoveEditableTextbox, TriggerTextCommit, TriggerViewportResize, @@ -37,6 +38,8 @@ // Interactive text editing let textInput: undefined | HTMLDivElement = undefined; + let showTextInput: boolean; + let textInputMatrix: number[]; // CSS properties let canvasSvgWidth: number | undefined = undefined; @@ -121,32 +124,6 @@ export async function updateDocumentArtwork(svg: string) { artworkSvg = svg; rasterizedCanvas = undefined; - - await tick(); - - if (textInput) { - const foreignObject = canvasContainer?.getElementsByTagName("foreignObject")[0] as SVGForeignObjectElement | undefined; - if (!foreignObject || foreignObject.children.length > 0) return; - - const addedInput = foreignObject.appendChild(textInput); - window.dispatchEvent(new CustomEvent("modifyinputfield", { detail: addedInput })); - - await tick(); - - // Necessary to select contenteditable: https://stackoverflow.com/questions/6139107/programmatically-select-text-in-a-contenteditable-html-element/6150060#6150060 - - const range = window.document.createRange(); - range.selectNodeContents(addedInput); - - const selection = window.getSelection(); - if (selection) { - selection.removeAllRanges(); - selection.addRange(range); - } - - addedInput.focus(); - addedInput.click(); - } } export function updateDocumentOverlays(svg: string) { @@ -257,13 +234,20 @@ editor.instance.onChangeText(textCleaned); } - export function displayEditableTextbox(displayEditableTextbox: DisplayEditableTextbox) { - textInput = window.document.createElement("div") as HTMLDivElement; + export async function displayEditableTextbox(displayEditableTextbox: DisplayEditableTextbox) { + showTextInput = true; + + await tick(); + + if (!textInput) { + return; + } if (displayEditableTextbox.text === "") textInput.textContent = ""; else textInput.textContent = `${displayEditableTextbox.text}\n`; textInput.contentEditable = "true"; + textInput.style.transformOrigin = "0 0"; textInput.style.width = displayEditableTextbox.lineWidth ? `${displayEditableTextbox.lineWidth}px` : "max-content"; textInput.style.height = "auto"; textInput.style.fontSize = `${displayEditableTextbox.fontSize}px`; @@ -273,17 +257,37 @@ if (!textInput) return; editor.instance.updateBounds(textInputCleanup(textInput.innerText)); }; + textInputMatrix = displayEditableTextbox.transform; + const new_font = new FontFace("text-font", `url(${displayEditableTextbox.url})`); + window.document.fonts.add(new_font); + textInput.style.fontFamily = "text-font"; + + // Necessary to select contenteditable: https://stackoverflow.com/questions/6139107/programmatically-select-text-in-a-contenteditable-html-element/6150060#6150060 + + const range = window.document.createRange(); + range.selectNodeContents(textInput); + + const selection = window.getSelection(); + if (selection) { + selection.removeAllRanges(); + selection.addRange(range); + } + + textInput.focus(); + textInput.click(); + + window.dispatchEvent(new CustomEvent("modifyinputfield", { detail: textInput })); } export function displayRemoveEditableTextbox() { - textInput = undefined; window.dispatchEvent(new CustomEvent("modifyinputfield", { detail: undefined })); + showTextInput = false; } // Resize elements to render the new viewport size export function viewportResize() { if (!canvasContainer) return; - + // Resize the canvas canvasSvgWidth = Math.ceil(parseFloat(getComputedStyle(canvasContainer).width)); canvasSvgHeight = Math.ceil(parseFloat(getComputedStyle(canvasContainer).height)); @@ -364,6 +368,9 @@ displayEditableTextbox(data); }); + editor.subscriptions.subscribeJsMessage(DisplayEditableTextboxTransform, async (data) => { + textInputMatrix = data.transform; + }); editor.subscriptions.subscribeJsMessage(DisplayRemoveEditableTextbox, async () => { await tick(); @@ -432,6 +439,11 @@ {@html overlaysSvg} +
+ {#if showTextInput} +
+ {/if} +
@@ -575,29 +587,23 @@ } } - foreignObject { - width: 10000px; - height: 10000px; + .text-input div { + cursor: text; + background: none; + border: none; + margin: 0; + padding: 0; overflow: visible; + white-space: pre-wrap; + display: inline-block; + // Workaround to force Chrome to display the flashing text entry cursor when text is empty + padding-left: 1px; + margin-left: -1px; - div { - cursor: text; - background: none; + &:focus { border: none; - margin: 0; - padding: 0; - overflow: visible; - white-space: pre-wrap; - display: inline-block; - // Workaround to force Chrome to display the flashing text entry cursor when text is empty - padding-left: 1px; - margin-left: -1px; - - &:focus { - border: none; - outline: none; // Ok for contenteditable element - margin: -1px; - } + outline: none; // Ok for contenteditable element + margin: -1px; } } } diff --git a/frontend/src/wasm-communication/messages.ts b/frontend/src/wasm-communication/messages.ts index 7b7df48b82..e694ddec5e 100644 --- a/frontend/src/wasm-communication/messages.ts +++ b/frontend/src/wasm-communication/messages.ts @@ -704,6 +704,14 @@ export class DisplayEditableTextbox extends JsMessage { @Type(() => Color) readonly color!: Color; + + readonly url!: string; + + readonly transform!: number[]; +} + +export class DisplayEditableTextboxTransform extends JsMessage { + readonly transform!: number[]; } export class UpdateImageData extends JsMessage { @@ -1384,6 +1392,7 @@ export const messageMakers: Record = { DisplayDialogDismiss, DisplayDialogPanic, DisplayEditableTextbox, + DisplayEditableTextboxTransform, DisplayRemoveEditableTextbox, TriggerAboutGraphiteLocalizedCommitDate, TriggerImaginateCheckServerStatus, diff --git a/libraries/bezier-rs/src/subpath/manipulators.rs b/libraries/bezier-rs/src/subpath/manipulators.rs index 011fad493e..9d35da87c6 100644 --- a/libraries/bezier-rs/src/subpath/manipulators.rs +++ b/libraries/bezier-rs/src/subpath/manipulators.rs @@ -34,6 +34,16 @@ impl Subpath { self.manipulator_groups.insert(index, group) } + /// Push a manipulator group to the end. + pub fn push_manipulator_group(&mut self, group: ManipulatorGroup) { + self.manipulator_groups.push(group) + } + + /// Get a mutable reference to the last manipulator + pub fn last_manipulator_group_mut(&mut self) -> Option<&mut ManipulatorGroup> { + self.manipulator_groups.last_mut() + } + /// Remove a manipulator group at an index. pub fn remove_manipulator_group(&mut self, index: usize) -> ManipulatorGroup { self.manipulator_groups.remove(index) diff --git a/libraries/bezier-rs/src/subpath/structs.rs b/libraries/bezier-rs/src/subpath/structs.rs index 74ef188f81..656aa41772 100644 --- a/libraries/bezier-rs/src/subpath/structs.rs +++ b/libraries/bezier-rs/src/subpath/structs.rs @@ -72,6 +72,16 @@ impl ManipulatorGroup Self::new(anchor, Some(anchor), Some(anchor)) } + /// Construct a new manipulator group from an anchor, in handle, out handle and an id + pub fn new_with_id(anchor: DVec2, in_handle: Option, out_handle: Option, id: ManipulatorGroupId) -> Self { + Self { anchor, in_handle, out_handle, id } + } + + /// Construct a new manipulator point with just an anchor position and an id + pub fn new_anchor_with_id(anchor: DVec2, id: ManipulatorGroupId) -> Self { + Self::new_with_id(anchor, Some(anchor), Some(anchor), id) + } + /// Create a bezier curve that starts at the current manipulator group and finishes in the `end_group` manipulator group. pub fn to_bezier(&self, end_group: &ManipulatorGroup) -> Bezier { let start = self.anchor; diff --git a/node-graph/gcore/Cargo.toml b/node-graph/gcore/Cargo.toml index 60abd5690d..41b38ec1df 100644 --- a/node-graph/gcore/Cargo.toml +++ b/node-graph/gcore/Cargo.toml @@ -16,6 +16,7 @@ std = [ "glam/std", "specta", "num-traits/std", + "rustybuzz", ] default = ["async", "serde", "kurbo", "log", "std", "rand_chacha"] log = ["dep:log"] @@ -60,6 +61,9 @@ base64 = { version = "0.13", optional = true } specta.workspace = true specta.optional = true once_cell = { version = "1.17.0", default-features = false, optional = true } + +rustybuzz = { version = "0.6.0", optional = true } + num-derive = { version = "0.3.3" } num-traits = { version = "0.2.15", default-features = false, features = [ "i128", diff --git a/node-graph/gcore/src/lib.rs b/node-graph/gcore/src/lib.rs index f368a3fb23..e366bb7139 100644 --- a/node-graph/gcore/src/lib.rs +++ b/node-graph/gcore/src/lib.rs @@ -12,6 +12,8 @@ pub mod generic; pub mod ops; pub mod structural; #[cfg(feature = "std")] +pub mod text; +#[cfg(feature = "std")] pub mod uuid; pub mod value; @@ -114,3 +116,6 @@ impl<'i, I: 'i, O: 'i> Node<'i, I> for Pin Node<'a, I, Output = (**self).eval(input) } } + +#[cfg(feature = "alloc")] +pub use crate::raster::image::{EditorApi, ExtractImageFrame}; diff --git a/node-graph/gcore/src/raster.rs b/node-graph/gcore/src/raster.rs index 41cac43fcb..48c9a81fcd 100644 --- a/node-graph/gcore/src/raster.rs +++ b/node-graph/gcore/src/raster.rs @@ -532,283 +532,7 @@ fn dimensions_node<_P>(input: ImageSlice<'input, _P>) -> (u32, u32) { #[cfg(feature = "alloc")] pub use image::{CollectNode, Image, ImageFrame, ImageRefNode, MapImageSliceNode}; #[cfg(feature = "alloc")] -mod image { - use super::{Color, ImageSlice}; - use crate::Node; - use alloc::vec::Vec; - use core::hash::{Hash, Hasher}; - use dyn_any::StaticType; - use glam::{DAffine2, DVec2}; - - #[cfg(feature = "serde")] - mod base64_serde { - //! Basic wrapper for [`serde`] for [`base64`] encoding - - use super::super::Pixel; - use serde::{Deserialize, Deserializer, Serializer}; - - pub fn as_base64(key: &Vec

, serializer: S) -> Result - where - S: Serializer, - { - let u8_data = key.iter().flat_map(|color| color.to_bytes()).collect::>(); - serializer.serialize_str(&base64::encode(u8_data)) - } - - pub fn from_base64<'a, D, P: Pixel>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'a>, - { - use serde::de::Error; - - let color_from_chunk = |chunk: &[u8]| P::from_bytes(chunk.try_into().unwrap()).clone(); - - let colors_from_bytes = |bytes: Vec| bytes.chunks_exact(P::byte_size()).map(color_from_chunk).collect(); - - String::deserialize(deserializer) - .and_then(|string| base64::decode(string).map_err(|err| Error::custom(err.to_string()))) - .map(colors_from_bytes) - .map_err(serde::de::Error::custom) - } - } - - #[derive(Clone, Debug, PartialEq, Default, specta::Type)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - pub struct Image { - pub width: u32, - pub height: u32, - #[cfg_attr(feature = "serde", serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64"))] - pub data: Vec

, - } - - unsafe impl StaticType for Image

- where - P::Static: Pixel, - { - type Static = Image; - } - - impl Raster for Image

{ - type Pixel = P; - fn get_pixel(&self, x: u32, y: u32) -> Option

{ - self.data.get((x + y * self.width) as usize).copied() - } - fn width(&self) -> u32 { - self.width - } - fn height(&self) -> u32 { - self.height - } - } - - impl RasterMut for Image

{ - fn get_pixel_mut(&mut self, x: u32, y: u32) -> Option<&mut P> { - self.data.get_mut((x + y * self.width) as usize) - } - } - - // TODO: Evaluate if this will be a problem for our use case. - /// Warning: This is an approximation of a hash, and is not guaranteed to not collide. - impl Hash for Image

{ - fn hash(&self, state: &mut H) { - const HASH_SAMPLES: u64 = 1000; - let data_length = self.data.len() as u64; - self.width.hash(state); - self.height.hash(state); - for i in 0..HASH_SAMPLES.min(data_length) { - self.data[(i * data_length / HASH_SAMPLES) as usize].hash(state); - } - } - } - - impl Image

{ - pub const fn empty() -> Self { - Self { - width: 0, - height: 0, - data: Vec::new(), - } - } - - pub fn new(width: u32, height: u32, color: P) -> Self { - Self { - width, - height, - data: vec![color; (width * height) as usize], - } - } - - pub fn as_slice(&self) -> ImageSlice

{ - ImageSlice { - width: self.width, - height: self.height, - data: self.data.as_slice(), - } - } - } - - impl Image { - /// Generate Image from some frontend image data (the canvas pixels as u8s in a flat array) - pub fn from_image_data(image_data: &[u8], width: u32, height: u32) -> Self { - let data = image_data.chunks_exact(4).map(|v| Color::from_rgba8_srgb(v[0], v[1], v[2], v[3])).collect(); - Image { width, height, data } - } - } - - use super::*; - impl Image

- where - P::ColorChannel: Linear, - { - /// Flattens each channel cast to a u8 - pub fn into_flat_u8(self) -> (Vec, u32, u32) { - let Image { width, height, data } = self; - - let to_gamma = |x| SRGBGammaFloat::from_linear(x); - let to_u8 = |x| (num_cast::<_, f32>(x).unwrap() * 255.) as u8; - - let result_bytes = data - .into_iter() - .flat_map(|color| { - [ - to_u8(to_gamma(color.r() / color.a().to_channel())), - to_u8(to_gamma(color.g() / color.a().to_channel())), - to_u8(to_gamma(color.b() / color.a().to_channel())), - (num_cast::<_, f32>(color.a()).unwrap() * 255.) as u8, - ] - }) - .collect(); - - (result_bytes, width, height) - } - } - - impl IntoIterator for Image

{ - type Item = P; - type IntoIter = alloc::vec::IntoIter

; - fn into_iter(self) -> Self::IntoIter { - self.data.into_iter() - } - } - - #[derive(Debug, Clone, Copy, Default)] - pub struct ImageRefNode

{ - _p: PhantomData

, - } - - #[node_macro::node_fn(ImageRefNode<_P>)] - fn image_ref_node<_P: Pixel>(image: &'input Image<_P>) -> ImageSlice<'input, _P> { - image.as_slice() - } - - #[derive(Debug, Clone)] - pub struct CollectNode {} - - #[node_macro::node_fn(CollectNode)] - fn collect_node<_Iter>(input: _Iter) -> Vec<_Iter::Item> - where - _Iter: Iterator, - { - input.collect() - } - - #[derive(Debug)] - pub struct MapImageSliceNode { - data: Data, - } - - #[node_macro::node_fn(MapImageSliceNode)] - fn map_node(input: (u32, u32), data: Vec

) -> Image

{ - Image { - width: input.0, - height: input.1, - data, - } - } - - #[derive(Clone, Debug, PartialEq, Default, specta::Type)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - pub struct ImageFrame { - pub image: Image

, - pub transform: DAffine2, - } - - impl Sample for ImageFrame

{ - type Pixel = P; - - fn sample(&self, pos: DVec2) -> Option { - let image_size = DVec2::new(self.image.width() as f64, self.image.height() as f64); - let pos = (DAffine2::from_scale(image_size) * self.transform.inverse()).transform_point2(pos); - if pos.x < 0. || pos.y < 0. || pos.x >= image_size.x || pos.y >= image_size.y { - return None; - } - self.image.get_pixel(pos.x as u32, pos.y as u32) - } - } - - impl Raster for ImageFrame

{ - type Pixel = P; - - fn width(&self) -> u32 { - self.image.width() - } - - fn height(&self) -> u32 { - self.image.height() - } - - fn get_pixel(&self, x: u32, y: u32) -> Option { - self.image.get_pixel(x, y) - } - } - - impl RasterMut for ImageFrame

{ - fn get_pixel_mut(&mut self, x: u32, y: u32) -> Option<&mut Self::Pixel> { - self.image.get_pixel_mut(x, y) - } - } - - unsafe impl StaticType for ImageFrame

- where - P::Static: Pixel, - { - type Static = ImageFrame; - } - - impl ImageFrame

{ - pub const fn empty() -> Self { - Self { - image: Image::empty(), - transform: DAffine2::ZERO, - } - } - - pub fn get_mut(&mut self, x: usize, y: usize) -> &mut P { - &mut self.image.data[y * (self.image.width as usize) + x] - } - - /// Clamps the provided point to ((0, 0), (ImageSize.x, ImageSize.y)) and returns the closest pixel - pub fn sample(&self, position: DVec2) -> P { - let x = position.x.clamp(0., self.image.width as f64 - 1.) as usize; - let y = position.y.clamp(0., self.image.height as f64 - 1.) as usize; - - self.image.data[x + y * self.image.width as usize] - } - } - - impl AsRef> for ImageFrame

{ - fn as_ref(&self) -> &ImageFrame

{ - self - } - } - - impl Hash for ImageFrame

{ - fn hash(&self, state: &mut H) { - self.image.hash(state); - self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)) - } - } -} +pub(crate) mod image; #[cfg(test)] mod test { diff --git a/node-graph/gcore/src/raster/image.rs b/node-graph/gcore/src/raster/image.rs new file mode 100644 index 0000000000..052a67b936 --- /dev/null +++ b/node-graph/gcore/src/raster/image.rs @@ -0,0 +1,317 @@ +use super::{Color, ImageSlice}; +use crate::Node; +use alloc::vec::Vec; +use core::hash::{Hash, Hasher}; +use dyn_any::StaticType; +use glam::{DAffine2, DVec2}; + +#[cfg(feature = "serde")] +mod base64_serde { + //! Basic wrapper for [`serde`] to perform [`base64`] encoding + + use super::super::Pixel; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn as_base64(key: &Vec

, serializer: S) -> Result + where + S: Serializer, + { + let u8_data = key.iter().flat_map(|color| color.to_bytes()).collect::>(); + serializer.serialize_str(&base64::encode(u8_data)) + } + + pub fn from_base64<'a, D, P: Pixel>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'a>, + { + use serde::de::Error; + + let color_from_chunk = |chunk: &[u8]| P::from_bytes(chunk.try_into().unwrap()).clone(); + + let colors_from_bytes = |bytes: Vec| bytes.chunks_exact(P::byte_size()).map(color_from_chunk).collect(); + + String::deserialize(deserializer) + .and_then(|string| base64::decode(string).map_err(|err| Error::custom(err.to_string()))) + .map(colors_from_bytes) + .map_err(serde::de::Error::custom) + } +} + +#[derive(Clone, Debug, PartialEq, Default, specta::Type)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Image { + pub width: u32, + pub height: u32, + #[cfg_attr(feature = "serde", serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64"))] + pub data: Vec

, +} + +unsafe impl StaticType for Image

+where + P::Static: Pixel, +{ + type Static = Image; +} + +impl Raster for Image

{ + type Pixel = P; + fn get_pixel(&self, x: u32, y: u32) -> Option

{ + self.data.get((x + y * self.width) as usize).copied() + } + fn width(&self) -> u32 { + self.width + } + fn height(&self) -> u32 { + self.height + } +} + +impl RasterMut for Image

{ + fn get_pixel_mut(&mut self, x: u32, y: u32) -> Option<&mut P> { + self.data.get_mut((x + y * self.width) as usize) + } +} + +// TODO: Evaluate if this will be a problem for our use case. +/// Warning: This is an approximation of a hash, and is not guaranteed to not collide. +impl Hash for Image

{ + fn hash(&self, state: &mut H) { + const HASH_SAMPLES: u64 = 1000; + let data_length = self.data.len() as u64; + self.width.hash(state); + self.height.hash(state); + for i in 0..HASH_SAMPLES.min(data_length) { + self.data[(i * data_length / HASH_SAMPLES) as usize].hash(state); + } + } +} + +impl Image

{ + pub const fn empty() -> Self { + Self { + width: 0, + height: 0, + data: Vec::new(), + } + } + + pub fn new(width: u32, height: u32, color: P) -> Self { + Self { + width, + height, + data: vec![color; (width * height) as usize], + } + } + + pub fn as_slice(&self) -> ImageSlice

{ + ImageSlice { + width: self.width, + height: self.height, + data: self.data.as_slice(), + } + } +} + +impl Image { + /// Generate Image from some frontend image data (the canvas pixels as u8s in a flat array) + pub fn from_image_data(image_data: &[u8], width: u32, height: u32) -> Self { + let data = image_data.chunks_exact(4).map(|v| Color::from_rgba8_srgb(v[0], v[1], v[2], v[3])).collect(); + Image { width, height, data } + } +} + +use super::*; +impl Image

+where + P::ColorChannel: Linear, +{ + /// Flattens each channel cast to a u8 + pub fn into_flat_u8(self) -> (Vec, u32, u32) { + let Image { width, height, data } = self; + + let to_gamma = |x| SRGBGammaFloat::from_linear(x); + let to_u8 = |x| (num_cast::<_, f32>(x).unwrap() * 255.) as u8; + + let result_bytes = data + .into_iter() + .flat_map(|color| { + [ + to_u8(to_gamma(color.r() / color.a().to_channel())), + to_u8(to_gamma(color.g() / color.a().to_channel())), + to_u8(to_gamma(color.b() / color.a().to_channel())), + (num_cast::<_, f32>(color.a()).unwrap() * 255.) as u8, + ] + }) + .collect(); + + (result_bytes, width, height) + } +} + +impl IntoIterator for Image

{ + type Item = P; + type IntoIter = alloc::vec::IntoIter

; + fn into_iter(self) -> Self::IntoIter { + self.data.into_iter() + } +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct ImageRefNode

{ + _p: PhantomData

, +} + +#[node_macro::node_fn(ImageRefNode<_P>)] +fn image_ref_node<_P: Pixel>(image: &'input Image<_P>) -> ImageSlice<'input, _P> { + image.as_slice() +} + +#[derive(Debug, Clone)] +pub struct CollectNode {} + +#[node_macro::node_fn(CollectNode)] +fn collect_node<_Iter>(input: _Iter) -> Vec<_Iter::Item> +where + _Iter: Iterator, +{ + input.collect() +} + +#[derive(Debug)] +pub struct MapImageSliceNode { + data: Data, +} + +#[node_macro::node_fn(MapImageSliceNode)] +fn map_node(input: (u32, u32), data: Vec

) -> Image

{ + Image { + width: input.0, + height: input.1, + data, + } +} + +#[derive(Clone, Debug, PartialEq, Default, specta::Type)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ImageFrame { + pub image: Image

, + pub transform: DAffine2, +} + +impl Sample for ImageFrame

{ + type Pixel = P; + + fn sample(&self, pos: DVec2) -> Option { + let image_size = DVec2::new(self.image.width() as f64, self.image.height() as f64); + let pos = (DAffine2::from_scale(image_size) * self.transform.inverse()).transform_point2(pos); + if pos.x < 0. || pos.y < 0. || pos.x >= image_size.x || pos.y >= image_size.y { + return None; + } + self.image.get_pixel(pos.x as u32, pos.y as u32) + } +} + +impl Raster for ImageFrame

{ + type Pixel = P; + + fn width(&self) -> u32 { + self.image.width() + } + + fn height(&self) -> u32 { + self.image.height() + } + + fn get_pixel(&self, x: u32, y: u32) -> Option { + self.image.get_pixel(x, y) + } +} + +impl RasterMut for ImageFrame

{ + fn get_pixel_mut(&mut self, x: u32, y: u32) -> Option<&mut Self::Pixel> { + self.image.get_pixel_mut(x, y) + } +} + +unsafe impl StaticType for ImageFrame

+where + P::Static: Pixel, +{ + type Static = ImageFrame; +} + +impl ImageFrame

{ + pub const fn empty() -> Self { + Self { + image: Image::empty(), + transform: DAffine2::ZERO, + } + } + + pub fn get_mut(&mut self, x: usize, y: usize) -> &mut P { + &mut self.image.data[y * (self.image.width as usize) + x] + } + + /// Clamps the provided point to ((0, 0), (ImageSize.x, ImageSize.y)) and returns the closest pixel + pub fn sample(&self, position: DVec2) -> P { + let x = position.x.clamp(0., self.image.width as f64 - 1.) as usize; + let y = position.y.clamp(0., self.image.height as f64 - 1.) as usize; + + self.image.data[x + y * self.image.width as usize] + } +} + +impl AsRef> for ImageFrame

{ + fn as_ref(&self) -> &ImageFrame

{ + self + } +} + +impl Hash for ImageFrame

{ + fn hash(&self, state: &mut H) { + self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)); + 0.hash(state); + self.image.hash(state); + } +} + +use crate::text::FontCache; +#[derive(Clone, Debug, Hash, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct EditorApi<'a> { + #[cfg_attr(feature = "serde", serde(skip))] + pub image_frame: Option<&'a ImageFrame>, + #[cfg_attr(feature = "serde", serde(skip))] + pub font_cache: Option<&'a FontCache>, +} + +unsafe impl StaticType for EditorApi<'_> { + type Static = EditorApi<'static>; +} + +impl EditorApi<'_> { + pub fn empty() -> Self { + Self { image_frame: None, font_cache: None } + } +} + +impl<'a> AsRef> for EditorApi<'a> { + fn as_ref(&self) -> &EditorApi<'a> { + self + } +} + +pub struct ExtractImageFrame; + +impl<'a: 'input, 'input> Node<'input, EditorApi<'a>> for ExtractImageFrame { + type Output = ImageFrame; + fn eval(&'input self, editor_api: EditorApi<'a>) -> Self::Output { + editor_api.image_frame.cloned().unwrap_or(ImageFrame::empty()) + } +} + +impl ExtractImageFrame { + pub fn new() -> Self { + Self + } +} diff --git a/node-graph/gcore/src/text.rs b/node-graph/gcore/src/text.rs new file mode 100644 index 0000000000..44414712bf --- /dev/null +++ b/node-graph/gcore/src/text.rs @@ -0,0 +1,21 @@ +mod font_cache; +mod to_path; + +use crate::EditorApi; +pub use font_cache::*; +use node_macro::node_fn; +pub use to_path::*; + +use crate::Node; + +pub struct TextGenerator { + text: Text, + font_name: FontName, + font_size: Size, +} + +#[node_fn(TextGenerator)] +fn generate_text<'a: 'input>(editor: EditorApi<'a>, text: String, font_name: Font, font_size: f64) -> crate::vector::VectorData { + let buzz_face = editor.font_cache.and_then(|cache| cache.get(&font_name)).map(|data| load_face(data)); + crate::vector::VectorData::from_subpaths(to_path(&text, buzz_face, font_size, None)) +} diff --git a/document-legacy/src/layers/text_layer/font_cache.rs b/node-graph/gcore/src/text/font_cache.rs similarity index 82% rename from document-legacy/src/layers/text_layer/font_cache.rs rename to node-graph/gcore/src/text/font_cache.rs index 70a039c3e3..91fc3d5233 100644 --- a/document-legacy/src/layers/text_layer/font_cache.rs +++ b/node-graph/gcore/src/text/font_cache.rs @@ -1,8 +1,9 @@ +use dyn_any::{DynAny, StaticType}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// A font type (storing font family and font style and an optional preview URL) -#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq, specta::Type)] +#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq, DynAny, specta::Type)] pub struct Font { #[serde(rename = "fontFamily")] pub font_family: String, @@ -16,7 +17,7 @@ impl 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, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] pub struct FontCache { /// Actual font file data used for rendering a font with ttf_parser and rustybuzz font_file_data: HashMap>, @@ -64,3 +65,15 @@ impl FontCache { self.preview_urls.get(font) } } + +impl core::hash::Hash for FontCache { + fn hash(&self, state: &mut H) { + self.preview_urls.len().hash(state); + self.preview_urls.iter().for_each(|(font, url)| { + font.hash(state); + url.hash(state) + }); + self.font_file_data.len().hash(state); + self.font_file_data.keys().for_each(|font| font.hash(state)); + } +} diff --git a/document-legacy/src/layers/text_layer/to_path.rs b/node-graph/gcore/src/text/to_path.rs similarity index 72% rename from document-legacy/src/layers/text_layer/to_path.rs rename to node-graph/gcore/src/text/to_path.rs index a5f10fd164..efc43042d1 100644 --- a/document-legacy/src/layers/text_layer/to_path.rs +++ b/node-graph/gcore/src/text/to_path.rs @@ -1,18 +1,18 @@ -use graphene_std::vector::consts::ManipulatorType; -use graphene_std::vector::manipulator_group::ManipulatorGroup; -use graphene_std::vector::manipulator_point::ManipulatorPoint; -use graphene_std::vector::subpath::Subpath; +use crate::uuid::ManipulatorGroupId; +use bezier_rs::{ManipulatorGroup, Subpath}; use glam::DVec2; use rustybuzz::ttf_parser::{GlyphId, OutlineBuilder}; use rustybuzz::{GlyphBuffer, UnicodeBuffer}; struct Builder { - path: Subpath, + current_subpath: Subpath, + other_subpaths: Vec>, pos: DVec2, offset: DVec2, ascender: f64, scale: f64, + id: ManipulatorGroupId, } impl Builder { @@ -23,33 +23,31 @@ impl Builder { impl OutlineBuilder for Builder { fn move_to(&mut self, x: f32, y: f32) { - let anchor = self.point(x, y); - if self.path.manipulator_groups().last().filter(|el| el.points.iter().any(Option::is_some)).is_some() { - self.path.manipulator_groups_mut().push_end(ManipulatorGroup::closed()); + if !self.current_subpath.is_empty() { + self.other_subpaths.push(core::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false))); } - self.path.manipulator_groups_mut().push_end(ManipulatorGroup::new_with_anchor(anchor)); + self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next())); } fn line_to(&mut self, x: f32, y: f32) { - let anchor = self.point(x, y); - self.path.manipulator_groups_mut().push_end(ManipulatorGroup::new_with_anchor(anchor)); + self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next())); } fn quad_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) { let [handle, anchor] = [self.point(x1, y1), self.point(x2, y2)]; - self.path.manipulator_groups_mut().last_mut().unwrap().points[ManipulatorType::OutHandle] = Some(ManipulatorPoint::new(handle, ManipulatorType::OutHandle)); - self.path.manipulator_groups_mut().push_end(ManipulatorGroup::new_with_anchor(anchor)); + self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle); + self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(anchor, self.id.next())); } fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) { let [handle1, handle2, anchor] = [self.point(x1, y1), self.point(x2, y2), self.point(x3, y3)]; - self.path.manipulator_groups_mut().last_mut().unwrap().points[ManipulatorType::OutHandle] = Some(ManipulatorPoint::new(handle1, ManipulatorType::OutHandle)); - self.path.manipulator_groups_mut().push_end(ManipulatorGroup::new_with_anchor(anchor)); - self.path.manipulator_groups_mut().last_mut().unwrap().points[ManipulatorType::InHandle] = Some(ManipulatorPoint::new(handle2, ManipulatorType::InHandle)); + self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle1); + self.current_subpath.push_manipulator_group(ManipulatorGroup::new_with_id(anchor, Some(handle2), None, self.id.next())); } fn close(&mut self) { - self.path.manipulator_groups_mut().push_end(ManipulatorGroup::closed()); + self.current_subpath.set_closed(true); + self.other_subpaths.push(core::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false))); } } @@ -80,21 +78,23 @@ fn wrap_word(line_width: Option, glyph_buffer: &GlyphBuffer, scale: f64, x_ false } -pub fn to_path(str: &str, buzz_face: Option, font_size: f64, line_width: Option) -> Subpath { +pub fn to_path(str: &str, buzz_face: Option, font_size: f64, line_width: Option) -> Vec> { let buzz_face = match buzz_face { Some(face) => face, // Show blank layer if font has not loaded - None => return Subpath::default(), + None => return vec![], }; let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size); let mut builder = Builder { - path: Subpath::new(), + current_subpath: Subpath::new(Vec::new(), false), + other_subpaths: Vec::new(), pos: DVec2::ZERO, offset: DVec2::ZERO, ascender: (buzz_face.ascender() as f64 / buzz_face.height() as f64) * font_size / scale, scale, + id: ManipulatorGroupId::ZERO, }; for line in str.split('\n') { @@ -115,6 +115,10 @@ pub fn to_path(str: &str, buzz_face: Option, font_size: f64, li } builder.offset = DVec2::new(glyph_position.x_offset as f64, glyph_position.y_offset as f64) * builder.scale; buzz_face.outline_glyph(GlyphId(glyph_info.glyph_id as u16), &mut builder); + if !builder.current_subpath.is_empty() { + builder.other_subpaths.push(core::mem::replace(&mut builder.current_subpath, Subpath::new(Vec::new(), false))); + } + builder.pos += DVec2::new(glyph_position.x_advance as f64, glyph_position.y_advance as f64) * builder.scale; } @@ -122,7 +126,7 @@ pub fn to_path(str: &str, buzz_face: Option, font_size: f64, li } builder.pos = DVec2::new(0., builder.pos.y + line_height); } - builder.path + builder.other_subpaths } pub fn bounding_box(str: &str, buzz_face: Option, font_size: f64, line_width: Option) -> DVec2 { @@ -165,3 +169,7 @@ pub fn bounding_box(str: &str, buzz_face: Option, font_size: f6 bounds } + +pub fn load_face(data: &[u8]) -> rustybuzz::Face { + rustybuzz::Face::from_slice(data, 0).expect("Loading font failed") +} diff --git a/node-graph/gcore/src/uuid.rs b/node-graph/gcore/src/uuid.rs index 81119b2534..b8d9b041e4 100644 --- a/node-graph/gcore/src/uuid.rs +++ b/node-graph/gcore/src/uuid.rs @@ -80,3 +80,13 @@ impl bezier_rs::Identifier for ManipulatorGroupId { Self(generate_uuid()) } } + +impl ManipulatorGroupId { + pub const ZERO: ManipulatorGroupId = ManipulatorGroupId(0); + + pub fn next(&mut self) -> Self { + let old = self.0; + self.0 += 1; + Self(old) + } +} diff --git a/node-graph/gcore/src/vector/subpath.rs b/node-graph/gcore/src/vector/subpath.rs index d55f6fd5ca..2ae3a4ae51 100644 --- a/node-graph/gcore/src/vector/subpath.rs +++ b/node-graph/gcore/src/vector/subpath.rs @@ -54,8 +54,7 @@ impl Subpath { groups.push(ManipulatorGroup::new_with_handles(group.anchor, group.in_handle, group.out_handle)); } if subpath.closed() { - let group = subpath.manipulator_groups()[0]; - groups.push(ManipulatorGroup::new_with_handles(group.anchor, group.in_handle, group.out_handle)); + groups.push(ManipulatorGroup::closed()); } } Self(groups) diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 00ce2aadb0..b7157febc4 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -324,20 +324,19 @@ impl NodeNetwork { } /// Appends a new node to the network after the output node and sets it as the new output - pub fn push_node(&mut self, mut node: DocumentNode) -> NodeId { + pub fn push_node(&mut self, mut node: DocumentNode, connect_to_previous: bool) -> NodeId { let id = self.nodes.len().try_into().expect("Too many nodes in network"); // Set the correct position for the new node - if let Some(pos) = self.nodes.get(&self.original_outputs()[0].node_id).map(|n| n.metadata.position) { + if let Some(pos) = self.original_outputs().first().and_then(|first| self.nodes.get(&first.node_id)).map(|n| n.metadata.position) { node.metadata.position = pos + IVec2::new(8, 0); } - if self.outputs.is_empty() { - self.outputs.push(NodeOutput::new(id, 0)); - } - let input = NodeInput::node(self.outputs[0].node_id, self.outputs[0].node_output_index); - if node.inputs.is_empty() { - node.inputs.push(input); - } else { - node.inputs[0] = input; + if connect_to_previous && !self.outputs.is_empty() { + let input = NodeInput::node(self.outputs[0].node_id, self.outputs[0].node_output_index); + if node.inputs.is_empty() { + node.inputs.push(input); + } else { + node.inputs[0] = input; + } } self.nodes.insert(id, node); self.outputs = vec![NodeOutput::new(id, 0)]; @@ -352,7 +351,7 @@ impl NodeNetwork { implementation: DocumentNodeImplementation::Unresolved("graphene_core::ops::IdNode".into()), metadata: DocumentNodeMetadata { position: (0, 0).into() }, }; - self.push_node(node) + self.push_node(node, true) } /// Adds a Cache and a Clone node to the network @@ -389,7 +388,7 @@ impl NodeNetwork { }), metadata: DocumentNodeMetadata { position: (0, 0).into() }, }; - self.push_node(node) + self.push_node(node, true) } /// Get the nested network given by the path of node ids diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 73fd25ffd3..baac1edf44 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -52,8 +52,10 @@ pub enum TaggedValue { Quantization(graphene_core::quantization::QuantizationChannels), OptionalColor(Option), ManipulatorGroupIds(Vec), + Font(graphene_core::text::Font), VecDVec2(Vec), Segments(Vec>), + EditorApi(graphene_core::EditorApi<'static>), DocumentNode(DocumentNode), } @@ -115,6 +117,7 @@ impl Hash for TaggedValue { Self::Quantization(quantized_image) => quantized_image.hash(state), Self::OptionalColor(color) => color.hash(state), Self::ManipulatorGroupIds(mirror) => mirror.hash(state), + Self::Font(font) => font.hash(state), Self::VecDVec2(vec_dvec2) => { vec_dvec2.len().hash(state); for dvec2 in vec_dvec2 { @@ -126,9 +129,8 @@ impl Hash for TaggedValue { segment.hash(state) } } - Self::DocumentNode(document_node) => { - document_node.hash(state); - } + Self::EditorApi(editor_api) => editor_api.hash(state), + Self::DocumentNode(document_node) => document_node.hash(state), } } } @@ -173,8 +175,10 @@ impl<'a> TaggedValue { TaggedValue::Quantization(x) => Box::new(x), TaggedValue::OptionalColor(x) => Box::new(x), TaggedValue::ManipulatorGroupIds(x) => Box::new(x), + TaggedValue::Font(x) => Box::new(x), TaggedValue::VecDVec2(x) => Box::new(x), TaggedValue::Segments(x) => Box::new(x), + TaggedValue::EditorApi(x) => Box::new(x), TaggedValue::DocumentNode(x) => Box::new(x), } } @@ -231,8 +235,10 @@ impl<'a> TaggedValue { TaggedValue::Quantization(_) => concrete!(graphene_core::quantization::QuantizationChannels), TaggedValue::OptionalColor(_) => concrete!(Option), TaggedValue::ManipulatorGroupIds(_) => concrete!(Vec), + TaggedValue::Font(_) => concrete!(graphene_core::text::Font), TaggedValue::VecDVec2(_) => concrete!(Vec), TaggedValue::Segments(_) => concrete!(graphene_core::raster::IndexNode>>), + TaggedValue::EditorApi(_) => concrete!(graphene_core::EditorApi), TaggedValue::DocumentNode(_) => concrete!(crate::document::DocumentNode), } } diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index c6426898ba..5a2989a768 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -140,6 +140,7 @@ fn node_registry() -> HashMap, input: &ImageFrame, params: []), + register_node!(graphene_core::ops::CloneNode<_>, input: &graphene_core::EditorApi, params: []), register_node!(graphene_core::ops::AddParameterNode<_>, input: u32, params: [u32]), register_node!(graphene_core::ops::AddParameterNode<_>, input: &u32, params: [u32]), register_node!(graphene_core::ops::AddParameterNode<_>, input: u32, params: [&u32]), @@ -148,7 +149,7 @@ fn node_registry() -> HashMap, input: &f64, params: [f64]), register_node!(graphene_core::ops::AddParameterNode<_>, input: f64, params: [&f64]), register_node!(graphene_core::ops::AddParameterNode<_>, input: &f64, params: [&f64]), - register_node!(graphene_core::ops::SomeNode, input: ImageFrame, params: []), + register_node!(graphene_core::ops::SomeNode, input: graphene_core::EditorApi, params: []), register_node!(graphene_std::raster::DownresNode<_>, input: ImageFrame, params: []), register_node!(graphene_std::raster::MaskImageNode<_, _, _>, input: ImageFrame, params: [ImageFrame]), register_node!(graphene_std::raster::MaskImageNode<_, _, _>, input: ImageFrame, params: [ImageFrame]), @@ -317,35 +318,44 @@ fn node_registry() -> HashMap>), concrete!(&ImageFrame), vec![]), ), + ( + NodeIdentifier::new("graphene_std::memo::LetNode<_>"), + |_| { + let node: LetNode = graphene_std::memo::LetNode::new(); + let any = graphene_std::any::DynAnyRefNode::new(node); + any.into_type_erased() + }, + NodeIOTypes::new(concrete!(Option), concrete!(&graphene_core::EditorApi), vec![]), + ), ( NodeIdentifier::new("graphene_std::memo::EndLetNode<_>"), |args| { let input: DowncastBothNode<(), ImageFrame> = DowncastBothNode::new(args[0]); let node = graphene_std::memo::EndLetNode::new(input); - let any: DynAnyInRefNode, _, _> = graphene_std::any::DynAnyInRefNode::new(node); + let any: DynAnyInRefNode = graphene_std::any::DynAnyInRefNode::new(node); any.into_type_erased() }, - NodeIOTypes::new(generic!(T), concrete!(ImageFrame), vec![value_fn!(ImageFrame)]), + NodeIOTypes::new(generic!(T), concrete!(graphene_core::EditorApi), vec![value_fn!(ImageFrame)]), ), ( NodeIdentifier::new("graphene_std::memo::EndLetNode<_>"), |args| { let input: DowncastBothNode<(), VectorData> = DowncastBothNode::new(args[0]); let node = graphene_std::memo::EndLetNode::new(input); - let any: DynAnyInRefNode, _, _> = graphene_std::any::DynAnyInRefNode::new(node); + let any: DynAnyInRefNode = graphene_std::any::DynAnyInRefNode::new(node); any.into_type_erased() }, - NodeIOTypes::new(generic!(T), concrete!(ImageFrame), vec![value_fn!(VectorData)]), + NodeIOTypes::new(generic!(T), concrete!(graphene_core::EditorApi), vec![value_fn!(VectorData)]), ), ( NodeIdentifier::new("graphene_std::memo::RefNode<_, _>"), |args| { - let map_fn: DowncastBothRefNode>, ImageFrame> = DowncastBothRefNode::new(args[0]); + let map_fn: DowncastBothRefNode, graphene_core::EditorApi> = DowncastBothRefNode::new(args[0]); let node = graphene_std::memo::RefNode::new(map_fn); let any = graphene_std::any::DynAnyRefNode::new(node); any.into_type_erased() }, - NodeIOTypes::new(concrete!(()), concrete!(&ImageFrame), vec![]), + NodeIOTypes::new(concrete!(()), concrete!(&graphene_core::EditorApi), vec![]), ), ( NodeIdentifier::new("graphene_core::structural::MapImageNode"), @@ -500,7 +510,9 @@ fn node_registry() -> HashMap>, params: [Vec] ), + register_node!(graphene_core::text::TextGenerator<_, _, _>, input: graphene_core::EditorApi, params: [String, graphene_core::text::Font, f64]), register_node!(graphene_std::brush::VectorPointsNode, input: VectorData, params: []), + register_node!(graphene_core::ExtractImageFrame, input: graphene_core::EditorApi, params: []), ]; let mut map: HashMap> = HashMap::new(); for (id, c, types) in node_types.into_iter().flatten() { diff --git a/node-graph/node-macro/src/lib.rs b/node-graph/node-macro/src/lib.rs index cb6867530b..67d594beed 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -139,6 +139,8 @@ pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream { .collect::>(); where_clause.predicates.extend(extra_where_clause.clone()); + let input_lifetime = if generics.is_empty() { quote::quote!() } else { quote::quote!('input,) }; + quote::quote! { impl <'input, #generics> Node<'input, #primary_input_ty> for #node_name<#(#args),*> @@ -155,7 +157,7 @@ pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream { } } - impl <'input, #new_fn_generics> #node_name<#(#args),*> + impl <#input_lifetime #new_fn_generics> #node_name<#(#args),*> where #(#extra_where_clause),* { pub const fn new(#(#parameter_idents: #struct_generics_iter),*) -> Self{