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:
0HyperCube
2023-04-27 03:07:43 +01:00
committed by Keavon Chambers
parent 271f9d5158
commit ef93f8442a
44 changed files with 1082 additions and 1143 deletions

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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;

View File

@@ -176,10 +176,6 @@ pub enum DocumentMessage {
SetSnapping {
snap: bool,
},
SetTextboxEditability {
path: Vec<LayerId>,
editable: bool,
},
SetViewMode {
view_mode: ViewMode,
},

View File

@@ -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());
}
}
}
}
_ => {}
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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
}

View File

@@ -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();

View File

@@ -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 },

View File

@@ -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();

View File

@@ -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,

View File

@@ -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| {

View File

@@ -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};

View File

@@ -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 });
}
}
_ => {}
}
}
}
}

View File

@@ -1,4 +1,4 @@
use document_legacy::layers::text_layer::FontCache;
use graphene_std::text::FontCache;
use serde::{Deserialize, Serialize};

View File

@@ -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)),
}?;

View File

@@ -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());
}
}
_ => {}
}

View File

@@ -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 });
}
}

View File

@@ -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()) {