mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-02 20:42:16 +00:00
Migrate text layers to nodes (#1155)
* Initial work towards text to node * Add the text generate node * Implement live edit * Fix merge error * Cleanup text tool * Implement text * Fix transforms * Fix broken image frame * Double click to edit text * Fix rendering text on load * Moving whilst editing * Better text properties * Prevent changing vector when there is a Text node * Push node api * Use node fn macro * Stable ids * Image module as a seperate file * Explain check for "Input Frame" node * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
271f9d5158
commit
ef93f8442a
44 changed files with 1082 additions and 1143 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1640,6 +1640,7 @@ dependencies = [
|
|||
"num-traits",
|
||||
"once_cell",
|
||||
"rand_chacha 0.3.1",
|
||||
"rustybuzz",
|
||||
"serde",
|
||||
"specta",
|
||||
"spin 0.9.8",
|
||||
|
|
|
@ -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<Vec<DAffine2>, 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)?;
|
||||
|
|
|
@ -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<ShapeLayer> for Layer {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<TextLayer> 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>;
|
||||
|
|
|
@ -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 [`<path>`](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;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::style::ViewMode;
|
||||
use super::text_layer::FontCache;
|
||||
use graphene_std::text::FontCache;
|
||||
|
||||
use glam::DVec2;
|
||||
|
||||
|
|
|
@ -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
|
||||
/// [`<path>`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<f64>,
|
||||
pub font: Font,
|
||||
#[serde(skip)]
|
||||
pub editable: bool,
|
||||
#[serde(skip)]
|
||||
pub cached_path: Option<Subpath>,
|
||||
}
|
||||
|
||||
impl LayerData for TextLayer {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: &RenderData) -> bool {
|
||||
let transform = self.transform(transforms, render_data.view_mode);
|
||||
let inverse = transform.inverse();
|
||||
|
||||
if !inverse.is_finite() {
|
||||
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
|
||||
return false;
|
||||
}
|
||||
|
||||
let _ = writeln!(svg, r#"<g transform="matrix("#);
|
||||
inverse.to_cols_array().iter().enumerate().for_each(|(i, entry)| {
|
||||
let _ = svg.write_str(&(entry.to_string() + if i == 5 { "" } else { "," }));
|
||||
});
|
||||
let _ = svg.write_str(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#"<style>@font-face {{font-family: local-font;src: url({});}}")</style>"#, url);
|
||||
}
|
||||
|
||||
let _ = write!(
|
||||
svg,
|
||||
r#"<foreignObject transform="matrix({})"{}></foreignObject>"#,
|
||||
transform
|
||||
.to_cols_array()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, entry)| { entry.to_string() + if i == 5 { "" } else { "," } })
|
||||
.collect::<String>(),
|
||||
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 d="{}" {} />"#,
|
||||
path.to_svg(),
|
||||
self.path_style.render(render_data.view_mode, svg_defs, transform, bounds, transformed_bounds)
|
||||
);
|
||||
}
|
||||
let _ = svg.write_str("</g>");
|
||||
|
||||
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<LayerId>, intersections: &mut Vec<Vec<LayerId>>, 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<Face<'a>> {
|
||||
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<Face>) -> 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<Face>) -> 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<Face>) -> 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));
|
||||
}
|
||||
}
|
|
@ -36,16 +36,6 @@ pub enum Operation {
|
|||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
},
|
||||
AddText {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
text: String,
|
||||
size: f64,
|
||||
font_name: String,
|
||||
font_style: String,
|
||||
},
|
||||
AddNodeGraphFrame {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
|
@ -68,14 +58,6 @@ pub enum Operation {
|
|||
layer_path: Vec<LayerId>,
|
||||
pivot: (f64, f64),
|
||||
},
|
||||
SetTextEditability {
|
||||
path: Vec<LayerId>,
|
||||
editable: bool,
|
||||
},
|
||||
SetTextContent {
|
||||
path: Vec<LayerId>,
|
||||
new_text: String,
|
||||
},
|
||||
AddPolyline {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
|
@ -128,12 +110,6 @@ pub enum Operation {
|
|||
DuplicateLayer {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
ModifyFont {
|
||||
path: Vec<LayerId>,
|
||||
font_family: String,
|
||||
size: f64,
|
||||
font_style: String,
|
||||
},
|
||||
MoveSelectedManipulatorPoints {
|
||||
layer_path: Vec<LayerId>,
|
||||
delta: (f64, f64),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -176,10 +176,6 @@ pub enum DocumentMessage {
|
|||
SetSnapping {
|
||||
snap: bool,
|
||||
},
|
||||
SetTextboxEditability {
|
||||
path: Vec<LayerId>,
|
||||
editable: bool,
|
||||
},
|
||||
SetViewMode {
|
||||
view_mode: ViewMode,
|
||||
},
|
||||
|
|
|
@ -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<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
|
|||
SetSnapping { snap } => {
|
||||
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<Color>) => {
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<GraphOperationMessage, (&mut Document, &mut NodeGraphMessage
|
|||
responses.add(Operation::SetLayerFill { path: layer, fill });
|
||||
}
|
||||
}
|
||||
|
||||
GraphOperationMessage::UpdateBounds { layer, old_bounds, new_bounds } => {
|
||||
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);
|
||||
|
|
|
@ -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<DocumentNodeType> {
|
|||
outputs: vec![NodeOutput::new(0, 0), NodeOutput::new(1, 0)],
|
||||
nodes: [DocumentNode {
|
||||
name: "Identity".to_string(),
|
||||
inputs: vec![NodeInput::Network(concrete!(ImageFrame<Color>))],
|
||||
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<DocumentNodeType> {
|
|||
nodes: [
|
||||
DocumentNode {
|
||||
name: "SetNode".to_string(),
|
||||
inputs: vec![NodeInput::Network(concrete!(ImageFrame<Color>))],
|
||||
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<DocumentNodeType> {
|
|||
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<DocumentNodeType> {
|
|||
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<bezier_rs::Subpath<uuid::ManipulatorGroupId>>) -> NodeNetwork {
|
||||
|
@ -1074,27 +1089,48 @@ pub fn new_vector_network(subpaths: Vec<bezier_rs::Subpath<uuid::ManipulatorGrou
|
|||
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 pos = 0;
|
||||
let mut next_pos = || {
|
||||
let node_pos = DocumentNodeMetadata::position((pos, 4));
|
||||
pos += 8;
|
||||
node_pos
|
||||
let mut network = NodeNetwork {
|
||||
inputs: vec![0],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
NodeNetwork {
|
||||
inputs: vec![],
|
||||
outputs: vec![NodeOutput::new(4, 0)],
|
||||
nodes: [
|
||||
path_generator.to_document_node_default_inputs([Some(NodeInput::value(TaggedValue::Subpaths(subpaths), false))], next_pos()),
|
||||
transform.to_document_node_default_inputs([Some(NodeInput::node(0, 0))], next_pos()),
|
||||
fill.to_document_node_default_inputs([Some(NodeInput::node(1, 0))], next_pos()),
|
||||
stroke.to_document_node_default_inputs([Some(NodeInput::node(2, 0))], next_pos()),
|
||||
output.to_document_node_default_inputs([Some(NodeInput::node(3, 0))], next_pos()),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(id, node)| (id as NodeId, node))
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}
|
||||
network.push_node(
|
||||
path_generator.to_document_node_default_inputs([Some(NodeInput::value(TaggedValue::Subpaths(subpaths), 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
|
||||
}
|
||||
|
||||
pub fn new_text_network(text: String, font: Font, size: f64) -> 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
|
||||
}
|
||||
|
|
|
@ -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<WidgetHolder>, Option<Vec<WidgetHolder>>) {
|
||||
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<WidgetHolder> {
|
||||
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<LayoutGroup> {
|
||||
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<LayoutGroup> {
|
||||
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<Color> = context.executor.compute_input(context.network, &imaginate_node, 0, image_frame).unwrap_or_default();
|
||||
|
|
|
@ -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<Vec<LayerId>>, document: TargetDocument },
|
||||
|
|
|
@ -94,12 +94,6 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
|
|||
}
|
||||
.into(),
|
||||
),
|
||||
ModifyFont { font_family, font_style, size } => {
|
||||
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<PropertiesPanelMessage, (&PersistentData, PropertiesPane
|
|||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
self.create_document_operation(Operation::SetLayerStroke { path, stroke }, true, responses);
|
||||
}
|
||||
ModifyText { new_text } => {
|
||||
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<glam::DVec2> = new_position.into();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
|
|||
data,
|
||||
is_default,
|
||||
} => {
|
||||
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<Message>) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use document_legacy::layers::text_layer::FontCache;
|
||||
use graphene_std::text::FontCache;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
|
|
@ -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)),
|
||||
}?;
|
||||
|
|
|
@ -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<Message>) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
@ -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<LayerId>,
|
||||
overlays: Vec<Vec<LayerId>>,
|
||||
editing_text: Option<EditingText>,
|
||||
new_text: String,
|
||||
}
|
||||
|
||||
impl TextToolData {
|
||||
/// Set the editing state of the currently modifying layer
|
||||
fn set_editing(&self, editable: bool, responses: &mut VecDeque<Message>) {
|
||||
fn set_editing(&self, editable: bool, render_data: &RenderData, responses: &mut VecDeque<Message>) {
|
||||
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<Message>) {
|
||||
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<Message>) -> 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<Message>) -> 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<Message>) -> 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<Vec<LayerId>>, responses: &mut VecDeque<Message>, 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<Vec<LayerId>>, responses: &mut VecDeque<Me
|
|||
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
|
||||
insert_index: -1,
|
||||
};
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
responses.add(DocumentMessage::Overlays(operation.into()));
|
||||
}
|
||||
}
|
||||
|
||||
fn update_overlays(document: &DocumentMessageHandler, tool_data: &mut TextToolData, responses: &mut VecDeque<Message>, render_data: &RenderData) {
|
||||
let visible_text_layers = document.selected_visible_text_layers().collect::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
|
||||
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<Message>) {
|
||||
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<NodeId> {
|
||||
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<Vec<LayerId>> {
|
||||
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<Message>) -> Self {
|
||||
let ToolActionHandlerData {
|
||||
document,
|
||||
global_tool_data,
|
||||
input,
|
||||
render_data,
|
||||
..
|
||||
}: &mut ToolActionHandlerData,
|
||||
tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> 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<Message>) {
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Text }.into());
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Text });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Color>) -> Result<Box<dyn dyn_any::DynAny + 'a>, String> {
|
||||
fn execute_network<'a>(&'a mut self, network: NodeNetwork, editor_api: EditorApi<'a>) -> Result<Box<dyn dyn_any::DynAny + 'a>, 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<Color>) => 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<T: dyn_any::StaticType>(&mut self, old_network: &NodeNetwork, node_path: &[NodeId], mut input_index: usize, image_frame: Cow<ImageFrame<Color>>) -> Result<T, String> {
|
||||
pub fn compute_input<T: dyn_any::StaticType>(&mut self, old_network: &NodeNetwork, node_path: &[NodeId], mut input_index: usize, editor_api: Cow<EditorApi<'_>>) -> Result<T, String> {
|
||||
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::<T>(boxed).map(|v| *v)
|
||||
}
|
||||
|
@ -119,7 +119,7 @@ impl NodeGraphExecutor {
|
|||
imaginate_node: Vec<NodeId>,
|
||||
(document, document_id): (&mut DocumentMessageHandler, u64),
|
||||
layer_path: Vec<LayerId>,
|
||||
image_frame: ImageFrame<Color>,
|
||||
editor_api: EditorApi<'_>,
|
||||
(preferences, persistent_data): (&PreferencesMessageHandler, &PersistentData),
|
||||
) -> Result<Message, String> {
|
||||
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<glam::DVec2> = self.compute_input(&network, &imaginate_node, get("Resolution"), Cow::Borrowed(&image_frame))?;
|
||||
let resolution: Option<glam::DVec2> = 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::<f64>(&network, &imaginate_node, get("Seed"), Cow::Borrowed(&image_frame))? as u64,
|
||||
seed: self.compute_input::<f64>(&network, &imaginate_node, get("Seed"), Cow::Borrowed(&editor_api))? as u64,
|
||||
resolution: resolution.as_uvec2().into(),
|
||||
samples: self.compute_input::<f64>(&network, &imaginate_node, get("Samples"), Cow::Borrowed(&image_frame))? as u32,
|
||||
samples: self.compute_input::<f64>(&network, &imaginate_node, get("Samples"), Cow::Borrowed(&editor_api))? as u32,
|
||||
sampling_method: self
|
||||
.compute_input::<ImaginateSamplingMethod>(&network, &imaginate_node, get("Sampling Method"), Cow::Borrowed(&image_frame))?
|
||||
.compute_input::<ImaginateSamplingMethod>(&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::<f64>(&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::<f64>(&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::<bool>(&network, &imaginate_node, get("Adapt Input Image"), Cow::Borrowed(&image_frame))?;
|
||||
let use_base_image = self.compute_input::<bool>(&network, &imaginate_node, get("Adapt Input Image"), Cow::Borrowed(&editor_api))?;
|
||||
|
||||
let input_image_frame: Option<ImageFrame<Color>> = if use_base_image {
|
||||
Some(self.compute_input::<ImageFrame<Color>>(&network, &imaginate_node, get("Input Image"), Cow::Borrowed(&image_frame))?)
|
||||
Some(self.compute_input::<ImageFrame<Color>>(&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<Vec<LayerId>> = self.compute_input(&network, &imaginate_node, get("Masking Layer"), Cow::Borrowed(&image_frame))?;
|
||||
let mask_path: Option<Vec<LayerId>> = 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::<bool>(&network, &imaginate_node, get("Inpaint"), Cow::Borrowed(&image_frame))? {
|
||||
mask_paint_mode: if self.compute_input::<bool>(&network, &imaginate_node, get("Inpaint"), Cow::Borrowed(&editor_api))? {
|
||||
ImaginateMaskPaintMode::Inpaint
|
||||
} else {
|
||||
ImaginateMaskPaintMode::Outpaint
|
||||
},
|
||||
mask_blur_px: self.compute_input::<f64>(&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::<f64>(&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::<VectorData>() == DynAny::type_id(boxed_node_graph_output.as_ref()) {
|
||||
|
|
|
@ -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,11 +257,31 @@
|
|||
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
|
||||
|
@ -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 @@
|
|||
<svg class="overlays" style:width={canvasWidthCSS} style:height={canvasHeightCSS}>
|
||||
{@html overlaysSvg}
|
||||
</svg>
|
||||
<div class="text-input" style:width={canvasWidthCSS} style:height={canvasHeightCSS}>
|
||||
{#if showTextInput}
|
||||
<div bind:this={textInput} style:transform="matrix({textInputMatrix})" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</LayoutCol>
|
||||
<LayoutCol class="bar-area right-scrollbar">
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<string, MessageMaker> = {
|
|||
DisplayDialogDismiss,
|
||||
DisplayDialogPanic,
|
||||
DisplayEditableTextbox,
|
||||
DisplayEditableTextboxTransform,
|
||||
DisplayRemoveEditableTextbox,
|
||||
TriggerAboutGraphiteLocalizedCommitDate,
|
||||
TriggerImaginateCheckServerStatus,
|
||||
|
|
|
@ -34,6 +34,16 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
self.manipulator_groups.insert(index, group)
|
||||
}
|
||||
|
||||
/// Push a manipulator group to the end.
|
||||
pub fn push_manipulator_group(&mut self, group: ManipulatorGroup<ManipulatorGroupId>) {
|
||||
self.manipulator_groups.push(group)
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the last manipulator
|
||||
pub fn last_manipulator_group_mut(&mut self) -> Option<&mut ManipulatorGroup<ManipulatorGroupId>> {
|
||||
self.manipulator_groups.last_mut()
|
||||
}
|
||||
|
||||
/// Remove a manipulator group at an index.
|
||||
pub fn remove_manipulator_group(&mut self, index: usize) -> ManipulatorGroup<ManipulatorGroupId> {
|
||||
self.manipulator_groups.remove(index)
|
||||
|
|
|
@ -72,6 +72,16 @@ impl<ManipulatorGroupId: crate::Identifier> ManipulatorGroup<ManipulatorGroupId>
|
|||
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<DVec2>, out_handle: Option<DVec2>, 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<ManipulatorGroupId>) -> Bezier {
|
||||
let start = self.anchor;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<Box<dyn for<'a> Node<'a, I, Output =
|
|||
(**self).eval(input)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
pub use crate::raster::image::{EditorApi, ExtractImageFrame};
|
||||
|
|
|
@ -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<S, P: Pixel>(key: &Vec<P>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let u8_data = key.iter().flat_map(|color| color.to_bytes()).collect::<Vec<_>>();
|
||||
serializer.serialize_str(&base64::encode(u8_data))
|
||||
}
|
||||
|
||||
pub fn from_base64<'a, D, P: Pixel>(deserializer: D) -> Result<Vec<P>, 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<u8>| 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<P: Pixel> {
|
||||
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<P>,
|
||||
}
|
||||
|
||||
unsafe impl<P: StaticTypeSized + Pixel> StaticType for Image<P>
|
||||
where
|
||||
P::Static: Pixel,
|
||||
{
|
||||
type Static = Image<P::Static>;
|
||||
}
|
||||
|
||||
impl<P: Copy + Pixel> Raster for Image<P> {
|
||||
type Pixel = P;
|
||||
fn get_pixel(&self, x: u32, y: u32) -> Option<P> {
|
||||
self.data.get((x + y * self.width) as usize).copied()
|
||||
}
|
||||
fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Copy + Pixel> RasterMut for Image<P> {
|
||||
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<P: Hash + Pixel> Hash for Image<P> {
|
||||
fn hash<H: Hasher>(&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<P: Pixel> Image<P> {
|
||||
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<P> {
|
||||
ImageSlice {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
data: self.data.as_slice(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Image<Color> {
|
||||
/// 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<P: Alpha + RGB + AssociatedAlpha> Image<P>
|
||||
where
|
||||
P::ColorChannel: Linear,
|
||||
{
|
||||
/// Flattens each channel cast to a u8
|
||||
pub fn into_flat_u8(self) -> (Vec<u8>, 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<P: Pixel> IntoIterator for Image<P> {
|
||||
type Item = P;
|
||||
type IntoIter = alloc::vec::IntoIter<P>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.data.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ImageRefNode<P> {
|
||||
_p: PhantomData<P>,
|
||||
}
|
||||
|
||||
#[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: Data,
|
||||
}
|
||||
|
||||
#[node_macro::node_fn(MapImageSliceNode)]
|
||||
fn map_node<P: Pixel>(input: (u32, u32), data: Vec<P>) -> Image<P> {
|
||||
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<P: Pixel> {
|
||||
pub image: Image<P>,
|
||||
pub transform: DAffine2,
|
||||
}
|
||||
|
||||
impl<P: Debug + Copy + Pixel> Sample for ImageFrame<P> {
|
||||
type Pixel = P;
|
||||
|
||||
fn sample(&self, pos: DVec2) -> Option<Self::Pixel> {
|
||||
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<P: Copy + Pixel> Raster for ImageFrame<P> {
|
||||
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::Pixel> {
|
||||
self.image.get_pixel(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Copy + Pixel> RasterMut for ImageFrame<P> {
|
||||
fn get_pixel_mut(&mut self, x: u32, y: u32) -> Option<&mut Self::Pixel> {
|
||||
self.image.get_pixel_mut(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<P: StaticTypeSized + Pixel> StaticType for ImageFrame<P>
|
||||
where
|
||||
P::Static: Pixel,
|
||||
{
|
||||
type Static = ImageFrame<P::Static>;
|
||||
}
|
||||
|
||||
impl<P: Copy + Pixel> ImageFrame<P> {
|
||||
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<P: Pixel> AsRef<ImageFrame<P>> for ImageFrame<P> {
|
||||
fn as_ref(&self) -> &ImageFrame<P> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Hash + Pixel> Hash for ImageFrame<P> {
|
||||
fn hash<H: Hasher>(&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 {
|
||||
|
|
317
node-graph/gcore/src/raster/image.rs
Normal file
317
node-graph/gcore/src/raster/image.rs
Normal file
|
@ -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<S, P: Pixel>(key: &Vec<P>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let u8_data = key.iter().flat_map(|color| color.to_bytes()).collect::<Vec<_>>();
|
||||
serializer.serialize_str(&base64::encode(u8_data))
|
||||
}
|
||||
|
||||
pub fn from_base64<'a, D, P: Pixel>(deserializer: D) -> Result<Vec<P>, 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<u8>| 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<P: Pixel> {
|
||||
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<P>,
|
||||
}
|
||||
|
||||
unsafe impl<P: StaticTypeSized + Pixel> StaticType for Image<P>
|
||||
where
|
||||
P::Static: Pixel,
|
||||
{
|
||||
type Static = Image<P::Static>;
|
||||
}
|
||||
|
||||
impl<P: Copy + Pixel> Raster for Image<P> {
|
||||
type Pixel = P;
|
||||
fn get_pixel(&self, x: u32, y: u32) -> Option<P> {
|
||||
self.data.get((x + y * self.width) as usize).copied()
|
||||
}
|
||||
fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Copy + Pixel> RasterMut for Image<P> {
|
||||
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<P: Hash + Pixel> Hash for Image<P> {
|
||||
fn hash<H: Hasher>(&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<P: Pixel> Image<P> {
|
||||
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<P> {
|
||||
ImageSlice {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
data: self.data.as_slice(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Image<Color> {
|
||||
/// 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<P: Alpha + RGB + AssociatedAlpha> Image<P>
|
||||
where
|
||||
P::ColorChannel: Linear,
|
||||
{
|
||||
/// Flattens each channel cast to a u8
|
||||
pub fn into_flat_u8(self) -> (Vec<u8>, 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<P: Pixel> IntoIterator for Image<P> {
|
||||
type Item = P;
|
||||
type IntoIter = alloc::vec::IntoIter<P>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.data.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ImageRefNode<P> {
|
||||
_p: PhantomData<P>,
|
||||
}
|
||||
|
||||
#[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: Data,
|
||||
}
|
||||
|
||||
#[node_macro::node_fn(MapImageSliceNode)]
|
||||
fn map_node<P: Pixel>(input: (u32, u32), data: Vec<P>) -> Image<P> {
|
||||
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<P: Pixel> {
|
||||
pub image: Image<P>,
|
||||
pub transform: DAffine2,
|
||||
}
|
||||
|
||||
impl<P: Debug + Copy + Pixel> Sample for ImageFrame<P> {
|
||||
type Pixel = P;
|
||||
|
||||
fn sample(&self, pos: DVec2) -> Option<Self::Pixel> {
|
||||
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<P: Copy + Pixel> Raster for ImageFrame<P> {
|
||||
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::Pixel> {
|
||||
self.image.get_pixel(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Copy + Pixel> RasterMut for ImageFrame<P> {
|
||||
fn get_pixel_mut(&mut self, x: u32, y: u32) -> Option<&mut Self::Pixel> {
|
||||
self.image.get_pixel_mut(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<P: StaticTypeSized + Pixel> StaticType for ImageFrame<P>
|
||||
where
|
||||
P::Static: Pixel,
|
||||
{
|
||||
type Static = ImageFrame<P::Static>;
|
||||
}
|
||||
|
||||
impl<P: Copy + Pixel> ImageFrame<P> {
|
||||
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<P: Pixel> AsRef<ImageFrame<P>> for ImageFrame<P> {
|
||||
fn as_ref(&self) -> &ImageFrame<P> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Hash + Pixel> Hash for ImageFrame<P> {
|
||||
fn hash<H: Hasher>(&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<Color>>,
|
||||
#[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<EditorApi<'a>> 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<Color>;
|
||||
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
|
||||
}
|
||||
}
|
21
node-graph/gcore/src/text.rs
Normal file
21
node-graph/gcore/src/text.rs
Normal file
|
@ -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, FontName, Size> {
|
||||
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))
|
||||
}
|
|
@ -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<Font, Vec<u8>>,
|
||||
|
@ -64,3 +65,15 @@ impl FontCache {
|
|||
self.preview_urls.get(font)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::hash::Hash for FontCache {
|
||||
fn hash<H: core::hash::Hasher>(&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));
|
||||
}
|
||||
}
|
|
@ -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<ManipulatorGroupId>,
|
||||
other_subpaths: Vec<Subpath<ManipulatorGroupId>>,
|
||||
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<f64>, glyph_buffer: &GlyphBuffer, scale: f64, x_
|
|||
false
|
||||
}
|
||||
|
||||
pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> Subpath {
|
||||
pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> Vec<Subpath<ManipulatorGroupId>> {
|
||||
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<rustybuzz::Face>, 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<rustybuzz::Face>, 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<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> DVec2 {
|
||||
|
@ -165,3 +169,7 @@ pub fn bounding_box(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f6
|
|||
|
||||
bounds
|
||||
}
|
||||
|
||||
pub fn load_face(data: &[u8]) -> rustybuzz::Face {
|
||||
rustybuzz::Face::from_slice(data, 0).expect("Loading font failed")
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -52,8 +52,10 @@ pub enum TaggedValue {
|
|||
Quantization(graphene_core::quantization::QuantizationChannels),
|
||||
OptionalColor(Option<graphene_core::raster::color::Color>),
|
||||
ManipulatorGroupIds(Vec<graphene_core::uuid::ManipulatorGroupId>),
|
||||
Font(graphene_core::text::Font),
|
||||
VecDVec2(Vec<DVec2>),
|
||||
Segments(Vec<graphene_core::raster::ImageFrame<Color>>),
|
||||
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<graphene_core::Color>),
|
||||
TaggedValue::ManipulatorGroupIds(_) => concrete!(Vec<graphene_core::uuid::ManipulatorGroupId>),
|
||||
TaggedValue::Font(_) => concrete!(graphene_core::text::Font),
|
||||
TaggedValue::VecDVec2(_) => concrete!(Vec<DVec2>),
|
||||
TaggedValue::Segments(_) => concrete!(graphene_core::raster::IndexNode<Vec<graphene_core::raster::ImageFrame<Color>>>),
|
||||
TaggedValue::EditorApi(_) => concrete!(graphene_core::EditorApi),
|
||||
TaggedValue::DocumentNode(_) => concrete!(crate::document::DocumentNode),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -140,6 +140,7 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
|
|||
register_node!(graphene_core::ops::AddNode, input: (u32, u32), params: []),
|
||||
register_node!(graphene_core::ops::AddNode, input: (u32, &u32), params: []),
|
||||
register_node!(graphene_core::ops::CloneNode<_>, input: &ImageFrame<Color>, 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<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
|
|||
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::AddParameterNode<_>, input: &f64, params: [&f64]),
|
||||
register_node!(graphene_core::ops::SomeNode, input: ImageFrame<Color>, params: []),
|
||||
register_node!(graphene_core::ops::SomeNode, input: graphene_core::EditorApi, params: []),
|
||||
register_node!(graphene_std::raster::DownresNode<_>, input: ImageFrame<Color>, params: []),
|
||||
register_node!(graphene_std::raster::MaskImageNode<_, _, _>, input: ImageFrame<Color>, params: [ImageFrame<Color>]),
|
||||
register_node!(graphene_std::raster::MaskImageNode<_, _, _>, input: ImageFrame<Color>, params: [ImageFrame<Luma>]),
|
||||
|
@ -317,35 +318,44 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
|
|||
},
|
||||
NodeIOTypes::new(concrete!(Option<ImageFrame<Color>>), concrete!(&ImageFrame<Color>), vec![]),
|
||||
),
|
||||
(
|
||||
NodeIdentifier::new("graphene_std::memo::LetNode<_>"),
|
||||
|_| {
|
||||
let node: LetNode<graphene_core::EditorApi> = graphene_std::memo::LetNode::new();
|
||||
let any = graphene_std::any::DynAnyRefNode::new(node);
|
||||
any.into_type_erased()
|
||||
},
|
||||
NodeIOTypes::new(concrete!(Option<graphene_core::EditorApi>), concrete!(&graphene_core::EditorApi), vec![]),
|
||||
),
|
||||
(
|
||||
NodeIdentifier::new("graphene_std::memo::EndLetNode<_>"),
|
||||
|args| {
|
||||
let input: DowncastBothNode<(), ImageFrame<Color>> = DowncastBothNode::new(args[0]);
|
||||
let node = graphene_std::memo::EndLetNode::new(input);
|
||||
let any: DynAnyInRefNode<ImageFrame<Color>, _, _> = graphene_std::any::DynAnyInRefNode::new(node);
|
||||
let any: DynAnyInRefNode<graphene_core::EditorApi, _, _> = graphene_std::any::DynAnyInRefNode::new(node);
|
||||
any.into_type_erased()
|
||||
},
|
||||
NodeIOTypes::new(generic!(T), concrete!(ImageFrame<Color>), vec![value_fn!(ImageFrame<Color>)]),
|
||||
NodeIOTypes::new(generic!(T), concrete!(graphene_core::EditorApi), vec![value_fn!(ImageFrame<Color>)]),
|
||||
),
|
||||
(
|
||||
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<ImageFrame<Color>, _, _> = graphene_std::any::DynAnyInRefNode::new(node);
|
||||
let any: DynAnyInRefNode<graphene_core::EditorApi, _, _> = graphene_std::any::DynAnyInRefNode::new(node);
|
||||
any.into_type_erased()
|
||||
},
|
||||
NodeIOTypes::new(generic!(T), concrete!(ImageFrame<Color>), 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<Option<ImageFrame<Color>>, ImageFrame<Color>> = DowncastBothRefNode::new(args[0]);
|
||||
let map_fn: DowncastBothRefNode<Option<graphene_core::EditorApi>, 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<Color>), vec![]),
|
||||
NodeIOTypes::new(concrete!(()), concrete!(&graphene_core::EditorApi), vec![]),
|
||||
),
|
||||
(
|
||||
NodeIdentifier::new("graphene_core::structural::MapImageNode"),
|
||||
|
@ -500,7 +510,9 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
|
|||
input: Vec<graphene_core::vector::bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>>,
|
||||
params: [Vec<graphene_core::uuid::ManipulatorGroupId>]
|
||||
),
|
||||
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<NodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> = HashMap::new();
|
||||
for (id, c, types) in node_types.into_iter().flatten() {
|
||||
|
|
|
@ -139,6 +139,8 @@ pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||
.collect::<Vec<_>>();
|
||||
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{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue