Skip to content

Improve previewing node data #1446

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions document-legacy/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ impl Default for Document {
inputs: vec![NodeInput::value(TaggedValue::GraphicGroup(Default::default()), true), NodeInput::Network(concrete!(WasmEditorApi))],
implementation: graph_craft::document::DocumentNodeImplementation::Network(NodeNetwork {
inputs: vec![3, 0],
outputs: vec![NodeOutput::new(4, 0)],
outputs: vec![NodeOutput::new(3, 0)],
nodes: [
DocumentNode {
name: "EditorApi".to_string(),
Expand All @@ -82,16 +82,14 @@ impl Default for Document {
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::memo::MemoNode<_, _>")),
..Default::default()
},
DocumentNode {
name: "Conversion".to_string(),
inputs: vec![NodeInput::Network(graphene_core::Type::Fn(Box::new(concrete!(Footprint)), Box::new(generic!(T))))],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IntoNode<_, GraphicGroup>")),
..Default::default()
},
DocumentNode {
name: "RenderNode".to_string(),
inputs: vec![NodeInput::node(0, 0), NodeInput::node(3, 0), NodeInput::node(2, 0)],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_std::wasm_application_io::RenderNode<_, _>")),
inputs: vec![
NodeInput::node(0, 0),
NodeInput::Network(graphene_core::Type::Fn(Box::new(concrete!(Footprint)), Box::new(generic!(T)))),
NodeInput::node(2, 0),
],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_std::wasm_application_io::RenderNode<_, _, _>")),
..Default::default()
},
]
Expand Down
2 changes: 1 addition & 1 deletion document-legacy/src/document_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ impl core::fmt::Display for LayerNodeIdentifier {
}

impl LayerNodeIdentifier {
const ROOT: Self = LayerNodeIdentifier::new_unchecked(0);
pub const ROOT: Self = LayerNodeIdentifier::new_unchecked(0);

/// Construct a [`LayerNodeIdentifier`] without checking if it is a layer node
pub const fn new_unchecked(node_id: NodeId) -> Self {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ impl MessageHandler<NavigationMessage, (&Document, Option<[DVec2; 2]>, &InputPre
responses.add(BroadcastEvent::DocumentIsDirty);
responses.add(DocumentMessage::DirtyRenderDocumentInOutlineView);
responses.add(PortfolioMessage::UpdateDocumentWidgets);
self.create_document_transform(responses);
self.create_document_transform(ipp.viewport_bounds.center(), responses);
}
FitViewportToSelection => {
if let Some(bounds) = selection_bounds {
Expand Down Expand Up @@ -214,7 +214,7 @@ impl MessageHandler<NavigationMessage, (&Document, Option<[DVec2; 2]>, &InputPre
}
SetCanvasRotation { angle_radians } => {
self.tilt = angle_radians;
self.create_document_transform(responses);
self.create_document_transform(ipp.viewport_bounds.center(), responses);
responses.add(BroadcastEvent::DocumentIsDirty);
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
Expand All @@ -224,7 +224,7 @@ impl MessageHandler<NavigationMessage, (&Document, Option<[DVec2; 2]>, &InputPre
responses.add(BroadcastEvent::DocumentIsDirty);
responses.add(DocumentMessage::DirtyRenderDocumentInOutlineView);
responses.add(PortfolioMessage::UpdateDocumentWidgets);
self.create_document_transform(responses);
self.create_document_transform(ipp.viewport_bounds.center(), responses);
}
TransformCanvasEnd { abort_transform } => {
if abort_transform {
Expand All @@ -235,12 +235,12 @@ impl MessageHandler<NavigationMessage, (&Document, Option<[DVec2; 2]>, &InputPre
}
TransformOperation::Pan { pre_commit_pan, .. } => {
self.pan = pre_commit_pan;
self.create_document_transform(responses);
self.create_document_transform(ipp.viewport_bounds.center(), responses);
}
TransformOperation::Zoom { pre_commit_zoom, .. } => {
self.zoom = pre_commit_zoom;
responses.add(PortfolioMessage::UpdateDocumentWidgets);
self.create_document_transform(responses);
self.create_document_transform(ipp.viewport_bounds.center(), responses);
}
}
}
Expand All @@ -264,7 +264,7 @@ impl MessageHandler<NavigationMessage, (&Document, Option<[DVec2; 2]>, &InputPre
self.pan += transformed_delta;
responses.add(BroadcastEvent::CanvasTransformed);
responses.add(BroadcastEvent::DocumentIsDirty);
self.create_document_transform(responses);
self.create_document_transform(ipp.viewport_bounds.center(), responses);
}
TranslateCanvasBegin => {
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Grabbing });
Expand All @@ -281,7 +281,7 @@ impl MessageHandler<NavigationMessage, (&Document, Option<[DVec2; 2]>, &InputPre

self.pan += transformed_delta;
responses.add(BroadcastEvent::DocumentIsDirty);
self.create_document_transform(responses);
self.create_document_transform(ipp.viewport_bounds.center(), responses);
}
WheelCanvasTranslate { use_y_as_x } => {
let delta = match use_y_as_x {
Expand Down Expand Up @@ -381,21 +381,21 @@ impl NavigationMessageHandler {
}
}

pub fn calculate_offset_transform(&self, offset: DVec2) -> DAffine2 {
pub fn calculate_offset_transform(&self, viewport_center: DVec2) -> DAffine2 {
// Try to avoid fractional coordinates to reduce anti aliasing.
let scale = self.snapped_scale();
let rounded_pan = ((self.pan + offset) * scale).round() / scale - offset;
let rounded_pan = ((self.pan + viewport_center) * scale).round() / scale - viewport_center;

// TODO: replace with DAffine2::from_scale_angle_translation and fix the errors
let offset_transform = DAffine2::from_translation(offset);
let offset_transform = DAffine2::from_translation(viewport_center);
let scale_transform = DAffine2::from_scale(DVec2::splat(scale));
let angle_transform = DAffine2::from_angle(self.snapped_angle());
let translation_transform = DAffine2::from_translation(rounded_pan);
scale_transform * offset_transform * angle_transform * translation_transform
scale_transform * offset_transform * angle_transform * offset_transform.inverse() * translation_transform
}

fn create_document_transform(&self, responses: &mut VecDeque<Message>) {
let transform = self.calculate_offset_transform(DVec2::ZERO);
fn create_document_transform(&self, viewport_center: DVec2, responses: &mut VecDeque<Message>) {
let transform = self.calculate_offset_transform(viewport_center);
responses.add(DocumentMessage::UpdateDocumentTransform { transform });
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,11 @@ impl<'a> ModifyInputsContext<'a> {

// Update the document metadata structure
if let Some(new_id) = new_id {
let parent = LayerNodeIdentifier::new(output_node_id, self.network);
let parent = if self.network.nodes.get(&output_node_id).is_some_and(|node| node.name == "Layer") {
LayerNodeIdentifier::new(output_node_id, self.network)
} else {
LayerNodeIdentifier::ROOT
};
let new_child = LayerNodeIdentifier::new(new_id, self.network);
parent.push_front_child(self.document_metadata, new_child);
self.responses.add(DocumentMessage::DocumentStructureChanged);
Expand Down Expand Up @@ -562,25 +566,25 @@ impl MessageHandler<GraphOperationMessage, (&mut Document, &mut NodeGraphMessage
}
GraphOperationMessage::NewArtboard { id, artboard } => {
let mut modify_inputs = ModifyInputsContext::new(document, node_graph, responses);
if let Some(layer) = modify_inputs.create_layer(id, modify_inputs.network.outputs[0].node_id, 0) {
if let Some(layer) = modify_inputs.create_layer(id, modify_inputs.network.original_outputs()[0].node_id, 0) {
modify_inputs.insert_artboard(artboard, layer);
}
}
GraphOperationMessage::NewBitmapLayer { id, image_frame } => {
let mut modify_inputs = ModifyInputsContext::new(document, node_graph, responses);
if let Some(layer) = modify_inputs.create_layer(id, modify_inputs.network.outputs[0].node_id, 0) {
if let Some(layer) = modify_inputs.create_layer(id, modify_inputs.network.original_outputs()[0].node_id, 0) {
modify_inputs.insert_image_data(image_frame, layer);
}
}
GraphOperationMessage::NewVectorLayer { id, subpaths } => {
let mut modify_inputs = ModifyInputsContext::new(document, node_graph, responses);
if let Some(layer) = modify_inputs.create_layer(id, modify_inputs.network.outputs[0].node_id, 0) {
if let Some(layer) = modify_inputs.create_layer(id, modify_inputs.network.original_outputs()[0].node_id, 0) {
modify_inputs.insert_vector_data(subpaths, layer);
}
}
GraphOperationMessage::NewTextLayer { id, text, font, size } => {
let mut modify_inputs = ModifyInputsContext::new(document, node_graph, responses);
if let Some(layer) = modify_inputs.create_layer(id, modify_inputs.network.outputs[0].node_id, 0) {
if let Some(layer) = modify_inputs.create_layer(id, modify_inputs.network.original_outputs()[0].node_id, 0) {
modify_inputs.insert_text(text, font, size, layer);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ fn static_nodes() -> Vec<DocumentNodeType> {
DocumentNodeType {
name: "End Scope",
category: "Ignore",
identifier: NodeImplementation::proto("graphene_core::memo::EndLetNode<_>"),
identifier: NodeImplementation::proto("graphene_core::memo::EndLetNode<_, _>"),
inputs: vec![
DocumentInputType {
name: "Scope",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ fn gradient_row(row: &mut Vec<WidgetHolder>, positions: &Vec<(f64, Option<Color>
move |_: &IconButton| {
let mut new_positions = positions.clone();

// Blend linearly between the two colours.
// Blend linearly between the two colors.
let get_color = |index: usize| match (new_positions[index].1, new_positions.get(index + 1).and_then(|x| x.1)) {
(Some(a), Some(b)) => Color::from_rgbaf32((a.r() + b.r()) / 2., (a.g() + b.g()) / 2., (a.b() + b.b()) / 2., ((a.a() + b.a()) / 2.).clamp(0., 1.)),
(Some(v), _) | (_, Some(v)) => Some(v),
Expand Down
97 changes: 34 additions & 63 deletions editor/src/node_graph_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ use document_legacy::layers::layer_info::{LayerDataType, LayerDataTypeDiscrimina
use document_legacy::{LayerId, Operation};

use graph_craft::document::value::TaggedValue;
use graph_craft::document::{generate_uuid, DocumentNodeImplementation, NodeId, NodeNetwork};
use graph_craft::document::{generate_uuid, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, NodeOutput};
use graph_craft::graphene_compiler::Compiler;
use graph_craft::imaginate_input::ImaginatePreferences;
use graph_craft::{concrete, Type};
use graphene_core::application_io::{ApplicationIo, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
use graphene_core::raster::{Image, ImageFrame};
use graphene_core::renderer::{ClickTarget, SvgSegment, SvgSegmentList};
use graphene_core::renderer::{ClickTarget, GraphicElementRendered, SvgSegment, SvgSegmentList};
use graphene_core::text::FontCache;
use graphene_core::transform::{Footprint, Transform};
use graphene_core::vector::style::ViewMode;
Expand Down Expand Up @@ -82,6 +82,7 @@ pub(crate) struct GenerationResponse {
new_click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
new_transforms: HashMap<LayerNodeIdentifier, DAffine2>,
new_upstream_transforms: HashMap<NodeId, DAffine2>,
transform: DAffine2,
}

enum NodeGraphUpdate {
Expand Down Expand Up @@ -160,14 +161,15 @@ impl NodeRuntime {
new_click_targets: self.click_targets.clone().into_iter().map(|(id, targets)| (LayerNodeIdentifier::new_unchecked(id), targets)).collect(),
new_transforms: self.transforms.clone().into_iter().map(|(id, transform)| (LayerNodeIdentifier::new_unchecked(id), transform)).collect(),
new_upstream_transforms: self.upstream_transforms.clone(),
transform,
};
self.sender.send_generation_response(response);
}
}
}
}

async fn execute_network<'a>(&'a mut self, path: &[LayerId], graph: NodeNetwork, transform: DAffine2, viewport_resolution: UVec2) -> (Result<TaggedValue, String>, MonitorNodes) {
async fn execute_network<'a>(&'a mut self, path: &[LayerId], mut graph: NodeNetwork, transform: DAffine2, viewport_resolution: UVec2) -> (Result<TaggedValue, String>, MonitorNodes) {
if self.wasm_io.is_none() {
self.wasm_io = Some(WasmApplicationIo::new().await);
}
Expand Down Expand Up @@ -512,14 +514,15 @@ impl NodeGraphExecutor {
new_click_targets,
new_transforms,
new_upstream_transforms,
transform,
}) => {
self.thumbnails = new_thumbnails;
document.metadata.update_transforms(new_transforms, new_upstream_transforms);
document.metadata.update_click_targets(new_click_targets);
let node_graph_output = result.map_err(|e| format!("Node graph evaluation failed: {e:?}"))?;
let execution_context = self.futures.remove(&generation_id).ok_or_else(|| "Invalid generation ID".to_string())?;
responses.extend(updates);
self.process_node_graph_output(node_graph_output, execution_context.layer_path.clone(), responses, execution_context.document_id)?;
self.process_node_graph_output(node_graph_output, execution_context.layer_path.clone(), transform, responses, execution_context.document_id)?;
responses.add(DocumentMessage::LayerChanged {
affected_layer_path: execution_context.layer_path,
});
Expand All @@ -537,56 +540,40 @@ impl NodeGraphExecutor {
Ok(())
}

fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, layer_path: Vec<LayerId>, responses: &mut VecDeque<Message>, document_id: u64) -> Result<(), String> {
fn render(render_object: impl GraphicElementRendered, transform: DAffine2, responses: &mut VecDeque<Message>) {
use graphene_core::renderer::{ImageRenderMode, RenderParams, SvgRender};

// Setup rendering
let mut render = SvgRender::new();
let render_params = RenderParams::new(ViewMode::Normal, ImageRenderMode::BlobUrl, None, false);

// Render SVG
render_object.render_svg(&mut render, &render_params);

// Concatenate the defs and the SVG into one string
render.wrap_with_transform(transform);
let svg = render.svg.to_string();

// Send to frontend
responses.add(FrontendMessage::UpdateDocumentNodeRender { svg });
}

fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, layer_path: Vec<LayerId>, transform: DAffine2, responses: &mut VecDeque<Message>, document_id: u64) -> Result<(), String> {
self.last_output_type.insert(layer_path.clone(), Some(node_graph_output.ty()));
match node_graph_output {
TaggedValue::VectorData(vector_data) => {
// Update the cached vector data on the layer
let transform = vector_data.transform.to_cols_array();
responses.add(Operation::SetLayerTransform { path: layer_path.clone(), transform });
responses.add(Operation::SetVectorData { path: layer_path, vector_data });
}
TaggedValue::SurfaceFrame(SurfaceFrame { surface_id, transform }) => {
let transform = transform.to_cols_array();
responses.add(Operation::SetLayerTransform { path: layer_path.clone(), transform });
responses.add(Operation::SetSurface { path: layer_path, surface_id });
}
TaggedValue::ImageFrame(ImageFrame { image, transform }) => {
// Don't update the frame's transform if the new transform is DAffine2::ZERO.
let transform = (!transform.abs_diff_eq(DAffine2::ZERO, f64::EPSILON)).then_some(transform.to_cols_array());

// If no image was generated, clear the frame
if image.width == 0 || image.height == 0 {
responses.add(DocumentMessage::FrameClear);

// Update the transform based on the graph output
if let Some(transform) = transform {
responses.add(Operation::SetLayerTransform { path: layer_path, transform });
}
} else {
// Update the image data
let image_data = vec![Self::to_frontend_image_data(image, transform, &layer_path, None, None)?];
responses.add(FrontendMessage::UpdateImageData { document_id, image_data });
}
}
TaggedValue::Artboard(artboard) => {
warn!("Rendered graph produced artboard (which is not currently rendered): {artboard:#?}");
return Err("Artboard (see console)".to_string());
}
TaggedValue::RenderOutput(graphene_std::wasm_application_io::RenderOutput::Svg(svg)) => {
// Send to frontend
//log::debug!("svg: {svg}");
responses.add(FrontendMessage::UpdateDocumentNodeRender { svg });
responses.add(DocumentMessage::RenderScrollbars);
//responses.add(FrontendMessage::UpdateDocumentNodeRender { svg });

//return Err("Graphic group (see console)".to_string());
}
TaggedValue::RenderOutput(graphene_std::wasm_application_io::RenderOutput::CanvasFrame(frame)) => {
// Send to frontend
//log::debug!("svg: {svg}");
responses.add(DocumentMessage::RenderScrollbars);
//responses.add(FrontendMessage::UpdateDocumentNodeRender { svg });
let matrix = frame
.transform
.to_cols_array()
Expand All @@ -600,30 +587,14 @@ impl NodeGraphExecutor {
1920, 1080, matrix, frame.surface_id.0
);
responses.add(FrontendMessage::UpdateDocumentNodeRender { svg });

//return Err("Graphic group (see console)".to_string());
}
TaggedValue::GraphicGroup(graphic_group) => {
use graphene_core::renderer::{GraphicElementRendered, RenderParams, SvgRender};

// Setup rendering
let mut render = SvgRender::new();
let render_params = RenderParams::new(ViewMode::Normal, graphene_core::renderer::ImageRenderMode::BlobUrl, None, false);

// Render svg
graphic_group.render_svg(&mut render, &render_params);

// Conctenate the defs and the svg into one string
let mut svg = "<defs>".to_string();
svg.push_str(&render.svg_defs);
svg.push_str("</defs>");
use std::fmt::Write;
write!(svg, "{}", render.svg).unwrap();

// Send to frontend
responses.add(FrontendMessage::UpdateDocumentNodeRender { svg });
responses.add(DocumentMessage::RenderScrollbars);
}
TaggedValue::Bool(render_object) => Self::render(render_object, transform, responses),
TaggedValue::String(render_object) => Self::render(render_object, transform, responses),
TaggedValue::F32(render_object) => Self::render(render_object, transform, responses),
TaggedValue::F64(render_object) => Self::render(render_object, transform, responses),
TaggedValue::OptionalColor(render_object) => Self::render(render_object, transform, responses),
TaggedValue::VectorData(render_object) => Self::render(render_object, transform, responses),
TaggedValue::ImageFrame(render_object) => Self::render(render_object, transform, responses),
_ => {
return Err(format!("Invalid node graph output type: {node_graph_output:#?}"));
}
Expand All @@ -643,6 +614,6 @@ impl NodeGraphExecutor {
return;
}
}
warn!("Recieved blob url for invalid segment")
warn!("Received blob url for invalid segment")
}
}
2 changes: 1 addition & 1 deletion node-graph/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Purpose of Nodes

Graphite is an image editor which is centred around a node based editing workflow, which allows operations to be visually connected in a graph. This is flexible as it allows all operations to be viewed or modified at any time without losing original data. The node system has been designed to be as general as possible with all data types being representable and a broad selection of nodes for a variety of use cases being planned.
Graphite is an image editor which is centered around a node based editing workflow, which allows operations to be visually connected in a graph. This is flexible as it allows all operations to be viewed or modified at any time without losing original data. The node system has been designed to be as general as possible with all data types being representable and a broad selection of nodes for a variety of use cases being planned.

## The Document Graph

Expand Down
12 changes: 12 additions & 0 deletions node-graph/gcore/src/application_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,18 @@ impl<'a, T> AsRef<EditorApi<'a, T>> for EditorApi<'a, T> {
}
}

// Required for the EndLetNode
impl<'a, IO> From<EditorApi<'a, IO>> for Footprint {
fn from(value: EditorApi<'a, IO>) -> Self {
value.render_config.viewport
}
}

// Required for the EndLetNode
impl<'a, IO> From<EditorApi<'a, IO>> for () {
fn from(_value: EditorApi<'a, IO>) -> Self {}
}

#[derive(Debug, Clone, Copy, Default)]
pub struct ExtractImageFrame;

Expand Down
Loading