mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-31 02:07:21 +00:00
Update graph UI and improve simplicity and robustness of sending graph and layer panel updates (#1564)
* WIP * Fix loading the structure of layers * Fix broken indents * Remove debugging stuff * Fix displaying errors and node graph UI fixes/improvements * Fix compilation failure --------- Co-authored-by: 0hypercube <0hypercube@gmail.com>
This commit is contained in:
parent
83116aa744
commit
aab0fcf84c
33 changed files with 836 additions and 813 deletions
File diff suppressed because one or more lines are too long
|
@ -32,16 +32,15 @@ pub struct DispatcherMessageHandlers {
|
|||
/// The last occurrence of the message in the message queue is sufficient to ensure correct behavior.
|
||||
/// In addition, these messages do not change any state in the backend (aside from caches).
|
||||
const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderDocument)),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::NodeGraph(NodeGraphMessageDiscriminant::SendGraph))),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::PropertiesPanel(
|
||||
PropertiesPanelMessageDiscriminant::Refresh,
|
||||
))),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::DocumentStructureChanged)),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Overlays(OverlaysMessageDiscriminant::Draw))),
|
||||
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(BroadcastEventDiscriminant::DocumentIsDirty)),
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure),
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
|
||||
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(BroadcastEventDiscriminant::DocumentIsDirty)),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Overlays(OverlaysMessageDiscriminant::Draw))),
|
||||
];
|
||||
|
||||
impl Dispatcher {
|
||||
|
@ -119,7 +118,7 @@ impl Dispatcher {
|
|||
}
|
||||
Frontend(message) => {
|
||||
// Handle these messages immediately by returning early
|
||||
if let FrontendMessage::UpdateImageData { .. } | FrontendMessage::TriggerFontLoad { .. } | FrontendMessage::TriggerRefreshBoundsOfViewports = message {
|
||||
if let FrontendMessage::TriggerFontLoad { .. } | FrontendMessage::TriggerRefreshBoundsOfViewports = message {
|
||||
self.responses.push(message);
|
||||
self.cleanup_queues(false);
|
||||
|
||||
|
@ -307,8 +306,8 @@ mod test {
|
|||
});
|
||||
let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone();
|
||||
|
||||
let layers_before_copy = document_before_copy.metadata.all_layers().collect::<Vec<_>>();
|
||||
let layers_after_copy = document_after_copy.metadata.all_layers().collect::<Vec<_>>();
|
||||
let layers_before_copy = document_before_copy.document_metadata.all_layers().collect::<Vec<_>>();
|
||||
let layers_after_copy = document_after_copy.document_metadata.all_layers().collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(layers_before_copy.len(), 3);
|
||||
assert_eq!(layers_after_copy.len(), 4);
|
||||
|
@ -330,7 +329,7 @@ mod test {
|
|||
let mut editor = create_editor_with_three_layers();
|
||||
|
||||
let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone();
|
||||
let shape_id = document_before_copy.metadata.all_layers().nth(1).unwrap();
|
||||
let shape_id = document_before_copy.document_metadata.all_layers().nth(1).unwrap();
|
||||
|
||||
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![shape_id.to_node()] });
|
||||
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal });
|
||||
|
@ -342,8 +341,8 @@ mod test {
|
|||
|
||||
let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone();
|
||||
|
||||
let layers_before_copy = document_before_copy.metadata.all_layers().collect::<Vec<_>>();
|
||||
let layers_after_copy = document_after_copy.metadata.all_layers().collect::<Vec<_>>();
|
||||
let layers_before_copy = document_before_copy.document_metadata.all_layers().collect::<Vec<_>>();
|
||||
let layers_after_copy = document_after_copy.document_metadata.all_layers().collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(layers_before_copy.len(), 3);
|
||||
assert_eq!(layers_after_copy.len(), 4);
|
||||
|
@ -385,8 +384,8 @@ mod test {
|
|||
|
||||
let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone();
|
||||
|
||||
let layers_before_copy = document_before_copy.metadata.all_layers().collect::<Vec<_>>();
|
||||
let layers_after_copy = document_after_copy.metadata.all_layers().collect::<Vec<_>>();
|
||||
let layers_before_copy = document_before_copy.document_metadata.all_layers().collect::<Vec<_>>();
|
||||
let layers_after_copy = document_after_copy.document_metadata.all_layers().collect::<Vec<_>>();
|
||||
let [original_folder, original_freehand, original_line, original_ellipse, original_polygon, original_rect] = layers_before_copy[..] else {
|
||||
panic!("Layers before incorrect");
|
||||
};
|
||||
|
@ -414,7 +413,7 @@ mod test {
|
|||
let mut editor = create_editor_with_three_layers();
|
||||
|
||||
let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone();
|
||||
let mut layers = document_before_copy.metadata.all_layers();
|
||||
let mut layers = document_before_copy.document_metadata.all_layers();
|
||||
let rect_id = layers.next().expect("rectangle");
|
||||
let shape_id = layers.next().expect("shape");
|
||||
let ellipse_id = layers.next().expect("ellipse");
|
||||
|
@ -438,8 +437,8 @@ mod test {
|
|||
|
||||
let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone();
|
||||
|
||||
let layers_before_copy = document_before_copy.metadata.all_layers().collect::<Vec<_>>();
|
||||
let layers_after_copy = document_after_copy.metadata.all_layers().collect::<Vec<_>>();
|
||||
let layers_before_copy = document_before_copy.document_metadata.all_layers().collect::<Vec<_>>();
|
||||
let layers_after_copy = document_after_copy.document_metadata.all_layers().collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(layers_before_copy.len(), 3);
|
||||
assert_eq!(layers_after_copy.len(), 6);
|
||||
|
|
|
@ -75,7 +75,7 @@ impl MessageHandler<DialogMessage, DialogData<'_>> for DialogMessageHandler {
|
|||
if let Some(document) = portfolio.active_document() {
|
||||
let mut index = 0;
|
||||
let artboards = document
|
||||
.metadata
|
||||
.document_metadata
|
||||
.all_layers()
|
||||
.filter(|&layer| is_layer_fed_by_node_of_name(layer, &document.network, "Artboard"))
|
||||
.map(|layer| {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use super::utility_types::{FrontendDocumentDetails, FrontendImageData, MouseCursorIcon};
|
||||
use super::utility_types::{FrontendDocumentDetails, MouseCursorIcon};
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::node_graph::{FrontendNode, FrontendNodeLink, FrontendNodeType};
|
||||
use crate::messages::portfolio::document::utility_types::layer_panel::{JsRawBuffer, LayerPanelEntry, RawBuffer};
|
||||
|
@ -172,12 +172,6 @@ pub enum FrontendMessage {
|
|||
#[serde(rename = "setColorChoice")]
|
||||
set_color_choice: Option<String>,
|
||||
},
|
||||
UpdateImageData {
|
||||
#[serde(rename = "documentId")]
|
||||
document_id: DocumentId,
|
||||
#[serde(rename = "imageData")]
|
||||
image_data: Vec<FrontendImageData>,
|
||||
},
|
||||
UpdateInputHints {
|
||||
#[serde(rename = "hintData")]
|
||||
hint_data: HintData,
|
||||
|
|
|
@ -13,13 +13,6 @@ pub struct FrontendDocumentDetails {
|
|||
pub id: DocumentId,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)]
|
||||
pub struct FrontendImageData {
|
||||
pub mime: String,
|
||||
#[serde(skip)]
|
||||
pub image_data: std::sync::Arc<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize, specta::Type)]
|
||||
pub enum MouseCursorIcon {
|
||||
#[default]
|
||||
|
|
|
@ -87,7 +87,6 @@ pub enum DocumentMessage {
|
|||
RenameDocument {
|
||||
new_name: String,
|
||||
},
|
||||
RenderDocument,
|
||||
RenderRulers,
|
||||
RenderScrollbars,
|
||||
SaveDocument,
|
||||
|
|
|
@ -85,7 +85,7 @@ pub struct DocumentMessageHandler {
|
|||
#[serde(skip)]
|
||||
layer_range_selection_reference: Option<LayerNodeIdentifier>,
|
||||
#[serde(skip)]
|
||||
pub metadata: DocumentMetadata,
|
||||
pub document_metadata: DocumentMetadata,
|
||||
}
|
||||
|
||||
impl Default for DocumentMessageHandler {
|
||||
|
@ -121,7 +121,7 @@ impl Default for DocumentMessageHandler {
|
|||
graph_view_overlay_open: false,
|
||||
snapping_state: SnappingState::default(),
|
||||
layer_range_selection_reference: None,
|
||||
metadata: Default::default(),
|
||||
document_metadata: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -245,7 +245,13 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
self.navigation_handler.process_message(
|
||||
message,
|
||||
responses,
|
||||
(&self.metadata, document_bounds, ipp, self.selected_visible_layers_bounding_box_viewport(), &mut self.navigation),
|
||||
(
|
||||
&self.document_metadata,
|
||||
document_bounds,
|
||||
ipp,
|
||||
self.selected_visible_layers_bounding_box_viewport(),
|
||||
&mut self.navigation,
|
||||
),
|
||||
);
|
||||
}
|
||||
#[remain::unsorted]
|
||||
|
@ -259,7 +265,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
executor,
|
||||
document_name: self.name.as_str(),
|
||||
document_network: &self.network,
|
||||
document_metadata: &mut self.metadata,
|
||||
document_metadata: &mut self.document_metadata,
|
||||
};
|
||||
self.properties_panel_message_handler
|
||||
.process_message(message, responses, (persistent_data, properties_panel_message_handler_data));
|
||||
|
@ -271,7 +277,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
responses,
|
||||
NodeGraphHandlerData {
|
||||
document_network: &mut self.network,
|
||||
document_metadata: &mut self.metadata,
|
||||
document_metadata: &mut self.document_metadata,
|
||||
document_id,
|
||||
document_name: self.name.as_str(),
|
||||
collapsed: &mut self.collapsed,
|
||||
|
@ -281,13 +287,13 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
);
|
||||
}
|
||||
#[remain::unsorted]
|
||||
GraphOperation(message) => GraphOperationMessageHandler.process_message(message, responses, (&mut self.network, &mut self.metadata, &mut self.collapsed, &mut self.node_graph_handler)),
|
||||
GraphOperation(message) => GraphOperationMessageHandler.process_message(message, responses, (&mut self.network, &mut self.document_metadata, &mut self.collapsed, &mut self.node_graph_handler)),
|
||||
|
||||
// Messages
|
||||
AbortTransaction => {
|
||||
if !self.undo_in_progress {
|
||||
self.undo(responses);
|
||||
responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
}
|
||||
AlignSelectedLayers { axis, aggregate } => {
|
||||
|
@ -375,12 +381,9 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
DocumentStructureChanged => {
|
||||
self.update_layers_panel_options_bar_widgets(responses);
|
||||
|
||||
self.document_metadata.load_structure(&self.network);
|
||||
let data_buffer: RawBuffer = self.serialize_root();
|
||||
responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer });
|
||||
|
||||
if self.graph_view_overlay_open {
|
||||
responses.add(NodeGraphMessage::SendGraph { should_rerender: false });
|
||||
}
|
||||
}
|
||||
DuplicateSelectedLayers => {
|
||||
// TODO: Reimplement selected layer duplication
|
||||
|
@ -414,7 +417,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
self.graph_view_overlay_open = open;
|
||||
|
||||
if open {
|
||||
responses.add(NodeGraphMessage::SendGraph { should_rerender: false });
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
}
|
||||
responses.add(FrontendMessage::TriggerGraphViewOverlay { open });
|
||||
}
|
||||
|
@ -503,7 +506,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
});
|
||||
}
|
||||
// Nudge resize
|
||||
else if let Some([existing_top_left, existing_bottom_right]) = self.metadata.bounding_box_document(layer) {
|
||||
else if let Some([existing_top_left, existing_bottom_right]) = self.document_metadata.bounding_box_document(layer) {
|
||||
let size = existing_bottom_right - existing_top_left;
|
||||
let new_size = size + if opposite_corner { -delta } else { delta };
|
||||
let enlargement_factor = new_size / size;
|
||||
|
@ -572,19 +575,15 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
}
|
||||
Redo => {
|
||||
responses.add(SelectToolMessage::Abort);
|
||||
responses.add(DocumentHistoryForward);
|
||||
responses.add(DocumentMessage::DocumentHistoryForward);
|
||||
responses.add(BroadcastEvent::DocumentIsDirty);
|
||||
responses.add(RenderDocument);
|
||||
responses.add(DocumentStructureChanged);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
RenameDocument { new_name } => {
|
||||
self.name = new_name;
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
|
||||
}
|
||||
RenderDocument => {
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
RenderRulers => {
|
||||
let document_transform_scale = self.navigation_handler.snapped_scale(self.navigation.zoom);
|
||||
|
||||
|
@ -753,11 +752,10 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
Undo => {
|
||||
self.undo_in_progress = true;
|
||||
responses.add(BroadcastEvent::ToolAbort);
|
||||
responses.add(DocumentHistoryBackward);
|
||||
responses.add(DocumentMessage::DocumentHistoryBackward);
|
||||
responses.add(BroadcastEvent::DocumentIsDirty);
|
||||
responses.add(RenderDocument);
|
||||
responses.add(DocumentStructureChanged);
|
||||
responses.add(UndoFinished);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
responses.add(DocumentMessage::UndoFinished);
|
||||
}
|
||||
UndoFinished => self.undo_in_progress = false,
|
||||
UngroupSelectedLayers => {
|
||||
|
@ -786,7 +784,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
responses.add(DocumentMessage::CommitTransaction);
|
||||
}
|
||||
UpdateDocumentTransform { transform } => {
|
||||
self.metadata.document_to_viewport = transform;
|
||||
self.document_metadata.document_to_viewport = transform;
|
||||
responses.add(DocumentMessage::RenderRulers);
|
||||
responses.add(DocumentMessage::RenderScrollbars);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
|
@ -813,35 +811,43 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
|
||||
impl DocumentMessageHandler {
|
||||
pub fn layer_visible(&self, layer: LayerNodeIdentifier) -> bool {
|
||||
!layer.ancestors(&self.metadata).any(|layer| self.network.disabled.contains(&layer.to_node()))
|
||||
!layer.ancestors(&self.document_metadata).any(|layer| self.network.disabled.contains(&layer.to_node()))
|
||||
}
|
||||
|
||||
pub fn selected_visible_layers(&self) -> impl Iterator<Item = LayerNodeIdentifier> + '_ {
|
||||
self.metadata.selected_layers().filter(|&layer| self.layer_visible(layer))
|
||||
self.document_metadata.selected_layers().filter(|&layer| self.layer_visible(layer))
|
||||
}
|
||||
|
||||
/// Runs an intersection test with all layers and a viewport space quad
|
||||
pub fn intersect_quad<'a>(&'a self, viewport_quad: graphene_core::renderer::Quad, network: &'a NodeNetwork) -> impl Iterator<Item = LayerNodeIdentifier> + 'a {
|
||||
let document_quad = self.metadata.document_to_viewport.inverse() * viewport_quad;
|
||||
self.metadata
|
||||
let document_quad = self.document_metadata.document_to_viewport.inverse() * viewport_quad;
|
||||
self.document_metadata
|
||||
.root()
|
||||
.decendants(&self.metadata)
|
||||
.decendants(&self.document_metadata)
|
||||
.filter(|&layer| self.layer_visible(layer))
|
||||
.filter(|&layer| !is_artboard(layer, network))
|
||||
.filter_map(|layer| self.metadata.click_target(layer).map(|targets| (layer, targets)))
|
||||
.filter(move |(layer, target)| target.iter().any(move |target| target.intersect_rectangle(document_quad, self.metadata.transform_to_document(*layer))))
|
||||
.filter_map(|layer| self.document_metadata.click_target(layer).map(|targets| (layer, targets)))
|
||||
.filter(move |(layer, target)| {
|
||||
target
|
||||
.iter()
|
||||
.any(move |target| target.intersect_rectangle(document_quad, self.document_metadata.transform_to_document(*layer)))
|
||||
})
|
||||
.map(|(layer, _)| layer)
|
||||
}
|
||||
|
||||
/// Find all of the layers that were clicked on from a viewport space location
|
||||
pub fn click_xray(&self, viewport_location: DVec2) -> impl Iterator<Item = LayerNodeIdentifier> + '_ {
|
||||
let point = self.metadata.document_to_viewport.inverse().transform_point2(viewport_location);
|
||||
self.metadata
|
||||
let point = self.document_metadata.document_to_viewport.inverse().transform_point2(viewport_location);
|
||||
self.document_metadata
|
||||
.root()
|
||||
.decendants(&self.metadata)
|
||||
.decendants(&self.document_metadata)
|
||||
.filter(|&layer| self.layer_visible(layer))
|
||||
.filter_map(|layer| self.metadata.click_target(layer).map(|targets| (layer, targets)))
|
||||
.filter(move |(layer, target)| target.iter().any(|target: &ClickTarget| target.intersect_point(point, self.metadata.transform_to_document(*layer))))
|
||||
.filter_map(|layer| self.document_metadata.click_target(layer).map(|targets| (layer, targets)))
|
||||
.filter(move |(layer, target)| {
|
||||
target
|
||||
.iter()
|
||||
.any(|target: &ClickTarget| target.intersect_point(point, self.document_metadata.transform_to_document(*layer)))
|
||||
})
|
||||
.map(|(layer, _)| layer)
|
||||
}
|
||||
|
||||
|
@ -853,7 +859,7 @@ impl DocumentMessageHandler {
|
|||
/// Get the combined bounding box of the click targets of the selected visible layers in viewport space
|
||||
pub fn selected_visible_layers_bounding_box_viewport(&self) -> Option<[DVec2; 2]> {
|
||||
self.selected_visible_layers()
|
||||
.filter_map(|layer| self.metadata.bounding_box_viewport(layer))
|
||||
.filter_map(|layer| self.document_metadata.bounding_box_viewport(layer))
|
||||
.reduce(graphene_core::renderer::Quad::combine_bounds)
|
||||
}
|
||||
|
||||
|
@ -862,7 +868,7 @@ impl DocumentMessageHandler {
|
|||
}
|
||||
|
||||
pub fn metadata(&self) -> &DocumentMetadata {
|
||||
&self.metadata
|
||||
&self.document_metadata
|
||||
}
|
||||
|
||||
pub fn serialize_document(&self) -> String {
|
||||
|
@ -878,7 +884,7 @@ impl DocumentMessageHandler {
|
|||
pub fn with_name(name: String, ipp: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) -> Self {
|
||||
let mut document = Self { name, ..Self::default() };
|
||||
let transform = document.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.size() / 2., DVec2::ZERO, 0., 1.);
|
||||
document.metadata.document_to_viewport = transform;
|
||||
document.document_metadata.document_to_viewport = transform;
|
||||
responses.add(DocumentMessage::UpdateDocumentTransform { transform });
|
||||
|
||||
document
|
||||
|
@ -897,23 +903,24 @@ impl DocumentMessageHandler {
|
|||
std::iter::empty()
|
||||
}
|
||||
|
||||
fn serialize_structure(&self, folder: LayerNodeIdentifier, structure: &mut Vec<LayerNodeIdentifier>, data: &mut Vec<LayerNodeIdentifier>, path: &mut Vec<LayerNodeIdentifier>) {
|
||||
/// Called recursively by the entry function [`serialize_root`].
|
||||
fn serialize_structure(&self, folder: LayerNodeIdentifier, structure_section: &mut Vec<u64>, data_section: &mut Vec<u64>, path: &mut Vec<LayerNodeIdentifier>) {
|
||||
let mut space = 0;
|
||||
for layer_node in folder.children(self.metadata()) {
|
||||
data.push(layer_node);
|
||||
data_section.push(layer_node.to_node().0);
|
||||
space += 1;
|
||||
if layer_node.has_children(self.metadata()) && !self.collapsed.contains(&layer_node) {
|
||||
path.push(layer_node);
|
||||
|
||||
// TODO: Skip if folder is not expanded.
|
||||
structure.push(LayerNodeIdentifier::new_unchecked(NodeId(space)));
|
||||
self.serialize_structure(layer_node, structure, data, path);
|
||||
structure_section.push(space);
|
||||
self.serialize_structure(layer_node, structure_section, data_section, path);
|
||||
space = 0;
|
||||
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
structure.push(LayerNodeIdentifier::new_unchecked(NodeId(space | 1 << 63)));
|
||||
structure_section.push(space | 1 << 63);
|
||||
}
|
||||
|
||||
/// Serializes the layer structure into a condensed 1D structure.
|
||||
|
@ -921,17 +928,18 @@ impl DocumentMessageHandler {
|
|||
/// # Format
|
||||
/// It is a string of numbers broken into three sections:
|
||||
///
|
||||
/// | Data | Description | Length |
|
||||
/// |--------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------|------------------|
|
||||
/// | `4,` `2, 1, -2, -0,` `16533113728871998040,3427872634365736244,18115028555707261608,15878401910454357952,449479075714955186` | Encoded example data | |
|
||||
/// | `L` = `4` = `structure.len()` | `L`, the length of the **Structure** section | First value |
|
||||
/// | **Structure** section = `2, 1, -2, -0` | The **Structure** section | Next `L` values |
|
||||
/// | **Data** section = `16533113728871998040, 3427872634365736244, 18115028555707261608, 15878401910454357952, 449479075714955186` | The **Data** section (layer IDs) | Remaining values |
|
||||
/// | Data | Description | Length |
|
||||
/// |------------------------------------------------------------------------------------------------------------------------------ |---------------------------------------------------------------|------------------|
|
||||
/// | `4,` `2, 1, -2, -0,` `16533113728871998040,3427872634365736244,18115028555707261608,15878401910454357952,449479075714955186` | Encoded example data | |
|
||||
/// | _____________________________________________________________________________________________________________________________ | _____________________________________________________________ | ________________ |
|
||||
/// | **Length** section: `4` | Length of the **Structure** section (`L` = `structure.len()`) | First value |
|
||||
/// | **Structure** section: `2, 1, -2, -0` | The **Structure** section | Next `L` values |
|
||||
/// | **Data** section: `16533113728871998040, 3427872634365736244, 18115028555707261608, 15878401910454357952, 449479075714955186` | The **Data** section (layer IDs) | Remaining values |
|
||||
///
|
||||
/// The data section lists the layer IDs for all folders/layers in the tree as read from top to bottom.
|
||||
/// The structure section lists signed numbers. The sign indicates a folder indentation change (`+` is down a level, `-` is up a level).
|
||||
/// The numbers in the structure block encode the indentation. For example:
|
||||
/// - `2` means read two element from the data section, then place a `[`.
|
||||
/// - `2` means read two elements from the data section, then place a `[`.
|
||||
/// - `-x` means read `x` elements from the data section and then insert a `]`.
|
||||
///
|
||||
/// ```text
|
||||
|
@ -949,14 +957,16 @@ impl DocumentMessageHandler {
|
|||
/// [3427872634365736244,18115028555707261608,449479075714955186]
|
||||
/// ```
|
||||
pub fn serialize_root(&self) -> RawBuffer {
|
||||
let mut structure = vec![LayerNodeIdentifier::ROOT];
|
||||
let mut data = Vec::new();
|
||||
self.serialize_structure(self.metadata().root(), &mut structure, &mut data, &mut vec![]);
|
||||
let mut structure_section = vec![LayerNodeIdentifier::ROOT.to_node().0];
|
||||
let mut data_section = Vec::new();
|
||||
self.serialize_structure(self.metadata().root(), &mut structure_section, &mut data_section, &mut vec![]);
|
||||
|
||||
structure[0] = LayerNodeIdentifier::new_unchecked(NodeId(structure.len() as u64 - 1));
|
||||
structure.extend(data);
|
||||
// Remove the ROOT element. Prepend `L`, the length (excluding the ROOT) of the structure section (which happens to be where the ROOT element was).
|
||||
structure_section[0] = structure_section.len() as u64 - 1;
|
||||
// Append the data section to the end.
|
||||
structure_section.extend(data_section);
|
||||
|
||||
structure.iter().map(|id| id.to_node().0).collect::<Vec<_>>().as_slice().into()
|
||||
structure_section.as_slice().into()
|
||||
}
|
||||
|
||||
/// Places a document into the history system
|
||||
|
@ -1000,9 +1010,6 @@ impl DocumentMessageHandler {
|
|||
if self.document_redo_history.len() > crate::consts::MAX_UNDO_HISTORY_LEN {
|
||||
self.document_redo_history.pop_front();
|
||||
}
|
||||
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
responses.add(NodeGraphMessage::SendGraph { should_rerender: true });
|
||||
}
|
||||
|
||||
pub fn redo(&mut self, responses: &mut VecDeque<Message>) {
|
||||
|
@ -1018,9 +1025,6 @@ impl DocumentMessageHandler {
|
|||
if self.document_undo_history.len() > crate::consts::MAX_UNDO_HISTORY_LEN {
|
||||
self.document_undo_history.pop_front();
|
||||
}
|
||||
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
responses.add(NodeGraphMessage::SendGraph { should_rerender: true });
|
||||
}
|
||||
|
||||
pub fn current_hash(&self) -> Option<u64> {
|
||||
|
@ -1410,7 +1414,6 @@ impl DocumentMessageHandler {
|
|||
Redo,
|
||||
SelectAllLayers,
|
||||
DeselectAllLayers,
|
||||
RenderDocument,
|
||||
SaveDocument,
|
||||
SetSnapping,
|
||||
DebugPrintDocument,
|
||||
|
|
|
@ -83,8 +83,8 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
|
||||
pub fn insert_node_before(&mut self, new_id: NodeId, node_id: NodeId, input_index: usize, mut document_node: DocumentNode, offset: IVec2) -> Option<NodeId> {
|
||||
assert!(!self.document_network.nodes.contains_key(&new_id), "Creating already existing node");
|
||||
let post_node = self.document_network.nodes.get_mut(&node_id)?;
|
||||
|
||||
let post_node = self.document_network.nodes.get_mut(&node_id)?;
|
||||
post_node.inputs[input_index] = NodeInput::node(new_id, 0);
|
||||
document_node.metadata.position = post_node.metadata.position + offset;
|
||||
self.document_network.nodes.insert(new_id, document_node);
|
||||
|
@ -153,7 +153,6 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
};
|
||||
let new_child = LayerNodeIdentifier::new(new_id, self.document_network);
|
||||
parent.push_front_child(self.document_metadata, new_child);
|
||||
self.responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
}
|
||||
|
||||
new_id
|
||||
|
@ -181,7 +180,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
],
|
||||
Default::default(),
|
||||
);
|
||||
self.responses.add(NodeGraphMessage::SendGraph { should_rerender: true });
|
||||
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
self.insert_node_before(NodeId(generate_uuid()), layer, 0, artboard_node, IVec2::new(-8, 0))
|
||||
}
|
||||
|
||||
|
@ -202,7 +201,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
self.insert_node_before(transform_id, fill_id, 0, transform, IVec2::new(-8, 0));
|
||||
let shape_id = NodeId(generate_uuid());
|
||||
self.insert_node_before(shape_id, transform_id, 0, shape, IVec2::new(-8, 0));
|
||||
self.responses.add(NodeGraphMessage::SendGraph { should_rerender: true });
|
||||
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
fn insert_text(&mut self, text: String, font: Font, size: f32, layer: NodeId) {
|
||||
|
@ -227,7 +226,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
self.insert_node_before(transform_id, fill_id, 0, transform, IVec2::new(-8, 0));
|
||||
let text_id = NodeId(generate_uuid());
|
||||
self.insert_node_before(text_id, transform_id, 0, text, IVec2::new(-8, 0));
|
||||
self.responses.add(NodeGraphMessage::SendGraph { should_rerender: true });
|
||||
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
fn insert_image_data(&mut self, image_frame: ImageFrame<Color>, layer: NodeId) {
|
||||
|
@ -243,7 +242,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
let image_id = NodeId(generate_uuid());
|
||||
self.insert_node_before(image_id, transform_id, 0, image, IVec2::new(-8, 0));
|
||||
|
||||
self.responses.add(NodeGraphMessage::SendGraph { should_rerender: true });
|
||||
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
fn shift_upstream(&mut self, node_id: NodeId, shift: IVec2) {
|
||||
|
@ -317,11 +316,6 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
|
||||
if !skip_rerender {
|
||||
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
} else {
|
||||
// Code was removed from here which cleared the frame
|
||||
}
|
||||
if existing_node_id.is_none() {
|
||||
self.responses.add(NodeGraphMessage::SendGraph { should_rerender: false });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -567,8 +561,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
self.document_metadata.retain_selected_nodes(|id| !delete_nodes.contains(id));
|
||||
self.responses.add(BroadcastEvent::SelectionChanged);
|
||||
|
||||
self.responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
self.responses.add(NodeGraphMessage::SendGraph { should_rerender: true });
|
||||
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -702,7 +695,7 @@ impl MessageHandler<GraphOperationMessage, (&mut NodeNetwork, &mut DocumentMetad
|
|||
}
|
||||
}
|
||||
|
||||
modify_inputs.responses.add(NodeGraphMessage::SendGraph { should_rerender: true });
|
||||
modify_inputs.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
load_network_structure(document_network, document_metadata, collapsed);
|
||||
|
|
|
@ -35,7 +35,7 @@ pub enum NodeGraphMessage {
|
|||
node_id: NodeId,
|
||||
input_index: usize,
|
||||
},
|
||||
DoubleClickNode {
|
||||
EnterNestedNetwork {
|
||||
node: NodeId,
|
||||
},
|
||||
DuplicateSelectedNodes,
|
||||
|
@ -68,9 +68,7 @@ pub enum NodeGraphMessage {
|
|||
SelectedNodesSet {
|
||||
nodes: Vec<NodeId>,
|
||||
},
|
||||
SendGraph {
|
||||
should_rerender: bool,
|
||||
},
|
||||
SendGraph,
|
||||
SetInputValue {
|
||||
node_id: NodeId,
|
||||
input_index: usize,
|
||||
|
@ -86,6 +84,7 @@ pub enum NodeGraphMessage {
|
|||
input_index: usize,
|
||||
value: TaggedValue,
|
||||
},
|
||||
/// Move all the downstream nodes to the right in the graph to allow space for a newly inserted node
|
||||
ShiftNode {
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
|
|
@ -4,7 +4,9 @@ use crate::application::generate_uuid;
|
|||
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||
use crate::messages::portfolio::document::utility_types::layer_panel::{LayerClassification, LayerPanelEntry};
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNode, NodeId, NodeInput, NodeNetwork, NodeOutput, Source};
|
||||
use graph_craft::proto::GraphErrors;
|
||||
|
@ -68,6 +70,7 @@ pub struct FrontendGraphInput {
|
|||
name: String,
|
||||
#[serde(rename = "resolvedType")]
|
||||
resolved_type: Option<String>,
|
||||
connected: Option<NodeId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
|
@ -77,13 +80,14 @@ pub struct FrontendGraphOutput {
|
|||
name: String,
|
||||
#[serde(rename = "resolvedType")]
|
||||
resolved_type: Option<String>,
|
||||
connected: Option<NodeId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct FrontendNode {
|
||||
pub id: graph_craft::document::NodeId,
|
||||
#[serde(rename = "isLayer")]
|
||||
pub is_layer: bool,
|
||||
pub id: graph_craft::document::NodeId,
|
||||
pub alias: String,
|
||||
pub name: String,
|
||||
#[serde(rename = "primaryInput")]
|
||||
|
@ -274,14 +278,9 @@ impl NodeGraphMessageHandler {
|
|||
}
|
||||
}
|
||||
|
||||
fn send_graph(&self, network: &NodeNetwork, graph_view_overlay_open: bool, responses: &mut VecDeque<Message>) {
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
fn send_graph(&self, network: &NodeNetwork, graph_view_overlay_open: bool, document_metadata: &mut DocumentMetadata, collapsed: &Vec<LayerNodeIdentifier>, responses: &mut VecDeque<Message>) {
|
||||
document_metadata.load_structure(&network);
|
||||
|
||||
if !graph_view_overlay_open {
|
||||
return;
|
||||
}
|
||||
|
||||
// List of links in format (link_start, link_end, link_end_input_index)
|
||||
let links = network
|
||||
.nodes
|
||||
.iter()
|
||||
|
@ -306,52 +305,111 @@ impl NodeGraphMessageHandler {
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let connected_node_to_output_lookup = links.iter().map(|link| ((link.link_start, link.link_start_output_index), link.link_end)).collect::<HashMap<_, _>>();
|
||||
|
||||
let mut nodes = Vec::new();
|
||||
for (id, node) in &network.nodes {
|
||||
let node_path = vec![*id];
|
||||
for (&node_id, node) in &network.nodes {
|
||||
let node_path = vec![node_id];
|
||||
// TODO: This should be based on the graph runtime type inference system in order to change the colors of node connectors to match the data type in use
|
||||
let Some(node_type) = document_node_types::resolve_document_node_type(&node.name) else {
|
||||
let Some(document_node_definition) = document_node_types::resolve_document_node_type(&node.name) else {
|
||||
warn!("Node '{}' does not exist in library", node.name);
|
||||
continue;
|
||||
};
|
||||
|
||||
// Inputs
|
||||
let mut inputs = node.inputs.iter().zip(node_type.inputs.iter().enumerate().map(|(index, input_type)| {
|
||||
let index = node.inputs.iter().take(index).filter(|input| input.is_exposed()).count();
|
||||
FrontendGraphInput {
|
||||
data_type: input_type.data_type,
|
||||
name: input_type.name.to_string(),
|
||||
resolved_type: self.resolved_types.inputs.get(&Source { node: node_path.clone(), index }).map(|input| format!("{input:?}")),
|
||||
}
|
||||
}));
|
||||
let mut inputs = {
|
||||
let frontend_graph_inputs = document_node_definition.inputs.iter().enumerate().map(|(index, input_type)| {
|
||||
// Convert the index in all inputs to the index in only the exposed inputs
|
||||
let index = node.inputs.iter().take(index).filter(|input| input.is_exposed()).count();
|
||||
|
||||
FrontendGraphInput {
|
||||
data_type: input_type.data_type,
|
||||
name: input_type.name.to_string(),
|
||||
resolved_type: self.resolved_types.inputs.get(&Source { node: node_path.clone(), index }).map(|input| format!("{input:?}")),
|
||||
connected: None,
|
||||
}
|
||||
});
|
||||
|
||||
node.inputs.iter().zip(frontend_graph_inputs).map(|(node_input, mut frontend_graph_input)| {
|
||||
if let NodeInput::Node { node_id: connected_node_id, .. } = node_input {
|
||||
frontend_graph_input.connected = Some(*connected_node_id);
|
||||
}
|
||||
(node_input, frontend_graph_input)
|
||||
})
|
||||
};
|
||||
let primary_input = inputs.next().filter(|(input, _)| input.is_exposed()).map(|(_, input_type)| input_type);
|
||||
let exposed_inputs = inputs.filter(|(input, _)| input.is_exposed()).map(|(_, input_type)| input_type).collect();
|
||||
|
||||
// Outputs
|
||||
let mut outputs = node_type.outputs.iter().enumerate().map(|(index, output_type)| FrontendGraphOutput {
|
||||
let mut outputs = document_node_definition.outputs.iter().enumerate().map(|(index, output_type)| FrontendGraphOutput {
|
||||
data_type: output_type.data_type,
|
||||
name: output_type.name.to_string(),
|
||||
resolved_type: self.resolved_types.outputs.get(&Source { node: node_path.clone(), index }).map(|output| format!("{output:?}")),
|
||||
connected: connected_node_to_output_lookup.get(&(node_id, index)).copied(),
|
||||
});
|
||||
let primary_output = if node.has_primary_output { outputs.next() } else { None };
|
||||
let primary_output = node.has_primary_output.then(|| outputs.next()).flatten();
|
||||
let exposed_outputs = outputs.collect::<Vec<_>>();
|
||||
|
||||
// Errors
|
||||
let errors = self.node_graph_errors.iter().find(|error| error.node_path.starts_with(&node_path)).map(|error| error.error.clone());
|
||||
|
||||
nodes.push(FrontendNode {
|
||||
id: node_id,
|
||||
is_layer: node.is_layer(),
|
||||
id: *id,
|
||||
alias: node.alias.clone(),
|
||||
name: node.name.clone(),
|
||||
primary_input,
|
||||
exposed_inputs,
|
||||
primary_output,
|
||||
exposed_outputs: outputs.collect::<Vec<_>>(),
|
||||
exposed_outputs,
|
||||
position: node.metadata.position.into(),
|
||||
previewed: network.outputs_contain(*id),
|
||||
disabled: network.disabled.contains(id),
|
||||
previewed: network.outputs_contain(node_id),
|
||||
disabled: network.disabled.contains(&node_id),
|
||||
errors: errors.map(|e| format!("{e:?}")),
|
||||
})
|
||||
});
|
||||
|
||||
if node.is_layer() {
|
||||
let layer = LayerNodeIdentifier::new(node_id, network);
|
||||
let layer_classification = {
|
||||
if document_metadata.is_artboard(layer) {
|
||||
LayerClassification::Artboard
|
||||
} else if document_metadata.is_folder(layer) {
|
||||
LayerClassification::Folder
|
||||
} else {
|
||||
LayerClassification::Layer
|
||||
}
|
||||
// TODO: Maybe switch to this below if perhaps it's simpler?
|
||||
// if node.is_artboard() {
|
||||
// LayerClassification::Artboard
|
||||
// } else if node.is_folder(network) {
|
||||
// LayerClassification::Folder
|
||||
// } else {
|
||||
// LayerClassification::Layer
|
||||
// }
|
||||
};
|
||||
|
||||
let data = LayerPanelEntry {
|
||||
id: node_id,
|
||||
layer_classification,
|
||||
expanded: layer.has_children(document_metadata) && !collapsed.contains(&layer),
|
||||
depth: layer.ancestors(document_metadata).count() - 1,
|
||||
parent_id: layer.parent(document_metadata).map(|parent| parent.to_node()),
|
||||
// TODO: Remove and take this from the graph data in the frontend similar to thumbnail?
|
||||
name: network.nodes.get(&node_id).map(|node| node.alias.clone()).unwrap_or_default(),
|
||||
// TODO: Remove and take this from the graph data in the frontend similar to thumbnail?
|
||||
tooltip: if cfg!(debug_assertions) { format!("Layer ID: {node_id}") } else { "".into() },
|
||||
// TODO: Remove and take this from the graph data in the frontend similar to thumbnail?
|
||||
disabled: network.disabled.contains(&node_id),
|
||||
};
|
||||
responses.add(FrontendMessage::UpdateDocumentLayerDetails { data });
|
||||
}
|
||||
}
|
||||
responses.add(FrontendMessage::UpdateNodeGraph { nodes, links });
|
||||
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
if graph_view_overlay_open {
|
||||
responses.add(FrontendMessage::UpdateNodeGraph { nodes, links });
|
||||
}
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
}
|
||||
|
||||
/// Updates the frontend's selection state in line with the backend
|
||||
|
@ -474,7 +532,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
fn process_message(&mut self, message: NodeGraphMessage, responses: &mut VecDeque<Message>, data: NodeGraphHandlerData<'a>) {
|
||||
let NodeGraphHandlerData {
|
||||
document_network,
|
||||
document_metadata: metadata,
|
||||
document_metadata,
|
||||
document_id,
|
||||
collapsed,
|
||||
graph_view_overlay_open,
|
||||
|
@ -487,15 +545,14 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
on: BroadcastEvent::SelectionChanged,
|
||||
send: Box::new(NodeGraphMessage::SelectedNodesUpdated.into()),
|
||||
});
|
||||
load_network_structure(document_network, metadata, collapsed);
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
load_network_structure(document_network, document_metadata, collapsed);
|
||||
}
|
||||
NodeGraphMessage::SelectedNodesUpdated => {
|
||||
self.update_selection_action_buttons(document_network, metadata, responses);
|
||||
self.update_selected(document_network, metadata, responses);
|
||||
if metadata.selected_layers().count() <= 1 {
|
||||
self.update_selection_action_buttons(document_network, document_metadata, responses);
|
||||
self.update_selected(document_network, document_metadata, responses);
|
||||
if document_metadata.selected_layers().count() <= 1 {
|
||||
responses.add(DocumentMessage::SetRangeSelectionLayer {
|
||||
new_layer: metadata.selected_layers().next(),
|
||||
new_layer: document_metadata.selected_layers().next(),
|
||||
});
|
||||
}
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
|
@ -520,15 +577,15 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
error!("Failed to find actual index of connector index {input_node_connector_index} on node {input_node:#?}");
|
||||
return;
|
||||
};
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
let input = NodeInput::node(output_node, output_node_connector_index);
|
||||
responses.add(NodeGraphMessage::SetNodeInput { node_id, input_index, input });
|
||||
|
||||
let should_rerender = network.connected_to_output(node_id);
|
||||
responses.add(NodeGraphMessage::SendGraph { should_rerender });
|
||||
if network.connected_to_output(node_id) {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
NodeGraphMessage::Copy => {
|
||||
let Some(network) = document_network.nested_network(&self.network) else {
|
||||
|
@ -537,7 +594,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
};
|
||||
|
||||
// Collect the selected nodes
|
||||
let new_ids = &metadata.selected_nodes().copied().enumerate().map(|(new, old)| (old, NodeId(new as u64))).collect();
|
||||
let new_ids = &document_metadata.selected_nodes().copied().enumerate().map(|(new, old)| (old, NodeId(new as u64))).collect();
|
||||
let copied_nodes: Vec<_> = Self::copy_nodes(network, new_ids).collect();
|
||||
|
||||
// Prefix to show that this is nodes
|
||||
|
@ -564,28 +621,24 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
graph_craft::document::DocumentNodeMetadata::position((x, y)),
|
||||
);
|
||||
responses.add(NodeGraphMessage::InsertNode { node_id, document_node });
|
||||
|
||||
responses.add(NodeGraphMessage::SendGraph { should_rerender: false });
|
||||
}
|
||||
NodeGraphMessage::Cut => {
|
||||
responses.add(NodeGraphMessage::Copy);
|
||||
responses.add(NodeGraphMessage::DeleteSelectedNodes { reconnect: true });
|
||||
}
|
||||
NodeGraphMessage::DeleteNode { node_id, reconnect } => {
|
||||
self.remove_node(document_network, metadata, node_id, responses, reconnect);
|
||||
self.remove_node(document_network, document_metadata, node_id, responses, reconnect);
|
||||
}
|
||||
NodeGraphMessage::DeleteSelectedNodes { reconnect } => {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
for node_id in metadata.selected_nodes().copied() {
|
||||
for node_id in document_metadata.selected_nodes().copied() {
|
||||
responses.add(NodeGraphMessage::DeleteNode { node_id, reconnect });
|
||||
}
|
||||
|
||||
responses.add(NodeGraphMessage::SendGraph { should_rerender: false });
|
||||
|
||||
if let Some(network) = document_network.nested_network(&self.network) {
|
||||
// Only generate node graph if one of the selected nodes is connected to the output
|
||||
if metadata.selected_nodes().any(|&node_id| network.connected_to_output(node_id)) {
|
||||
if document_metadata.selected_nodes().any(|&node_id| network.connected_to_output(node_id)) {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
|
@ -615,34 +668,35 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
}
|
||||
responses.add(NodeGraphMessage::SetNodeInput { node_id, input_index, input });
|
||||
|
||||
let should_rerender = network.connected_to_output(node_id);
|
||||
responses.add(NodeGraphMessage::SendGraph { should_rerender });
|
||||
if network.connected_to_output(node_id) {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
NodeGraphMessage::DoubleClickNode { node } => {
|
||||
NodeGraphMessage::EnterNestedNetwork { node } => {
|
||||
if let Some(network) = document_network.nested_network(&self.network) {
|
||||
if network.nodes.get(&node).and_then(|node| node.implementation.get_network()).is_some() {
|
||||
self.network.push(node);
|
||||
}
|
||||
}
|
||||
if let Some(network) = document_network.nested_network(&self.network) {
|
||||
self.send_graph(network, graph_view_overlay_open, responses);
|
||||
self.send_graph(network, graph_view_overlay_open, document_metadata, collapsed, responses);
|
||||
}
|
||||
self.update_selected(document_network, metadata, responses);
|
||||
self.update_selected(document_network, document_metadata, responses);
|
||||
}
|
||||
NodeGraphMessage::DuplicateSelectedNodes => {
|
||||
if let Some(network) = document_network.nested_network(&self.network) {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
let new_ids = &metadata.selected_nodes().map(|&id| (id, NodeId(generate_uuid()))).collect();
|
||||
let new_ids = &document_metadata.selected_nodes().map(|&id| (id, NodeId(generate_uuid()))).collect();
|
||||
|
||||
metadata.clear_selected_nodes();
|
||||
document_metadata.clear_selected_nodes();
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
|
||||
// Copy the selected nodes
|
||||
let copied_nodes = Self::copy_nodes(network, new_ids).collect::<Vec<_>>();
|
||||
|
||||
// Select the new nodes
|
||||
metadata.add_selected_nodes(copied_nodes.iter().map(|(node_id, _)| *node_id));
|
||||
document_metadata.add_selected_nodes(copied_nodes.iter().map(|(node_id, _)| *node_id));
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
|
||||
for (node_id, mut document_node) in copied_nodes {
|
||||
|
@ -653,22 +707,20 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
responses.add(NodeGraphMessage::InsertNode { node_id, document_node });
|
||||
}
|
||||
|
||||
self.send_graph(network, graph_view_overlay_open, responses);
|
||||
self.update_selected(document_network, metadata, responses);
|
||||
responses.add(NodeGraphMessage::SendGraph { should_rerender: false });
|
||||
self.update_selected(document_network, document_metadata, responses);
|
||||
}
|
||||
}
|
||||
NodeGraphMessage::ExitNestedNetwork { depth_of_nesting } => {
|
||||
metadata.clear_selected_nodes();
|
||||
document_metadata.clear_selected_nodes();
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
|
||||
for _ in 0..depth_of_nesting {
|
||||
self.network.pop();
|
||||
}
|
||||
if let Some(network) = document_network.nested_network(&self.network) {
|
||||
self.send_graph(network, graph_view_overlay_open, responses);
|
||||
self.send_graph(network, graph_view_overlay_open, document_metadata, collapsed, responses);
|
||||
}
|
||||
self.update_selected(document_network, metadata, responses);
|
||||
self.update_selected(document_network, document_metadata, responses);
|
||||
}
|
||||
NodeGraphMessage::ExposeInput { node_id, input_index, new_exposed } => {
|
||||
let Some(network) = document_network.nested_network(&self.network) else {
|
||||
|
@ -696,8 +748,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
}
|
||||
responses.add(NodeGraphMessage::SetNodeInput { node_id, input_index, input });
|
||||
|
||||
let should_rerender = network.connected_to_output(node_id);
|
||||
responses.add(NodeGraphMessage::SendGraph { should_rerender });
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
}
|
||||
NodeGraphMessage::InsertNode { node_id, document_node } => {
|
||||
|
@ -711,12 +761,12 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
return;
|
||||
};
|
||||
|
||||
for node_id in metadata.selected_nodes() {
|
||||
for node_id in document_metadata.selected_nodes() {
|
||||
if let Some(node) = network.nodes.get_mut(node_id) {
|
||||
node.metadata.position += IVec2::new(displacement_x, displacement_y)
|
||||
}
|
||||
}
|
||||
self.send_graph(network, graph_view_overlay_open, responses);
|
||||
self.send_graph(network, graph_view_overlay_open, document_metadata, collapsed, responses);
|
||||
}
|
||||
NodeGraphMessage::PasteNodes { serialized_nodes } => {
|
||||
let Some(network) = document_network.nested_network(&self.network) else {
|
||||
|
@ -762,29 +812,26 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
|
||||
let nodes = new_ids.values().copied().collect();
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes });
|
||||
|
||||
responses.add(NodeGraphMessage::SendGraph { should_rerender: false });
|
||||
}
|
||||
NodeGraphMessage::RunDocumentGraph => responses.add(PortfolioMessage::SubmitGraphRender { document_id }),
|
||||
NodeGraphMessage::RunDocumentGraph => {
|
||||
responses.add(PortfolioMessage::SubmitGraphRender { document_id });
|
||||
}
|
||||
NodeGraphMessage::SelectedNodesAdd { nodes } => {
|
||||
metadata.add_selected_nodes(nodes);
|
||||
document_metadata.add_selected_nodes(nodes);
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
}
|
||||
NodeGraphMessage::SelectedNodesRemove { nodes } => {
|
||||
metadata.retain_selected_nodes(|node| !nodes.contains(node));
|
||||
document_metadata.retain_selected_nodes(|node| !nodes.contains(node));
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
}
|
||||
NodeGraphMessage::SelectedNodesSet { nodes } => {
|
||||
metadata.set_selected_nodes(nodes);
|
||||
document_metadata.set_selected_nodes(nodes);
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
}
|
||||
NodeGraphMessage::SendGraph { should_rerender } => {
|
||||
NodeGraphMessage::SendGraph => {
|
||||
if let Some(network) = document_network.nested_network(&self.network) {
|
||||
self.send_graph(network, graph_view_overlay_open, responses);
|
||||
if should_rerender {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
self.send_graph(network, graph_view_overlay_open, document_metadata, collapsed, responses);
|
||||
}
|
||||
}
|
||||
NodeGraphMessage::SetInputValue { node_id, input_index, value } => {
|
||||
|
@ -811,7 +858,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
let structure_changed = node_input.as_node().is_some() || input.as_node().is_some();
|
||||
*node_input = input;
|
||||
if structure_changed {
|
||||
load_network_structure(document_network, metadata, collapsed);
|
||||
load_network_structure(document_network, document_metadata, collapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -837,6 +884,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
}
|
||||
}
|
||||
}
|
||||
// Move all the downstream nodes to the right in the graph to allow space for a newly inserted node
|
||||
NodeGraphMessage::ShiftNode { node_id } => {
|
||||
let Some(network) = document_network.nested_network_mut(&self.network) else {
|
||||
warn!("No network");
|
||||
|
@ -883,14 +931,15 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
stack.extend(outwards_links.get(&id).unwrap_or(&Vec::new()).iter().copied())
|
||||
}
|
||||
}
|
||||
responses.add(NodeGraphMessage::SendGraph { should_rerender: false });
|
||||
|
||||
self.send_graph(network, graph_view_overlay_open, document_metadata, collapsed, responses);
|
||||
}
|
||||
NodeGraphMessage::ToggleSelectedHidden => {
|
||||
if let Some(network) = document_network.nested_network(&self.network) {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
let new_hidden = !metadata.selected_nodes().any(|id| network.disabled.contains(id));
|
||||
for &node_id in metadata.selected_nodes() {
|
||||
let new_hidden = !document_metadata.selected_nodes().any(|id| network.disabled.contains(id));
|
||||
for &node_id in document_metadata.selected_nodes() {
|
||||
responses.add(NodeGraphMessage::SetHidden { node_id, hidden: new_hidden });
|
||||
}
|
||||
}
|
||||
|
@ -908,14 +957,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
} else if !network.inputs.contains(&node_id) && !network.original_outputs().iter().any(|output| output.node_id == node_id) {
|
||||
network.disabled.push(node_id);
|
||||
}
|
||||
self.send_graph(network, graph_view_overlay_open, responses);
|
||||
|
||||
// Only generate node graph if one of the selected nodes is connected to the output
|
||||
if network.connected_to_output(node_id) {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
self.update_selection_action_buttons(document_network, metadata, responses);
|
||||
self.update_selection_action_buttons(document_network, document_metadata, responses);
|
||||
}
|
||||
NodeGraphMessage::SetName { node_id, name } => {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
@ -925,7 +973,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
if let Some(network) = document_network.nested_network_mut(&self.network) {
|
||||
if let Some(node) = network.nodes.get_mut(&node_id) {
|
||||
node.alias = name;
|
||||
responses.add(NodeGraphMessage::SendGraph { should_rerender: false });
|
||||
|
||||
self.send_graph(network, graph_view_overlay_open, document_metadata, collapsed, responses);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -944,37 +993,30 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
} else {
|
||||
return;
|
||||
}
|
||||
self.send_graph(network, graph_view_overlay_open, responses);
|
||||
}
|
||||
self.update_selection_action_buttons(document_network, metadata, responses);
|
||||
|
||||
self.update_selection_action_buttons(document_network, document_metadata, responses);
|
||||
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
NodeGraphMessage::UpdateNewNodeGraph => {
|
||||
if let Some(network) = document_network.nested_network(&self.network) {
|
||||
metadata.clear_selected_nodes();
|
||||
document_metadata.clear_selected_nodes();
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
|
||||
self.send_graph(network, graph_view_overlay_open, responses);
|
||||
self.send_graph(network, graph_view_overlay_open, document_metadata, collapsed, responses);
|
||||
|
||||
let node_types = document_node_types::collect_node_types();
|
||||
responses.add(FrontendMessage::UpdateNodeTypes { node_types });
|
||||
}
|
||||
self.update_selected(document_network, metadata, responses);
|
||||
self.update_selected(document_network, document_metadata, responses);
|
||||
}
|
||||
NodeGraphMessage::UpdateTypes { resolved_types, node_graph_errors } => {
|
||||
let changed = self.resolved_types != resolved_types || self.node_graph_errors != node_graph_errors;
|
||||
|
||||
self.resolved_types = resolved_types;
|
||||
self.node_graph_errors = node_graph_errors;
|
||||
|
||||
if changed {
|
||||
if let Some(network) = document_network.nested_network(&self.network) {
|
||||
self.send_graph(network, graph_view_overlay_open, responses)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.has_selection = metadata.has_selected_nodes();
|
||||
self.has_selection = document_metadata.has_selected_nodes();
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
|
|
|
@ -39,15 +39,14 @@ pub enum LayerClassification {
|
|||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, specta::Type)]
|
||||
pub struct LayerPanelEntry {
|
||||
pub id: NodeId,
|
||||
pub name: String,
|
||||
pub tooltip: String,
|
||||
#[serde(rename = "layerClassification")]
|
||||
pub layer_classification: LayerClassification,
|
||||
pub selected: bool,
|
||||
pub expanded: bool,
|
||||
pub disabled: bool,
|
||||
#[serde(rename = "parentId")]
|
||||
pub parent_id: Option<NodeId>,
|
||||
pub id: NodeId,
|
||||
pub depth: usize,
|
||||
pub thumbnail: String,
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
pub use super::layer_panel::LayerPanelEntry;
|
||||
|
||||
use glam::DVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
|
|
@ -95,9 +95,6 @@ pub enum PortfolioMessage {
|
|||
SelectDocument {
|
||||
document_id: DocumentId,
|
||||
},
|
||||
SetActiveDocument {
|
||||
document_id: DocumentId,
|
||||
},
|
||||
SubmitDocumentExport {
|
||||
file_name: String,
|
||||
file_type: FileType,
|
||||
|
|
|
@ -139,8 +139,6 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
|
|||
|
||||
// Send the new list of document tab names
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
responses.add(DocumentMessage::RenderDocument);
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
}
|
||||
PortfolioMessage::CloseDocumentWithConfirmation { document_id } => {
|
||||
let target_document = self.documents.get(&document_id).unwrap();
|
||||
|
@ -420,6 +418,7 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
|
|||
}
|
||||
}
|
||||
PortfolioMessage::SelectDocument { document_id } => {
|
||||
// Auto-save the document we are leaving
|
||||
if let Some(document) = self.active_document() {
|
||||
if !document.is_auto_saved() {
|
||||
responses.add(PortfolioMessage::AutoSaveDocument {
|
||||
|
@ -429,27 +428,20 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
|
|||
}
|
||||
}
|
||||
|
||||
if self.active_document().is_some() {
|
||||
responses.add(BroadcastEvent::ToolAbort);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
// Set the new active document ID
|
||||
self.active_document_id = Some(document_id);
|
||||
|
||||
// TODO: Remove this message in favor of having tools have specific data per document instance
|
||||
responses.add(PortfolioMessage::SetActiveDocument { document_id });
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
responses.add(FrontendMessage::UpdateActiveDocument { document_id });
|
||||
responses.add(DocumentMessage::RenderDocument);
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
responses.add(BroadcastEvent::ToolAbort);
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(BroadcastEvent::DocumentIsDirty);
|
||||
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
||||
responses.add(NavigationMessage::TranslateCanvas { delta: (0., 0.).into() });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
PortfolioMessage::SetActiveDocument { document_id } => {
|
||||
self.active_document_id = Some(document_id);
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
}
|
||||
PortfolioMessage::SubmitDocumentExport {
|
||||
file_name,
|
||||
file_type,
|
||||
|
@ -619,7 +611,6 @@ impl PortfolioMessageHandler {
|
|||
responses.add(ToolMessage::InitTools);
|
||||
responses.add(NodeGraphMessage::Init);
|
||||
responses.add(NavigationMessage::TranslateCanvas { delta: (0., 0.).into() });
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
responses.add(PropertiesPanelMessage::Clear);
|
||||
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ impl Pivot {
|
|||
// If more than one layer is selected we use the AABB with the mean of the pivots
|
||||
let xy_summation = document
|
||||
.selected_visible_layers()
|
||||
.map(|layer| graph_modification_utils::get_viewport_pivot(layer, &document.network, &document.metadata))
|
||||
.map(|layer| graph_modification_utils::get_viewport_pivot(layer, &document.network, &document.document_metadata))
|
||||
.reduce(|a, b| a + b)
|
||||
.unwrap_or_default();
|
||||
|
||||
|
|
|
@ -144,9 +144,10 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, DocumentId, &InputPre
|
|||
}
|
||||
ToolMessage::InitTools => {
|
||||
// Subscribe the transform layer to selection change events
|
||||
let send = Box::new(TransformLayerMessage::SelectionChanged.into());
|
||||
let on = BroadcastEvent::SelectionChanged;
|
||||
responses.add(BroadcastMessage::SubscribeEvent { send, on });
|
||||
responses.add(BroadcastMessage::SubscribeEvent {
|
||||
on: BroadcastEvent::SelectionChanged,
|
||||
send: Box::new(TransformLayerMessage::SelectionChanged.into()),
|
||||
});
|
||||
|
||||
let tool_data = &mut self.tool_state.tool_data;
|
||||
let document_data = &self.tool_state.document_tool_data;
|
||||
|
|
|
@ -223,7 +223,7 @@ impl PathToolData {
|
|||
let _selected_layers = shape_editor.selected_layers().cloned().collect::<Vec<_>>();
|
||||
|
||||
// Select the first point within the threshold (in pixels)
|
||||
if let Some(selected_points) = shape_editor.select_point(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD, shift) {
|
||||
if let Some(selected_points) = shape_editor.select_point(&document.network, &document.document_metadata, input.mouse.position, SELECTION_THRESHOLD, shift) {
|
||||
self.start_dragging_point(selected_points, input, document, responses);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
|
@ -298,7 +298,7 @@ impl PathToolData {
|
|||
|
||||
// Move the selected points with the mouse
|
||||
let snapped_position = self.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
shape_editor.move_selected_points(&document.network, &document.metadata, snapped_position - self.previous_mouse_position, shift, responses);
|
||||
shape_editor.move_selected_points(&document.network, &document.document_metadata, snapped_position - self.previous_mouse_position, shift, responses);
|
||||
self.previous_mouse_position = snapped_position;
|
||||
}
|
||||
}
|
||||
|
@ -362,7 +362,12 @@ impl Fsm for PathToolFsmState {
|
|||
if tool_data.drag_start_pos == tool_data.previous_mouse_position {
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
|
||||
} else {
|
||||
shape_editor.select_all_in_quad(&document.network, &document.metadata, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !shift_pressed);
|
||||
shape_editor.select_all_in_quad(
|
||||
&document.network,
|
||||
&document.document_metadata,
|
||||
[tool_data.drag_start_pos, tool_data.previous_mouse_position],
|
||||
!shift_pressed,
|
||||
);
|
||||
}
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
|
@ -376,7 +381,12 @@ impl Fsm for PathToolFsmState {
|
|||
if tool_data.drag_start_pos == tool_data.previous_mouse_position {
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
|
||||
} else {
|
||||
shape_editor.select_all_in_quad(&document.network, &document.metadata, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !shift_pressed);
|
||||
shape_editor.select_all_in_quad(
|
||||
&document.network,
|
||||
&document.document_metadata,
|
||||
[tool_data.drag_start_pos, tool_data.previous_mouse_position],
|
||||
!shift_pressed,
|
||||
);
|
||||
}
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||
|
@ -388,16 +398,16 @@ impl Fsm for PathToolFsmState {
|
|||
let shift_pressed = input.keyboard.get(shift_mirror_distance as usize);
|
||||
|
||||
let nearest_point = shape_editor
|
||||
.find_nearest_point_indices(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD)
|
||||
.find_nearest_point_indices(&document.network, &document.document_metadata, input.mouse.position, SELECTION_THRESHOLD)
|
||||
.map(|(_, nearest_point)| nearest_point);
|
||||
|
||||
shape_editor.delete_selected_handles_with_zero_length(&document.network, &document.metadata, &tool_data.opposing_handle_lengths, responses);
|
||||
shape_editor.delete_selected_handles_with_zero_length(&document.network, &document.document_metadata, &tool_data.opposing_handle_lengths, responses);
|
||||
|
||||
if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD && !shift_pressed {
|
||||
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == Some(point));
|
||||
if clicked_selected {
|
||||
shape_editor.deselect_all();
|
||||
shape_editor.select_point(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD, false);
|
||||
shape_editor.select_point(&document.network, &document.document_metadata, input.mouse.position, SELECTION_THRESHOLD, false);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
}
|
||||
|
@ -418,9 +428,9 @@ impl Fsm for PathToolFsmState {
|
|||
}
|
||||
(_, PathToolMessage::InsertPoint) => {
|
||||
// First we try and flip the sharpness (if they have clicked on an anchor)
|
||||
if !shape_editor.flip_sharp(&document.network, &document.metadata, input.mouse.position, SELECTION_TOLERANCE, responses) {
|
||||
if !shape_editor.flip_sharp(&document.network, &document.document_metadata, input.mouse.position, SELECTION_TOLERANCE, responses) {
|
||||
// If not, then we try and split the path that may have been clicked upon
|
||||
shape_editor.split(&document.network, &document.metadata, input.mouse.position, SELECTION_TOLERANCE, responses);
|
||||
shape_editor.split(&document.network, &document.document_metadata, input.mouse.position, SELECTION_TOLERANCE, responses);
|
||||
}
|
||||
|
||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||
|
@ -433,7 +443,7 @@ impl Fsm for PathToolFsmState {
|
|||
}
|
||||
(_, PathToolMessage::PointerMove { .. }) => self,
|
||||
(_, PathToolMessage::NudgeSelectedPoints { delta_x, delta_y }) => {
|
||||
shape_editor.move_selected_points(&document.network, &document.metadata, (delta_x, delta_y).into(), true, responses);
|
||||
shape_editor.move_selected_points(&document.network, &document.document_metadata, (delta_x, delta_y).into(), true, responses);
|
||||
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
|
@ -444,18 +454,18 @@ impl Fsm for PathToolFsmState {
|
|||
}
|
||||
(_, PathToolMessage::SelectedPointXChanged { new_x }) => {
|
||||
if let Some(&SingleSelectedPoint { coordinates, id, layer, .. }) = tool_data.selection_status.as_one() {
|
||||
shape_editor.reposition_control_point(&id, responses, &document.network, &document.metadata, DVec2::new(new_x, coordinates.y), layer);
|
||||
shape_editor.reposition_control_point(&id, responses, &document.network, &document.document_metadata, DVec2::new(new_x, coordinates.y), layer);
|
||||
}
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
(_, PathToolMessage::SelectedPointYChanged { new_y }) => {
|
||||
if let Some(&SingleSelectedPoint { coordinates, id, layer, .. }) = tool_data.selection_status.as_one() {
|
||||
shape_editor.reposition_control_point(&id, responses, &document.network, &document.metadata, DVec2::new(coordinates.x, new_y), layer);
|
||||
shape_editor.reposition_control_point(&id, responses, &document.network, &document.document_metadata, DVec2::new(coordinates.x, new_y), layer);
|
||||
}
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
(_, PathToolMessage::SelectedPointUpdated) => {
|
||||
tool_data.selection_status = get_selection_status(&document.network, &document.metadata, shape_editor);
|
||||
tool_data.selection_status = get_selection_status(&document.network, &document.document_metadata, shape_editor);
|
||||
self
|
||||
}
|
||||
(_, PathToolMessage::ManipulatorAngleMakeSmooth) => {
|
||||
|
|
|
@ -491,7 +491,7 @@ impl Fsm for SelectToolFsmState {
|
|||
&tool_data.layers_dragging,
|
||||
responses,
|
||||
&document.network,
|
||||
&document.metadata,
|
||||
&document.document_metadata,
|
||||
None,
|
||||
&ToolType::Select,
|
||||
);
|
||||
|
@ -510,7 +510,7 @@ impl Fsm for SelectToolFsmState {
|
|||
&selected,
|
||||
responses,
|
||||
&document.network,
|
||||
&document.metadata,
|
||||
&document.document_metadata,
|
||||
None,
|
||||
&ToolType::Select,
|
||||
);
|
||||
|
@ -630,7 +630,7 @@ impl Fsm for SelectToolFsmState {
|
|||
selected,
|
||||
responses,
|
||||
&document.network,
|
||||
&document.metadata,
|
||||
&document.document_metadata,
|
||||
None,
|
||||
&ToolType::Select,
|
||||
);
|
||||
|
@ -665,7 +665,7 @@ impl Fsm for SelectToolFsmState {
|
|||
&tool_data.layers_dragging,
|
||||
responses,
|
||||
&document.network,
|
||||
&document.metadata,
|
||||
&document.document_metadata,
|
||||
None,
|
||||
&ToolType::Select,
|
||||
);
|
||||
|
@ -832,7 +832,7 @@ impl Fsm for SelectToolFsmState {
|
|||
&tool_data.layers_dragging,
|
||||
responses,
|
||||
&document.network,
|
||||
&document.metadata,
|
||||
&document.document_metadata,
|
||||
None,
|
||||
&ToolType::Select,
|
||||
);
|
||||
|
|
|
@ -56,7 +56,7 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
|
|||
&selected_layers,
|
||||
responses,
|
||||
&document.network,
|
||||
&document.metadata,
|
||||
&document.document_metadata,
|
||||
Some(shape_editor),
|
||||
&tool_data.active_tool_type,
|
||||
);
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
use crate::consts::FILE_SAVE_SUFFIX;
|
||||
use crate::messages::frontend::utility_types::FrontendImageData;
|
||||
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
||||
use crate::messages::portfolio::document::node_graph::wrap_network_in_scope;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::layer_panel::LayerClassification;
|
||||
use crate::messages::portfolio::document::utility_types::misc::LayerPanelEntry;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graph_craft::concrete;
|
||||
|
@ -15,8 +12,9 @@ use graph_craft::imaginate_input::ImaginatePreferences;
|
|||
use graph_craft::proto::GraphErrors;
|
||||
use graphene_core::application_io::{NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
|
||||
use graphene_core::memo::IORecord;
|
||||
use graphene_core::raster::{Image, ImageFrame};
|
||||
use graphene_core::renderer::{ClickTarget, GraphicElementRendered, SvgSegmentList};
|
||||
use graphene_core::raster::ImageFrame;
|
||||
use graphene_core::renderer::{ClickTarget, GraphicElementRendered, ImageRenderMode, RenderParams, SvgRender};
|
||||
use graphene_core::renderer::{RenderSvgSegmentList, SvgSegment};
|
||||
use graphene_core::text::FontCache;
|
||||
use graphene_core::transform::{Footprint, Transform};
|
||||
use graphene_core::vector::style::ViewMode;
|
||||
|
@ -27,28 +25,45 @@ use interpreted_executor::dynamic_executor::{DynamicExecutor, ResolvedDocumentNo
|
|||
|
||||
use glam::{DAffine2, DVec2, UVec2};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::Hash;
|
||||
use std::hash::Hasher;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Persistent data between graph executions. It's updated via message passing from the editor thread with [`NodeRuntimeMessage`]`.
|
||||
/// Some of these fields are put into a [`WasmEditorApi`] which is passed to the final compiled graph network upon each execution.
|
||||
/// Once the implementation is finished, this will live in a separate thread. Right now it's part of the main JS thread, but its own separate JS stack frame independent from the editor.
|
||||
pub struct NodeRuntime {
|
||||
pub(crate) executor: DynamicExecutor,
|
||||
font_cache: FontCache,
|
||||
executor: DynamicExecutor,
|
||||
receiver: Receiver<NodeRuntimeMessage>,
|
||||
sender: InternalNodeGraphUpdateSender,
|
||||
wasm_io: Option<WasmApplicationIo>,
|
||||
|
||||
/// Font data (for rendering text) made available to the graph through the [`WasmEditorApi`].
|
||||
font_cache: FontCache,
|
||||
/// Imaginate preferences made available to the graph through the [`WasmEditorApi`].
|
||||
imaginate_preferences: ImaginatePreferences,
|
||||
pub(crate) thumbnails: HashMap<NodeId, SvgSegmentList>,
|
||||
pub(crate) click_targets: HashMap<NodeId, Vec<ClickTarget>>,
|
||||
pub(crate) upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
|
||||
pub(crate) resolved_types: ResolvedDocumentNodeTypes,
|
||||
pub(crate) node_graph_errors: GraphErrors,
|
||||
|
||||
/// Gives access to APIs like a rendering surface (native window handle or HTML5 canvas) and WGPU (which becomes WebGPU on web).
|
||||
wasm_application_io: Option<WasmApplicationIo>,
|
||||
graph_hash: Option<u64>,
|
||||
node_graph_errors: GraphErrors,
|
||||
resolved_types: ResolvedDocumentNodeTypes,
|
||||
monitor_nodes: Vec<Vec<NodeId>>,
|
||||
|
||||
// TODO: Remove, it doesn't need to be persisted anymore
|
||||
/// The current renders of the thumbnails for layer nodes.
|
||||
thumbnail_renders: HashMap<NodeId, Vec<SvgSegment>>,
|
||||
/// The current click targets for layer nodes.
|
||||
click_targets: HashMap<NodeId, Vec<ClickTarget>>,
|
||||
/// The current upstream transforms for nodes.
|
||||
upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
|
||||
}
|
||||
|
||||
/// Messages passed from the editor thread to the node runtime thread.
|
||||
enum NodeRuntimeMessage {
|
||||
GenerationRequest(GenerationRequest),
|
||||
ExecutionRequest(ExecutionRequest),
|
||||
FontCacheUpdate(FontCache),
|
||||
ImaginatePreferencesUpdate(ImaginatePreferences),
|
||||
}
|
||||
|
@ -63,17 +78,16 @@ pub struct ExportConfig {
|
|||
pub size: DVec2,
|
||||
}
|
||||
|
||||
pub(crate) struct GenerationRequest {
|
||||
generation_id: u64,
|
||||
pub(crate) struct ExecutionRequest {
|
||||
execution_id: u64,
|
||||
graph: NodeNetwork,
|
||||
render_config: RenderConfig,
|
||||
}
|
||||
|
||||
pub(crate) struct GenerationResponse {
|
||||
generation_id: u64,
|
||||
pub(crate) struct ExecutionResponse {
|
||||
execution_id: u64,
|
||||
result: Result<TaggedValue, String>,
|
||||
updates: VecDeque<Message>,
|
||||
new_thumbnails: HashMap<NodeId, SvgSegmentList>,
|
||||
responses: VecDeque<Message>,
|
||||
new_click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
|
||||
new_upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
|
||||
resolved_types: ResolvedDocumentNodeTypes,
|
||||
|
@ -82,15 +96,15 @@ pub(crate) struct GenerationResponse {
|
|||
}
|
||||
|
||||
enum NodeGraphUpdate {
|
||||
GenerationResponse(GenerationResponse),
|
||||
ExecutionResponse(ExecutionResponse),
|
||||
NodeGraphUpdateMessage(NodeGraphUpdateMessage),
|
||||
}
|
||||
|
||||
struct InternalNodeGraphUpdateSender(Sender<NodeGraphUpdate>);
|
||||
|
||||
impl InternalNodeGraphUpdateSender {
|
||||
fn send_generation_response(&self, response: GenerationResponse) {
|
||||
self.0.send(NodeGraphUpdate::GenerationResponse(response)).expect("Failed to send response")
|
||||
fn send_generation_response(&self, response: ExecutionResponse) {
|
||||
self.0.send(NodeGraphUpdate::ExecutionResponse(response)).expect("Failed to send response")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,30 +120,33 @@ thread_local! {
|
|||
|
||||
impl NodeRuntime {
|
||||
fn new(receiver: Receiver<NodeRuntimeMessage>, sender: Sender<NodeGraphUpdate>) -> Self {
|
||||
let executor = DynamicExecutor::default();
|
||||
Self {
|
||||
executor,
|
||||
executor: DynamicExecutor::default(),
|
||||
receiver,
|
||||
sender: InternalNodeGraphUpdateSender(sender),
|
||||
|
||||
font_cache: FontCache::default(),
|
||||
imaginate_preferences: Default::default(),
|
||||
thumbnails: Default::default(),
|
||||
wasm_io: None,
|
||||
click_targets: HashMap::new(),
|
||||
|
||||
wasm_application_io: None,
|
||||
graph_hash: None,
|
||||
upstream_transforms: HashMap::new(),
|
||||
resolved_types: ResolvedDocumentNodeTypes::default(),
|
||||
node_graph_errors: Vec::new(),
|
||||
resolved_types: ResolvedDocumentNodeTypes::default(),
|
||||
monitor_nodes: Vec::new(),
|
||||
|
||||
thumbnail_renders: Default::default(),
|
||||
click_targets: HashMap::new(),
|
||||
upstream_transforms: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) {
|
||||
let mut requests = self.receiver.try_iter().collect::<Vec<_>>();
|
||||
// TODO: Currently we still render the document after we submit the node graph execution request.
|
||||
// This should be avoided in the future.
|
||||
requests.reverse();
|
||||
requests.dedup_by_key(|x| match x {
|
||||
NodeRuntimeMessage::GenerationRequest(x) => Some(x.graph.current_hash()),
|
||||
NodeRuntimeMessage::ExecutionRequest(x) => Some(x.graph.current_hash()),
|
||||
_ => None,
|
||||
});
|
||||
requests.reverse();
|
||||
|
@ -137,49 +154,45 @@ impl NodeRuntime {
|
|||
match request {
|
||||
NodeRuntimeMessage::FontCacheUpdate(font_cache) => self.font_cache = font_cache,
|
||||
NodeRuntimeMessage::ImaginatePreferencesUpdate(preferences) => self.imaginate_preferences = preferences,
|
||||
NodeRuntimeMessage::GenerationRequest(GenerationRequest {
|
||||
generation_id, graph, render_config, ..
|
||||
NodeRuntimeMessage::ExecutionRequest(ExecutionRequest {
|
||||
execution_id, graph, render_config, ..
|
||||
}) => {
|
||||
let transform = render_config.viewport.transform;
|
||||
|
||||
let result = self.execute_network(graph, render_config).await;
|
||||
|
||||
let mut responses = VecDeque::new();
|
||||
self.process_monitor_nodes(&mut responses);
|
||||
|
||||
self.update_thumbnails(&mut responses);
|
||||
self.update_upstream_transforms();
|
||||
|
||||
let response = GenerationResponse {
|
||||
generation_id,
|
||||
self.sender.send_generation_response(ExecutionResponse {
|
||||
execution_id,
|
||||
result,
|
||||
updates: responses,
|
||||
new_thumbnails: self.thumbnails.clone(),
|
||||
responses,
|
||||
new_click_targets: self.click_targets.clone().into_iter().map(|(id, targets)| (LayerNodeIdentifier::new_unchecked(id), targets)).collect(),
|
||||
new_upstream_transforms: self.upstream_transforms.clone(),
|
||||
resolved_types: self.resolved_types.clone(),
|
||||
node_graph_errors: core::mem::take(&mut self.node_graph_errors),
|
||||
transform,
|
||||
};
|
||||
self.sender.send_generation_response(response);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_network<'a>(&'a mut self, graph: NodeNetwork, render_config: RenderConfig) -> Result<TaggedValue, String> {
|
||||
if self.wasm_io.is_none() {
|
||||
self.wasm_io = Some(WasmApplicationIo::new().await);
|
||||
if self.wasm_application_io.is_none() {
|
||||
self.wasm_application_io = Some(WasmApplicationIo::new().await);
|
||||
}
|
||||
|
||||
let editor_api = WasmEditorApi {
|
||||
font_cache: &self.font_cache,
|
||||
application_io: self.wasm_io.as_ref().unwrap(),
|
||||
node_graph_message_sender: &self.sender,
|
||||
imaginate_preferences: &self.imaginate_preferences,
|
||||
application_io: self.wasm_application_io.as_ref().unwrap(),
|
||||
node_graph_message_sender: &self.sender,
|
||||
render_config,
|
||||
image_frame: None,
|
||||
};
|
||||
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::Hash;
|
||||
use std::hash::Hasher;
|
||||
// Required to ensure that the appropriate protonodes are reinserted when the Editor API changes.
|
||||
let mut graph_input_hash = DefaultHasher::new();
|
||||
editor_api.font_cache.hash(&mut graph_input_hash);
|
||||
|
@ -243,85 +256,85 @@ impl NodeRuntime {
|
|||
Ok(result)
|
||||
}
|
||||
|
||||
/// Recomputes the thumbnails for the layers in the graph, modifying the state and updating the UI.
|
||||
pub fn update_thumbnails(&mut self, responses: &mut VecDeque<Message>) {
|
||||
let mut image_data: Vec<_> = Vec::new();
|
||||
self.thumbnails.retain(|id, _| self.monitor_nodes.iter().any(|node_path| node_path.contains(id)));
|
||||
for node_path in &self.monitor_nodes {
|
||||
let Some(node_id) = node_path.get(node_path.len() - 2).copied() else {
|
||||
/// Updates state data
|
||||
pub fn process_monitor_nodes(&mut self, responses: &mut VecDeque<Message>) {
|
||||
// TODO: Consider optimizing this since it's currently O(m*n^2), with a sort it could be made O(m * n*log(n))
|
||||
self.thumbnail_renders.retain(|id, _| self.monitor_nodes.iter().any(|monitor_node_path| monitor_node_path.contains(id)));
|
||||
|
||||
for monitor_node_path in &self.monitor_nodes {
|
||||
// The monitor nodes are located within a document node, and are thus children in that network, so this gets the parent document node's ID
|
||||
let Some(parent_network_node_id) = monitor_node_path.get(monitor_node_path.len() - 2).copied() else {
|
||||
warn!("Monitor node has invalid node id");
|
||||
continue;
|
||||
};
|
||||
let Some(value) = self.executor.introspect(node_path).flatten() else {
|
||||
warn!("Failed to introspect monitor node for thumbnail");
|
||||
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(io_data) = value.downcast_ref::<IORecord<Footprint, graphene_core::GraphicElement>>() else {
|
||||
// Extract the monitor node's stored `GraphicElement` data.
|
||||
let Some(introspected_data) = self.executor.introspect(monitor_node_path).flatten() else {
|
||||
// TODO: Fix the root of the issue causing the spam of this warning (this at least temporarily disables it in release builds)
|
||||
#[cfg(debug_assertions)]
|
||||
warn!("Failed to introspect monitor node");
|
||||
|
||||
continue;
|
||||
};
|
||||
let graphic_element = &io_data.output;
|
||||
use graphene_core::renderer::*;
|
||||
let bounds = graphic_element.bounding_box(DAffine2::IDENTITY);
|
||||
let render_params = RenderParams::new(ViewMode::Normal, ImageRenderMode::BlobUrl, bounds, true, false, false);
|
||||
let mut render = SvgRender::new();
|
||||
graphic_element.render_svg(&mut render, &render_params);
|
||||
let [min, max] = bounds.unwrap_or_default();
|
||||
render.format_svg(min, max);
|
||||
|
||||
let click_targets = self.click_targets.entry(node_id).or_default();
|
||||
click_targets.clear();
|
||||
// Add the graphic element data's click targets to the click targets vector
|
||||
graphic_element.add_click_targets(click_targets);
|
||||
// If this is `GraphicElement` data:
|
||||
// Regenerate click targets and thumbnails for the layers in the graph, modifying the state and updating the UI.
|
||||
if let Some(io_data) = introspected_data.downcast_ref::<IORecord<Footprint, graphene_core::GraphicElement>>() {
|
||||
let graphic_element = &io_data.output;
|
||||
|
||||
let old_thumbnail = self.thumbnails.entry(node_id).or_default();
|
||||
if *old_thumbnail != render.svg {
|
||||
responses.add(FrontendMessage::UpdateNodeThumbnail {
|
||||
id: node_id,
|
||||
value: render.svg.to_string(),
|
||||
});
|
||||
*old_thumbnail = render.svg;
|
||||
// UPDATE CLICK TARGETS
|
||||
|
||||
// Get the previously stored click targets and wipe them out, then regenerate them
|
||||
let click_targets = self.click_targets.entry(parent_network_node_id).or_default();
|
||||
click_targets.clear();
|
||||
graphic_element.add_click_targets(click_targets);
|
||||
|
||||
// RENDER THUMBNAIL
|
||||
|
||||
let bounds = graphic_element.bounding_box(DAffine2::IDENTITY);
|
||||
|
||||
// Render the thumbnail from a `GraphicElement` into an SVG string
|
||||
let render_params = RenderParams::new(ViewMode::Normal, ImageRenderMode::Base64, bounds, true, false, false);
|
||||
let mut render = SvgRender::new();
|
||||
graphic_element.render_svg(&mut render, &render_params);
|
||||
|
||||
// And give the SVG a viewbox and outer <svg>...</svg> wrapper tag
|
||||
let [min, max] = bounds.unwrap_or_default();
|
||||
render.format_svg(min, max);
|
||||
|
||||
// UPDATE FRONTEND THUMBNAIL
|
||||
|
||||
let new_thumbnail_svg = render.svg;
|
||||
let old_thumbnail_svg = self.thumbnail_renders.entry(parent_network_node_id).or_default();
|
||||
|
||||
if old_thumbnail_svg != &new_thumbnail_svg {
|
||||
responses.add(FrontendMessage::UpdateNodeThumbnail {
|
||||
id: parent_network_node_id,
|
||||
value: new_thumbnail_svg.to_svg_string(),
|
||||
});
|
||||
*old_thumbnail_svg = new_thumbnail_svg;
|
||||
}
|
||||
}
|
||||
|
||||
let resize = Some(DVec2::splat(100.));
|
||||
image_data.extend(render.image_data.into_iter().filter_map(|(_, image)| NodeGraphExecutor::to_frontend_image_data(image, resize).ok()))
|
||||
}
|
||||
if !image_data.is_empty() {
|
||||
responses.add(FrontendMessage::UpdateImageData {
|
||||
document_id: DocumentId(0),
|
||||
image_data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_upstream_transforms(&mut self) {
|
||||
for node_path in &self.monitor_nodes {
|
||||
let Some(node_id) = node_path.get(node_path.len() - 2).copied() else {
|
||||
warn!("Monitor node has invalid node id");
|
||||
continue;
|
||||
};
|
||||
let Some(value) = self.executor.introspect(node_path).flatten() else {
|
||||
warn!("Failed to introspect monitor node for upstream transforms");
|
||||
continue;
|
||||
};
|
||||
|
||||
fn try_downcast<T: Transform + 'static>(value: &dyn std::any::Any) -> Option<(Footprint, DAffine2)> {
|
||||
let io_data = value.downcast_ref::<IORecord<Footprint, T>>()?;
|
||||
let transform = io_data.output.transform();
|
||||
Some((io_data.input, transform))
|
||||
// If this is `VectorData`, `ImageFrame`, or `GraphicElement` data:
|
||||
// Update the stored upstream transforms for this layer/node.
|
||||
if let Some(transform) = {
|
||||
fn try_downcast<T: Transform + 'static>(value: &dyn std::any::Any) -> Option<(Footprint, DAffine2)> {
|
||||
let io_data = value.downcast_ref::<IORecord<Footprint, T>>()?;
|
||||
let transform = io_data.output.transform();
|
||||
Some((io_data.input, transform))
|
||||
}
|
||||
None.or_else(|| try_downcast::<VectorData>(introspected_data.as_ref()))
|
||||
.or_else(|| try_downcast::<ImageFrame<Color>>(introspected_data.as_ref()))
|
||||
.or_else(|| try_downcast::<GraphicElement>(introspected_data.as_ref()))
|
||||
} {
|
||||
self.upstream_transforms.insert(parent_network_node_id, transform);
|
||||
}
|
||||
|
||||
let Some(transform) = try_downcast::<VectorData>(value.as_ref())
|
||||
.or_else(|| try_downcast::<ImageFrame<Color>>(value.as_ref()))
|
||||
.or_else(|| try_downcast::<GraphicElement>(value.as_ref()))
|
||||
else {
|
||||
warn!("Failed to downcast transform input");
|
||||
continue;
|
||||
};
|
||||
self.upstream_transforms.insert(node_id, transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn introspect_node(path: &[NodeId]) -> Option<Arc<dyn std::any::Any>> {
|
||||
NODE_RUNTIME
|
||||
.try_with(|runtime| {
|
||||
|
@ -384,15 +397,15 @@ impl Default for NodeGraphExecutor {
|
|||
impl NodeGraphExecutor {
|
||||
/// Execute the network by flattening it and creating a borrow stack.
|
||||
fn queue_execution(&self, network: NodeNetwork, render_config: RenderConfig) -> u64 {
|
||||
let generation_id = generate_uuid();
|
||||
let request = GenerationRequest {
|
||||
let execution_id = generate_uuid();
|
||||
let request = ExecutionRequest {
|
||||
graph: network,
|
||||
generation_id,
|
||||
execution_id,
|
||||
render_config,
|
||||
};
|
||||
self.sender.send(NodeRuntimeMessage::GenerationRequest(request)).expect("Failed to send generation request");
|
||||
self.sender.send(NodeRuntimeMessage::ExecutionRequest(request)).expect("Failed to send generation request");
|
||||
|
||||
generation_id
|
||||
execution_id
|
||||
}
|
||||
|
||||
pub fn introspect_node(&self, path: &[NodeId]) -> Option<Arc<dyn std::any::Any>> {
|
||||
|
@ -429,36 +442,6 @@ impl NodeGraphExecutor {
|
|||
Some(extract_data(downcasted))
|
||||
}
|
||||
|
||||
/// Encodes an image into a format using the image crate
|
||||
fn encode_img(image: Image<Color>, resize: Option<DVec2>, format: image::ImageOutputFormat) -> Result<(Vec<u8>, (u32, u32)), String> {
|
||||
use image::{ImageBuffer, Rgba};
|
||||
use std::io::Cursor;
|
||||
|
||||
let (result_bytes, width, height) = image.to_flat_u8();
|
||||
|
||||
let mut output: ImageBuffer<Rgba<u8>, _> = image::ImageBuffer::from_raw(width, height, result_bytes).ok_or_else(|| "Invalid image size".to_string())?;
|
||||
if let Some(size) = resize {
|
||||
let size = size.as_uvec2();
|
||||
if size.x > 0 && size.y > 0 {
|
||||
output = image::imageops::resize(&output, size.x, size.y, image::imageops::Triangle);
|
||||
}
|
||||
}
|
||||
let size = output.dimensions();
|
||||
let mut image_data: Vec<u8> = Vec::new();
|
||||
output.write_to(&mut Cursor::new(&mut image_data), format).map_err(|e| e.to_string())?;
|
||||
Ok::<_, String>((image_data, size))
|
||||
}
|
||||
|
||||
/// Generate a new [`FrontendImageData`] from the [`Image`].
|
||||
fn to_frontend_image_data(image: Image<Color>, resize: Option<DVec2>) -> Result<FrontendImageData, String> {
|
||||
let (image_data, _size) = Self::encode_img(image, resize, image::ImageOutputFormat::Bmp)?;
|
||||
|
||||
let mime = "image/bmp".to_string();
|
||||
let image_data = std::sync::Arc::new(image_data);
|
||||
|
||||
Ok(FrontendImageData { image_data, mime })
|
||||
}
|
||||
|
||||
/// Evaluates a node graph, computing the entire graph
|
||||
pub fn submit_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, viewport_resolution: UVec2) -> Result<(), String> {
|
||||
// Get the node graph layer
|
||||
|
@ -466,7 +449,7 @@ impl NodeGraphExecutor {
|
|||
|
||||
let render_config = RenderConfig {
|
||||
viewport: Footprint {
|
||||
transform: document.metadata.document_to_viewport,
|
||||
transform: document.document_metadata.document_to_viewport,
|
||||
resolution: viewport_resolution,
|
||||
..Default::default()
|
||||
},
|
||||
|
@ -480,9 +463,9 @@ impl NodeGraphExecutor {
|
|||
};
|
||||
|
||||
// Execute the node graph
|
||||
let generation_id = self.queue_execution(network, render_config);
|
||||
let execution_id = self.queue_execution(network, render_config);
|
||||
|
||||
self.futures.insert(generation_id, ExecutionContext { export_config: None });
|
||||
self.futures.insert(execution_id, ExecutionContext { export_config: None });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -515,9 +498,9 @@ impl NodeGraphExecutor {
|
|||
export_config.size = size;
|
||||
|
||||
// Execute the node graph
|
||||
let generation_id = self.queue_execution(network, render_config);
|
||||
let execution_id = self.queue_execution(network, render_config);
|
||||
let execution_context = ExecutionContext { export_config: Some(export_config) };
|
||||
self.futures.insert(generation_id, execution_context);
|
||||
self.futures.insert(execution_id, execution_context);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -552,89 +535,59 @@ impl NodeGraphExecutor {
|
|||
}
|
||||
|
||||
pub fn poll_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
||||
let DocumentMessageHandler {
|
||||
network: document_network,
|
||||
metadata: document_metadata,
|
||||
collapsed,
|
||||
..
|
||||
} = document;
|
||||
|
||||
let results = self.receiver.try_iter().collect::<Vec<_>>();
|
||||
for response in results {
|
||||
match response {
|
||||
NodeGraphUpdate::GenerationResponse(GenerationResponse {
|
||||
generation_id,
|
||||
result,
|
||||
updates,
|
||||
new_thumbnails,
|
||||
new_click_targets,
|
||||
new_upstream_transforms,
|
||||
resolved_types,
|
||||
node_graph_errors,
|
||||
transform,
|
||||
}) => {
|
||||
NodeGraphUpdate::ExecutionResponse(execution_response) => {
|
||||
let ExecutionResponse {
|
||||
execution_id,
|
||||
result,
|
||||
responses: existing_responses,
|
||||
new_click_targets,
|
||||
new_upstream_transforms,
|
||||
resolved_types,
|
||||
node_graph_errors,
|
||||
transform,
|
||||
} = execution_response;
|
||||
|
||||
responses.extend(existing_responses);
|
||||
responses.add(NodeGraphMessage::UpdateTypes { resolved_types, node_graph_errors });
|
||||
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())?;
|
||||
|
||||
if let Some(export_config) = execution_context.export_config {
|
||||
return self.export(node_graph_output, export_config, responses);
|
||||
}
|
||||
|
||||
for (&node_id, svg) in &new_thumbnails {
|
||||
if !document_network.nodes.contains_key(&node_id) {
|
||||
warn!("Missing node");
|
||||
continue;
|
||||
}
|
||||
let layer = LayerNodeIdentifier::new(node_id, document_network);
|
||||
responses.add(FrontendMessage::UpdateDocumentLayerDetails {
|
||||
data: LayerPanelEntry {
|
||||
name: document_network.nodes.get(&node_id).map(|node| node.alias.clone()).unwrap_or_default(),
|
||||
tooltip: if cfg!(debug_assertions) { format!("Layer ID: {node_id}") } else { "".into() },
|
||||
layer_classification: if document_metadata.is_artboard(layer) {
|
||||
LayerClassification::Artboard
|
||||
} else if document_metadata.is_folder(layer) {
|
||||
LayerClassification::Folder
|
||||
} else {
|
||||
LayerClassification::Layer
|
||||
},
|
||||
expanded: layer.has_children(document_metadata) && !collapsed.contains(&layer),
|
||||
selected: document_metadata.selected_layers_contains(layer),
|
||||
parent_id: layer.parent(document_metadata).map(|parent| parent.to_node()),
|
||||
id: node_id,
|
||||
depth: layer.ancestors(document_metadata).count() - 1,
|
||||
thumbnail: svg.to_string(),
|
||||
},
|
||||
});
|
||||
}
|
||||
document_metadata.update_transforms(new_upstream_transforms);
|
||||
document_metadata.update_click_targets(new_click_targets);
|
||||
responses.extend(updates);
|
||||
self.process_node_graph_output(node_graph_output, transform, responses)?;
|
||||
responses.add(DocumentMessage::RenderDocument);
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
responses.add(BroadcastEvent::DocumentIsDirty);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
let node_graph_output = result.map_err(|e| format!("Node graph evaluation failed: {e:?}"))?;
|
||||
|
||||
document.document_metadata.update_transforms(new_upstream_transforms);
|
||||
document.document_metadata.update_click_targets(new_click_targets);
|
||||
|
||||
let execution_context = self.futures.remove(&execution_id).ok_or_else(|| "Invalid generation ID".to_string())?;
|
||||
if let Some(export_config) = execution_context.export_config {
|
||||
// Special handling for exporting the artwork
|
||||
self.export(node_graph_output, export_config, responses)?
|
||||
} else {
|
||||
self.process_node_graph_output(node_graph_output, transform, responses)?
|
||||
}
|
||||
}
|
||||
NodeGraphUpdate::NodeGraphUpdateMessage(NodeGraphUpdateMessage::ImaginateStatusUpdate) => {
|
||||
responses.add(DocumentMessage::PropertiesPanel(PropertiesPanelMessage::Refresh));
|
||||
}
|
||||
NodeGraphUpdate::NodeGraphUpdateMessage(NodeGraphUpdateMessage::ImaginateStatusUpdate) => responses.add(DocumentMessage::PropertiesPanel(PropertiesPanelMessage::Refresh)),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(render_object: impl GraphicElementRendered, transform: DAffine2, responses: &mut VecDeque<Message>) {
|
||||
use graphene_core::renderer::{ImageRenderMode, RenderParams, SvgRender};
|
||||
|
||||
fn debug_render(render_object: impl GraphicElementRendered, transform: DAffine2, responses: &mut VecDeque<Message>) {
|
||||
// Setup rendering
|
||||
let mut render = SvgRender::new();
|
||||
let render_params = RenderParams::new(ViewMode::Normal, ImageRenderMode::BlobUrl, None, false, false, false);
|
||||
let render_params = RenderParams::new(ViewMode::Normal, ImageRenderMode::Base64, None, false, false, 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, None);
|
||||
let svg = render.svg.to_string();
|
||||
let svg = render.svg.to_svg_string();
|
||||
|
||||
// Send to frontend
|
||||
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
||||
|
@ -667,16 +620,16 @@ impl NodeGraphExecutor {
|
|||
);
|
||||
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
||||
}
|
||||
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::GraphicGroup(render_object) => Self::render(render_object, transform, responses),
|
||||
TaggedValue::Artboard(render_object) => Self::render(render_object, transform, responses),
|
||||
TaggedValue::ImageFrame(render_object) => Self::render(render_object, transform, responses),
|
||||
TaggedValue::Palette(render_object) => Self::render(render_object, transform, responses),
|
||||
TaggedValue::Bool(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
TaggedValue::String(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
TaggedValue::F32(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
TaggedValue::F64(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
TaggedValue::OptionalColor(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
TaggedValue::VectorData(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
TaggedValue::GraphicGroup(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
TaggedValue::Artboard(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
TaggedValue::ImageFrame(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
TaggedValue::Palette(render_object) => Self::debug_render(render_object, transform, responses),
|
||||
_ => {
|
||||
return Err(format!("Invalid node graph output type: {node_graph_output:#?}"));
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")]
|
||||
|
||||
use graphite_editor::application::Editor;
|
||||
use graphite_editor::messages::frontend::utility_types::FrontendImageData;
|
||||
use graphite_editor::messages::prelude::*;
|
||||
|
||||
// use axum::body::StreamBody;
|
||||
|
@ -90,32 +89,15 @@ fn handle_message(message: String) -> String {
|
|||
editor.as_mut().unwrap().handle_message(message)
|
||||
});
|
||||
|
||||
// Sends a FrontendMessage to JavaScript
|
||||
fn send_frontend_message_to_js(message: FrontendMessage) -> FrontendMessage {
|
||||
// Special case for update image data to avoid serialization times.
|
||||
if let FrontendMessage::UpdateImageData { document_id, image_data } = message {
|
||||
let mut stub_data = Vec::with_capacity(image_data.len());
|
||||
for image in image_data {
|
||||
stub_data.push(FrontendImageData {
|
||||
mime: image.mime.clone(),
|
||||
image_data: Arc::new(Vec::new()),
|
||||
});
|
||||
}
|
||||
FrontendMessage::UpdateImageData { document_id, image_data: stub_data }
|
||||
} else {
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
for response in &responses {
|
||||
let serialized = ron::to_string(&send_frontend_message_to_js(response.clone())).unwrap();
|
||||
let serialized = ron::to_string(&response.clone()).unwrap();
|
||||
if let Err(error) = ron::from_str::<FrontendMessage>(&serialized) {
|
||||
log::error!("Error deserializing message: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
// Process any `FrontendMessage` responses resulting from the backend processing the dispatched message
|
||||
let result: Vec<_> = responses.into_iter().map(send_frontend_message_to_js).collect();
|
||||
let result: Vec<_> = responses.into_iter().collect();
|
||||
|
||||
ron::to_string(&result).expect("Failed to serialize FrontendMessage")
|
||||
}
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
import { getContext, onMount, tick } from "svelte";
|
||||
|
||||
import { beginDraggingElement } from "@graphite/io-managers/drag";
|
||||
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
import { platformIsMac } from "@graphite/utility-functions/platform";
|
||||
import type { Editor } from "@graphite/wasm-communication/editor";
|
||||
import { defaultWidgetLayout, patchWidgetLayout, UpdateDocumentLayerDetails, UpdateDocumentLayerStructureJs, UpdateLayersPanelOptionsLayout } from "@graphite/wasm-communication/messages";
|
||||
import type { LayerClassification, LayerPanelEntry } from "@graphite/wasm-communication/messages";
|
||||
import type { DataBuffer, LayerClassification, LayerPanelEntry } from "@graphite/wasm-communication/messages";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
@ -20,8 +21,6 @@
|
|||
entry: LayerPanelEntry;
|
||||
};
|
||||
|
||||
let list: LayoutCol | undefined;
|
||||
|
||||
const RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT = 20;
|
||||
const INSERT_MARK_OFFSET = 2;
|
||||
|
||||
|
@ -35,6 +34,9 @@
|
|||
};
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
|
||||
|
||||
let list: LayoutCol | undefined;
|
||||
|
||||
// Layer data
|
||||
let layerCache = new Map<string, LayerPanelEntry>(); // TODO: replace with BigUint64Array as index
|
||||
|
@ -56,7 +58,8 @@
|
|||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerStructureJs, (updateDocumentLayerStructure) => {
|
||||
rebuildLayerHierarchy(updateDocumentLayerStructure);
|
||||
const structure = newUpdateDocumentLayerStructure(updateDocumentLayerStructure.dataBuffer);
|
||||
rebuildLayerHierarchy(structure);
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerDetails, (updateDocumentLayerDetails) => {
|
||||
|
@ -67,6 +70,65 @@
|
|||
});
|
||||
});
|
||||
|
||||
type DocumentLayerStructure = {
|
||||
layerId: bigint;
|
||||
children: DocumentLayerStructure[];
|
||||
};
|
||||
|
||||
function newUpdateDocumentLayerStructure(dataBuffer: DataBuffer): DocumentLayerStructure {
|
||||
const pointerNum = Number(dataBuffer.pointer);
|
||||
const lengthNum = Number(dataBuffer.length);
|
||||
|
||||
const wasmMemoryBuffer = editor.raw.buffer;
|
||||
|
||||
// Decode the folder structure encoding
|
||||
const encoding = new DataView(wasmMemoryBuffer, pointerNum, lengthNum);
|
||||
|
||||
// The structure section indicates how to read through the upcoming layer list and assign depths to each layer
|
||||
const structureSectionLength = Number(encoding.getBigUint64(0, true));
|
||||
const structureSectionMsbSigned = new DataView(wasmMemoryBuffer, pointerNum + 8, structureSectionLength * 8);
|
||||
|
||||
// The layer IDs section lists each layer ID sequentially in the tree, as it will show up in the panel
|
||||
const layerIdsSection = new DataView(wasmMemoryBuffer, pointerNum + 8 + structureSectionLength * 8);
|
||||
|
||||
let layersEncountered = 0;
|
||||
let currentFolder: DocumentLayerStructure = { layerId: BigInt(-1), children: [] };
|
||||
const currentFolderStack = [currentFolder];
|
||||
|
||||
for (let i = 0; i < structureSectionLength; i += 1) {
|
||||
const msbSigned = structureSectionMsbSigned.getBigUint64(i * 8, true);
|
||||
const msbMask = BigInt(1) << BigInt(64 - 1);
|
||||
|
||||
// Set the MSB to 0 to clear the sign and then read the number as usual
|
||||
const numberOfLayersAtThisDepth = msbSigned & ~msbMask;
|
||||
|
||||
// Store child folders in the current folder (until we are interrupted by an indent)
|
||||
for (let j = 0; j < numberOfLayersAtThisDepth; j += 1) {
|
||||
const layerId = layerIdsSection.getBigUint64(layersEncountered * 8, true);
|
||||
layersEncountered += 1;
|
||||
|
||||
const childLayer: DocumentLayerStructure = { layerId, children: [] };
|
||||
currentFolder.children.push(childLayer);
|
||||
}
|
||||
|
||||
// Check the sign of the MSB, where a 1 is a negative (outward) indent
|
||||
const subsequentDirectionOfDepthChange = (msbSigned & msbMask) === BigInt(0);
|
||||
// Inward
|
||||
if (subsequentDirectionOfDepthChange) {
|
||||
currentFolderStack.push(currentFolder);
|
||||
currentFolder = currentFolder.children[currentFolder.children.length - 1];
|
||||
}
|
||||
// Outward
|
||||
else {
|
||||
const popped = currentFolderStack.pop();
|
||||
if (!popped) throw Error("Too many negative indents in the folder structure");
|
||||
if (popped) currentFolder = popped;
|
||||
}
|
||||
}
|
||||
|
||||
return currentFolder;
|
||||
}
|
||||
|
||||
function toggleLayerVisibility(id: bigint) {
|
||||
editor.instance.toggleLayerVisibility(id);
|
||||
}
|
||||
|
@ -222,11 +284,11 @@
|
|||
async function dragStart(event: DragEvent, listing: LayerListingInfo) {
|
||||
const layer = listing.entry;
|
||||
dragInPanel = true;
|
||||
if (!layer.selected) {
|
||||
if (!$nodeGraph.selected.includes(layer.id)) {
|
||||
fakeHighlight = layer.id;
|
||||
}
|
||||
const select = () => {
|
||||
if (!layer.selected) selectLayer(listing, false, false);
|
||||
if (!$nodeGraph.selected.includes(layer.id)) selectLayer(listing, false, false);
|
||||
};
|
||||
|
||||
const target = (event.target instanceof HTMLElement && event.target) || undefined;
|
||||
|
@ -263,7 +325,7 @@
|
|||
dragInPanel = false;
|
||||
}
|
||||
|
||||
function rebuildLayerHierarchy(updateDocumentLayerStructure: UpdateDocumentLayerStructureJs) {
|
||||
function rebuildLayerHierarchy(updateDocumentLayerStructure: DocumentLayerStructure) {
|
||||
const layerWithNameBeingEdited = layers.find((layer: LayerListingInfo) => layer.editingName);
|
||||
const layerIdWithNameBeingEdited = layerWithNameBeingEdited?.entry.id;
|
||||
|
||||
|
@ -271,7 +333,7 @@
|
|||
layers = [];
|
||||
|
||||
// Build the new layer hierarchy
|
||||
const recurse = (folder: UpdateDocumentLayerStructureJs) => {
|
||||
const recurse = (folder: DocumentLayerStructure) => {
|
||||
folder.children.forEach((item, index) => {
|
||||
const mapping = layerCache.get(String(item.layerId));
|
||||
if (mapping) {
|
||||
|
@ -313,7 +375,7 @@
|
|||
<LayoutRow
|
||||
class="layer"
|
||||
classes={{
|
||||
selected: fakeHighlight !== undefined ? fakeHighlight === listing.entry.id : listing.entry.selected,
|
||||
selected: fakeHighlight !== undefined ? fakeHighlight === listing.entry.id : $nodeGraph.selected.includes(listing.entry.id),
|
||||
"insert-folder": (draggingData?.highlightFolder || false) && draggingData?.insertParentId === listing.entry.id,
|
||||
}}
|
||||
styles={{ "--layer-indent-levels": `${listing.entry.depth - 1}` }}
|
||||
|
@ -333,7 +395,9 @@
|
|||
{/if}
|
||||
{:else}
|
||||
<div class="thumbnail">
|
||||
{@html listing.entry.thumbnail}
|
||||
{#if $nodeGraph.thumbnails.has(listing.entry.id)}
|
||||
{@html $nodeGraph.thumbnails.get(listing.entry.id)}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<LayoutRow class="layer-name" on:dblclick={() => onEditLayerName(listing)}>
|
||||
|
@ -353,8 +417,8 @@
|
|||
class={"visibility"}
|
||||
action={(e) => (toggleLayerVisibility(listing.entry.id), e?.stopPropagation())}
|
||||
size={24}
|
||||
icon={(() => true)() ? "EyeVisible" : "EyeHidden"}
|
||||
tooltip={(() => true)() ? "Visible" : "Hidden"}
|
||||
icon={listing.entry.disabled ? "EyeHidden" : "EyeVisible"}
|
||||
tooltip={listing.entry.disabled ? "Disabled" : "Enabled"}
|
||||
/>
|
||||
</LayoutRow>
|
||||
{/each}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { getContext, onMount, tick } from "svelte";
|
||||
import { getContext, tick } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
|
||||
import { FADE_TRANSITION } from "@graphite/consts";
|
||||
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
import type { IconName } from "@graphite/utility-functions/icons";
|
||||
import type { Editor } from "@graphite/wasm-communication/editor";
|
||||
import { UpdateNodeGraphSelection } from "@graphite/wasm-communication/messages";
|
||||
import type { FrontendNodeLink, FrontendNodeType, FrontendNode, FrontendGraphInput, FrontendGraphOutput } from "@graphite/wasm-communication/messages";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
||||
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
|
@ -29,14 +29,18 @@
|
|||
let graph: HTMLDivElement | undefined;
|
||||
let nodesContainer: HTMLDivElement | undefined;
|
||||
let nodeSearchInput: TextInput | undefined;
|
||||
// TODO: MEMORY LEAK: Items never get removed from this array, so find a way to deal with garbage collection
|
||||
let layerNameLabelWidths: Record<string, number> = {};
|
||||
|
||||
let transform = { scale: 1, x: 1200, y: 0 };
|
||||
let panning = false;
|
||||
let selected: bigint[] = [];
|
||||
let draggingNodes: { startX: number; startY: number; roundX: number; roundY: number } | undefined = undefined;
|
||||
let selectIfNotDragged: undefined | bigint = undefined;
|
||||
let linkInProgressFromConnector: SVGSVGElement | undefined = undefined;
|
||||
let linkInProgressToConnector: SVGSVGElement | DOMRect | undefined = undefined;
|
||||
// TODO: Using this not-complete code, or another better approach, make it so the dragged in-progress connector correctly handles showing/hiding the SVG shape of the connector caps
|
||||
// let linkInProgressFromLayerTop: bigint | undefined = undefined;
|
||||
// let linkInProgressFromLayerBottom: bigint | undefined = undefined;
|
||||
let disconnecting: { nodeId: bigint; inputIndex: number; linkIndex: number } | undefined = undefined;
|
||||
let nodeLinkPaths: LinkPath[] = [];
|
||||
let searchTerm = "";
|
||||
|
@ -118,16 +122,16 @@
|
|||
const from = connectorToNodeIndex(linkInProgressFromConnector);
|
||||
const to = linkInProgressToConnector instanceof SVGSVGElement ? connectorToNodeIndex(linkInProgressToConnector) : undefined;
|
||||
|
||||
const linkStart = $nodeGraph.nodes.find((node) => node.id === from?.nodeId)?.isLayer || false;
|
||||
const linkEnd = ($nodeGraph.nodes.find((node) => node.id === to?.nodeId)?.isLayer && to?.index !== 0) || false;
|
||||
const linkStart = $nodeGraph.nodes.find((n) => n.id === from?.nodeId)?.isLayer || false;
|
||||
const linkEnd = ($nodeGraph.nodes.find((n) => n.id === to?.nodeId)?.isLayer && to?.index !== 0) || false;
|
||||
return createWirePath(linkInProgressFromConnector, linkInProgressToConnector, linkStart, linkEnd);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function createLinkPaths(linkPathInProgress: LinkPath | undefined, nodeLinkPaths: LinkPath[]): LinkPath[] {
|
||||
const optionalTuple = linkPathInProgress ? [linkPathInProgress] : [];
|
||||
return [...optionalTuple, ...nodeLinkPaths];
|
||||
const maybeLinkPathInProgress = linkPathInProgress ? [linkPathInProgress] : [];
|
||||
return [...maybeLinkPathInProgress, ...nodeLinkPaths];
|
||||
}
|
||||
|
||||
async function watchNodes(nodes: FrontendNode[]) {
|
||||
|
@ -136,7 +140,6 @@
|
|||
if (!outputs[index]) outputs[index] = [];
|
||||
});
|
||||
|
||||
selected = selected.filter((id) => nodes.find((node) => node.id === id));
|
||||
await refreshLinks();
|
||||
}
|
||||
|
||||
|
@ -144,8 +147,8 @@
|
|||
const outputIndex = Number(link.linkStartOutputIndex);
|
||||
const inputIndex = Number(link.linkEndInputIndex);
|
||||
|
||||
const nodeOutputConnectors = outputs[$nodeGraph.nodes.findIndex((node) => node.id === link.linkStart)];
|
||||
const nodeInputConnectors = inputs[$nodeGraph.nodes.findIndex((node) => node.id === link.linkEnd)] || undefined;
|
||||
const nodeOutputConnectors = outputs[$nodeGraph.nodes.findIndex((n) => n.id === link.linkStart)];
|
||||
const nodeInputConnectors = inputs[$nodeGraph.nodes.findIndex((n) => n.id === link.linkEnd)] || undefined;
|
||||
|
||||
const nodeOutput = nodeOutputConnectors?.[outputIndex] as SVGSVGElement | undefined;
|
||||
const nodeInput = nodeInputConnectors?.[inputIndex] as SVGSVGElement | undefined;
|
||||
|
@ -160,8 +163,9 @@
|
|||
const { nodeInput, nodeOutput } = resolveLink(link);
|
||||
if (!nodeInput || !nodeOutput) return [];
|
||||
if (disconnecting?.linkIndex === index) return [];
|
||||
const linkStart = $nodeGraph.nodes.find((node) => node.id === link.linkStart)?.isLayer || false;
|
||||
const linkEnd = ($nodeGraph.nodes.find((node) => node.id === link.linkEnd)?.isLayer && link.linkEndInputIndex !== 0n) || false;
|
||||
|
||||
const linkStart = $nodeGraph.nodes.find((n) => n.id === link.linkStart)?.isLayer || false;
|
||||
const linkEnd = ($nodeGraph.nodes.find((n) => n.id === link.linkEnd)?.isLayer && link.linkEndInputIndex !== 0n) || false;
|
||||
|
||||
return [createWirePath(nodeOutput, nodeInput.getBoundingClientRect(), linkStart, linkEnd)];
|
||||
});
|
||||
|
@ -177,21 +181,36 @@
|
|||
function buildWirePathLocations(outputBounds: DOMRect, inputBounds: DOMRect, verticalOut: boolean, verticalIn: boolean): { x: number; y: number }[] {
|
||||
if (!nodesContainer) return [];
|
||||
|
||||
const VERTICAL_LINK_OVERLAP_ON_SHAPED_CAP = 1;
|
||||
|
||||
const containerBounds = nodesContainer.getBoundingClientRect();
|
||||
|
||||
const outX = verticalOut ? outputBounds.x + outputBounds.width / 2 : outputBounds.x + outputBounds.width - 1;
|
||||
const outY = verticalOut ? outputBounds.y - 1 : outputBounds.y + outputBounds.height / 2;
|
||||
const outY = verticalOut ? outputBounds.y + VERTICAL_LINK_OVERLAP_ON_SHAPED_CAP : outputBounds.y + outputBounds.height / 2;
|
||||
const outConnectorX = (outX - containerBounds.x) / transform.scale;
|
||||
const outConnectorY = (outY - containerBounds.y) / transform.scale;
|
||||
|
||||
const inX = verticalIn ? inputBounds.x + inputBounds.width / 2 : inputBounds.x + 1;
|
||||
const inY = verticalIn ? inputBounds.y + inputBounds.height + 2 : inputBounds.y + inputBounds.height / 2;
|
||||
const inY = verticalIn ? inputBounds.y + inputBounds.height - VERTICAL_LINK_OVERLAP_ON_SHAPED_CAP : inputBounds.y + inputBounds.height / 2;
|
||||
const inConnectorX = (inX - containerBounds.x) / transform.scale;
|
||||
const inConnectorY = (inY - containerBounds.y) / transform.scale;
|
||||
const horizontalGap = Math.abs(outConnectorX - inConnectorX);
|
||||
const verticalGap = Math.abs(outConnectorY - inConnectorY);
|
||||
|
||||
const curveLength = 200;
|
||||
// TODO: Finish this commented out code replacement for the code below it based on this diagram: <https://files.keavon.com/-/InsubstantialElegantQueenant/capture.png>
|
||||
// // Straight: stacking lines which are always straight, or a straight horizontal link between two aligned nodes
|
||||
// if ((verticalOut && verticalIn) || (!verticalOut && !verticalIn && verticalGap === 0)) {
|
||||
// return [
|
||||
// { x: outConnectorX, y: outConnectorY },
|
||||
// { x: inConnectorX, y: inConnectorY },
|
||||
// ];
|
||||
// }
|
||||
|
||||
// // L-shape bend
|
||||
// if (verticalOut !== verticalIn) {
|
||||
// }
|
||||
|
||||
const curveLength = 24;
|
||||
const curveFalloffRate = curveLength * Math.PI * 2;
|
||||
|
||||
const horizontalCurveAmount = -(2 ** ((-10 * horizontalGap) / curveFalloffRate)) + 1;
|
||||
|
@ -210,7 +229,20 @@
|
|||
function buildWirePathString(outputBounds: DOMRect, inputBounds: DOMRect, verticalOut: boolean, verticalIn: boolean): string {
|
||||
const locations = buildWirePathLocations(outputBounds, inputBounds, verticalOut, verticalIn);
|
||||
if (locations.length === 0) return "[error]";
|
||||
return `M${locations[0].x},${locations[0].y} C${locations[1].x},${locations[1].y} ${locations[2].x},${locations[2].y} ${locations[3].x},${locations[3].y}`;
|
||||
const SMOOTHING = 0.5;
|
||||
const delta01 = { x: (locations[1].x - locations[0].x) * SMOOTHING, y: (locations[1].y - locations[0].y) * SMOOTHING };
|
||||
const delta23 = { x: (locations[3].x - locations[2].x) * SMOOTHING, y: (locations[3].y - locations[2].y) * SMOOTHING };
|
||||
return `
|
||||
M${locations[0].x},${locations[0].y}
|
||||
L${locations[1].x},${locations[1].y}
|
||||
C${locations[1].x + delta01.x},${locations[1].y + delta01.y}
|
||||
${locations[2].x - delta23.x},${locations[2].y - delta23.y}
|
||||
${locations[2].x},${locations[2].y}
|
||||
L${locations[3].x},${locations[3].y}
|
||||
`
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function createWirePath(outputPort: SVGSVGElement, inputPort: SVGSVGElement | DOMRect, verticalOut: boolean, verticalIn: boolean): LinkPath {
|
||||
|
@ -275,6 +307,8 @@
|
|||
nodeListLocation = undefined;
|
||||
document.removeEventListener("keydown", keydown);
|
||||
linkInProgressFromConnector = undefined;
|
||||
// linkInProgressFromLayerTop = undefined;
|
||||
// linkInProgressFromLayerBottom = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -298,7 +332,8 @@
|
|||
if (nodeError && lmb) return;
|
||||
const port = (e.target as SVGSVGElement).closest("[data-port]") as SVGSVGElement;
|
||||
const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined;
|
||||
const nodeId = node?.getAttribute("data-node") || undefined;
|
||||
const nodeIdString = node?.getAttribute("data-node") || undefined;
|
||||
const nodeId = nodeIdString ? BigInt(nodeIdString) : undefined;
|
||||
const nodeList = (e.target as HTMLElement).closest("[data-node-list]") as HTMLElement | undefined;
|
||||
|
||||
// Create the add node popup on right click, then exit
|
||||
|
@ -316,73 +351,97 @@
|
|||
if (lmb) {
|
||||
nodeListLocation = undefined;
|
||||
linkInProgressFromConnector = undefined;
|
||||
// linkInProgressFromLayerTop = undefined;
|
||||
// linkInProgressFromLayerBottom = undefined;
|
||||
}
|
||||
|
||||
// Alt-click sets the clicked node as previewed
|
||||
if (lmb && e.altKey && nodeId) {
|
||||
editor.instance.togglePreview(BigInt(nodeId));
|
||||
if (lmb && e.altKey && nodeId !== undefined) {
|
||||
editor.instance.togglePreview(nodeId);
|
||||
}
|
||||
|
||||
// Clicked on a port dot
|
||||
if (lmb && port && node) {
|
||||
const isOutput = Boolean(port.getAttribute("data-port") === "output");
|
||||
const frontendNode = (nodeId !== undefined && $nodeGraph.nodes.find((n) => n.id === nodeId)) || undefined;
|
||||
|
||||
if (isOutput) linkInProgressFromConnector = port;
|
||||
// Output: Begin dragging out a new link
|
||||
if (isOutput) {
|
||||
// Disallow creating additional vertical output links from an already-connected layer
|
||||
if (frontendNode?.isLayer && frontendNode.primaryOutput?.connected !== undefined) return;
|
||||
|
||||
linkInProgressFromConnector = port;
|
||||
// // Since we are just beginning to drag out a link from the top, we know the in-progress link exists from this layer's top and has no connection to any other layer bottom yet
|
||||
// linkInProgressFromLayerTop = nodeId !== undefined && frontendNode?.isLayer ? nodeId : undefined;
|
||||
// linkInProgressFromLayerBottom = undefined;
|
||||
}
|
||||
// Input: Begin moving an existing link
|
||||
else {
|
||||
const inputNodeInPorts = Array.from(node.querySelectorAll(`[data-port="input"]`));
|
||||
const inputNodeConnectionIndexSearch = inputNodeInPorts.indexOf(port);
|
||||
// const isLayerBottomConnector = frontendNode?.isLayer && inputNodeConnectionIndexSearch === 1;
|
||||
const inputIndex = inputNodeConnectionIndexSearch > -1 ? inputNodeConnectionIndexSearch : undefined;
|
||||
if (inputIndex === undefined || nodeId === undefined) return;
|
||||
|
||||
// Set the link to draw from the input that a previous link was on
|
||||
if (inputIndex !== undefined && nodeId !== undefined) {
|
||||
const nodeIdInt = BigInt(nodeId);
|
||||
const inputIndexInt = BigInt(inputIndex);
|
||||
const links = $nodeGraph.links;
|
||||
const linkIndex = links.findIndex((value) => value.linkEnd === nodeIdInt && value.linkEndInputIndex === inputIndexInt);
|
||||
if (linkIndex !== -1) {
|
||||
const nodeOutputConnectors = nodesContainer?.querySelectorAll(`[data-node="${String(links[linkIndex].linkStart)}"] [data-port="output"]`) || undefined;
|
||||
linkInProgressFromConnector = nodeOutputConnectors?.[Number(links[linkIndex].linkStartOutputIndex)] as SVGSVGElement | undefined;
|
||||
const nodeInputConnectors = nodesContainer?.querySelectorAll(`[data-node="${String(links[linkIndex].linkEnd)}"] [data-port="input"]`) || undefined;
|
||||
linkInProgressToConnector = nodeInputConnectors?.[Number(links[linkIndex].linkEndInputIndex)] as SVGSVGElement | undefined;
|
||||
disconnecting = { nodeId: nodeIdInt, inputIndex, linkIndex };
|
||||
refreshLinks();
|
||||
}
|
||||
}
|
||||
|
||||
const linkIndex = $nodeGraph.links.findIndex((value) => value.linkEnd === nodeId && value.linkEndInputIndex === BigInt(inputIndex));
|
||||
if (linkIndex === -1) return;
|
||||
|
||||
const nodeOutputConnectors = nodesContainer?.querySelectorAll(`[data-node="${String($nodeGraph.links[linkIndex].linkStart)}"] [data-port="output"]`) || undefined;
|
||||
linkInProgressFromConnector = nodeOutputConnectors?.[Number($nodeGraph.links[linkIndex].linkStartOutputIndex)] as SVGSVGElement | undefined;
|
||||
// linkInProgressFromLayerBottom = isLayerBottomConnector ? frontendNode.exposedInputs[0].connected : undefined;
|
||||
|
||||
const nodeInputConnectors = nodesContainer?.querySelectorAll(`[data-node="${String($nodeGraph.links[linkIndex].linkEnd)}"] [data-port="input"]`) || undefined;
|
||||
linkInProgressToConnector = nodeInputConnectors?.[Number($nodeGraph.links[linkIndex].linkEndInputIndex)] as SVGSVGElement | undefined;
|
||||
// linkInProgressFromLayerTop = undefined;
|
||||
|
||||
disconnecting = { nodeId: nodeId, inputIndex, linkIndex };
|
||||
refreshLinks();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Clicked on a node
|
||||
if (lmb && nodeId) {
|
||||
// Clicked on a node, so we select it
|
||||
if (lmb && nodeId !== undefined) {
|
||||
let updatedSelected = [...$nodeGraph.selected];
|
||||
let modifiedSelected = false;
|
||||
|
||||
const id = BigInt(nodeId);
|
||||
// Add to/remove from selection if holding Shift or Ctrl
|
||||
if (e.shiftKey || e.ctrlKey) {
|
||||
modifiedSelected = true;
|
||||
|
||||
if (selected.includes(id)) selected.splice(selected.lastIndexOf(id), 1);
|
||||
else selected.push(id);
|
||||
} else if (!selected.includes(id)) {
|
||||
// Remove from selection if already selected
|
||||
if (!updatedSelected.includes(nodeId)) updatedSelected.push(nodeId);
|
||||
// Add to selection if not already selected
|
||||
else updatedSelected.splice(updatedSelected.lastIndexOf(nodeId), 1);
|
||||
}
|
||||
// Replace selection with a non-selected node
|
||||
else if (!updatedSelected.includes(nodeId)) {
|
||||
modifiedSelected = true;
|
||||
|
||||
selected = [id];
|
||||
} else {
|
||||
selectIfNotDragged = id;
|
||||
updatedSelected = [nodeId];
|
||||
}
|
||||
// Replace selection (of multiple nodes including this one) with just this one, but only upon pointer up if the user didn't drag the selected nodes
|
||||
else {
|
||||
selectIfNotDragged = nodeId;
|
||||
}
|
||||
|
||||
if (selected.includes(id)) {
|
||||
// If this node is selected (whether from before or just now), prepare it for dragging
|
||||
if (updatedSelected.includes(nodeId)) {
|
||||
draggingNodes = { startX: e.x, startY: e.y, roundX: 0, roundY: 0 };
|
||||
}
|
||||
|
||||
if (modifiedSelected) editor.instance.selectNodes(selected.length > 0 ? new BigUint64Array(selected) : undefined);
|
||||
// Update the selection in the backend if it was modified
|
||||
if (modifiedSelected) editor.instance.selectNodes(new BigUint64Array(updatedSelected));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Clicked on the graph background
|
||||
if (lmb && selected.length !== 0) {
|
||||
selected = [];
|
||||
editor.instance.selectNodes(undefined);
|
||||
// Clicked on the graph background with something selected, so we deselect everything
|
||||
if (lmb && $nodeGraph.selected.length !== 0) {
|
||||
editor.instance.selectNodes(new BigUint64Array([]));
|
||||
}
|
||||
|
||||
// LMB clicked on the graph background or MMB clicked anywhere
|
||||
|
@ -392,9 +451,9 @@
|
|||
function doubleClick(_e: MouseEvent) {
|
||||
// const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined;
|
||||
// const nodeId = node?.getAttribute("data-node") || undefined;
|
||||
// if (nodeId) {
|
||||
// if (nodeId !== undefined) {
|
||||
// const id = BigInt(nodeId);
|
||||
// editor.instance.doubleClickNode(id);
|
||||
// editor.instance.enterNestedNetwork(id);
|
||||
// }
|
||||
}
|
||||
|
||||
|
@ -435,6 +494,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
function toggleLayerVisibility(id: bigint) {
|
||||
editor.instance.toggleLayerVisibility(id);
|
||||
}
|
||||
|
||||
function connectorToNodeIndex(svg: SVGSVGElement): { nodeId: bigint; index: number } | undefined {
|
||||
const node = svg.closest("[data-node]");
|
||||
|
||||
|
@ -454,8 +517,8 @@
|
|||
|
||||
// Check if this node should be inserted between two other nodes
|
||||
function checkInsertBetween() {
|
||||
if (selected.length !== 1) return;
|
||||
const selectedNodeId = selected[0];
|
||||
if ($nodeGraph.selected.length !== 1) return;
|
||||
const selectedNodeId = $nodeGraph.selected[0];
|
||||
const selectedNode = nodesContainer?.querySelector(`[data-node="${String(selectedNodeId)}"]`) || undefined;
|
||||
|
||||
// Check that neither the input or output of the selected node are already connected.
|
||||
|
@ -491,13 +554,14 @@
|
|||
|
||||
// If the node has been dragged on top of the link then connect it into the middle.
|
||||
if (link) {
|
||||
const isLayer = $nodeGraph.nodes.find((node) => node.id === selectedNodeId)?.isLayer;
|
||||
const isLayer = $nodeGraph.nodes.find((n) => n.id === selectedNodeId)?.isLayer;
|
||||
|
||||
editor.instance.connectNodesByLink(link.linkStart, 0, selectedNodeId, isLayer ? 1 : 0);
|
||||
editor.instance.connectNodesByLink(selectedNodeId, 0, link.linkEnd, Number(link.linkEndInputIndex));
|
||||
if (!isLayer) editor.instance.shiftNode(selectedNodeId);
|
||||
}
|
||||
}
|
||||
|
||||
function pointerUp(e: PointerEvent) {
|
||||
panning = false;
|
||||
|
||||
|
@ -536,13 +600,12 @@
|
|||
return;
|
||||
} else if (draggingNodes) {
|
||||
if (draggingNodes.startX === e.x || draggingNodes.startY === e.y) {
|
||||
if (selectIfNotDragged !== undefined && (selected.length !== 1 || selected[0] !== selectIfNotDragged)) {
|
||||
selected = [selectIfNotDragged];
|
||||
editor.instance.selectNodes(new BigUint64Array(selected));
|
||||
if (selectIfNotDragged !== undefined && ($nodeGraph.selected.length !== 1 || $nodeGraph.selected[0] !== selectIfNotDragged)) {
|
||||
editor.instance.selectNodes(new BigUint64Array([selectIfNotDragged]));
|
||||
}
|
||||
}
|
||||
|
||||
if (selected.length > 0 && (draggingNodes.roundX !== 0 || draggingNodes.roundY !== 0)) editor.instance.moveSelectedNodes(draggingNodes.roundX, draggingNodes.roundY);
|
||||
if ($nodeGraph.selected.length > 0 && (draggingNodes.roundX !== 0 || draggingNodes.roundY !== 0)) editor.instance.moveSelectedNodes(draggingNodes.roundX, draggingNodes.roundY);
|
||||
|
||||
checkInsertBetween();
|
||||
|
||||
|
@ -592,15 +655,18 @@
|
|||
|
||||
function layerBorderMask(nodeWidth: number): string {
|
||||
const NODE_HEIGHT = 2 * 24;
|
||||
const THUMBNAIL_WIDTH = 96;
|
||||
const FUDGE = 2;
|
||||
const THUMBNAIL_WIDTH = 72 + 8 * 2;
|
||||
const FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT = 2;
|
||||
|
||||
const boxes: { x: number; y: number; width: number; height: number }[] = [];
|
||||
// Left input
|
||||
boxes.push({ x: -8, y: 16, width: 16, height: 16 });
|
||||
|
||||
// Thumbnail
|
||||
boxes.push({ x: 24, y: -FUDGE, width: THUMBNAIL_WIDTH, height: NODE_HEIGHT + FUDGE * 2 });
|
||||
boxes.push({ x: 28, y: -FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT, width: THUMBNAIL_WIDTH, height: NODE_HEIGHT + FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT * 2 });
|
||||
|
||||
// Right visibility button
|
||||
boxes.push({ x: nodeWidth - 12, y: (NODE_HEIGHT - 24) / 2, width: 24, height: 24 });
|
||||
|
||||
return borderMask(boxes, nodeWidth, NODE_HEIGHT);
|
||||
}
|
||||
|
@ -614,12 +680,6 @@
|
|||
const dataTypeCapitalized = `${value.dataType[0].toUpperCase()}${value.dataType.slice(1)}`;
|
||||
return value.resolvedType ? `Resolved Data: ${value.resolvedType}` : `Unresolved Data: ${dataTypeCapitalized}`;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphSelection, (updateNodeGraphSelection) => {
|
||||
selected = updateNodeGraphSelection.selected;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -679,16 +739,19 @@
|
|||
{#each $nodeGraph.nodes.flatMap((node, nodeIndex) => (node.isLayer ? [{ node, nodeIndex }] : [])) as { node, nodeIndex } (nodeIndex)}
|
||||
{@const clipPathId = String(Math.random()).substring(2)}
|
||||
{@const stackDataInput = node.exposedInputs[0]}
|
||||
{@const extraWidthToReachGridMultiple = 8}
|
||||
{@const labelWidthGridCells = Math.ceil(((layerNameLabelWidths?.[String(node.id)] || 0) - extraWidthToReachGridMultiple) / 24)}
|
||||
<div
|
||||
class="layer"
|
||||
class:selected={selected.includes(node.id)}
|
||||
class:selected={$nodeGraph.selected.includes(node.id)}
|
||||
class:previewed={node.previewed}
|
||||
class:disabled={node.disabled}
|
||||
style:--offset-left={(node.position?.x || 0) + (selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0)}
|
||||
style:--offset-top={(node.position?.y || 0) + (selected.includes(node.id) ? draggingNodes?.roundY || 0 : 0)}
|
||||
style:--offset-left={(node.position?.x || 0) + ($nodeGraph.selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0)}
|
||||
style:--offset-top={(node.position?.y || 0) + ($nodeGraph.selected.includes(node.id) ? draggingNodes?.roundY || 0 : 0)}
|
||||
style:--clip-path-id={`url(#${clipPathId})`}
|
||||
style:--data-color={`var(--color-data-${node.primaryOutput?.dataType || "general"})`}
|
||||
style:--data-color-dim={`var(--color-data-${node.primaryOutput?.dataType || "general"}-dim)`}
|
||||
style:--label-width={labelWidthGridCells}
|
||||
data-node={node.id}
|
||||
>
|
||||
{#if node.errors}
|
||||
|
@ -709,19 +772,24 @@
|
|||
bind:this={inputs[nodeIndex][0]}
|
||||
>
|
||||
{#if node.primaryInput}
|
||||
<title>{dataTypeTooltip(node.primaryInput)}</title>
|
||||
<title>{`${dataTypeTooltip(node.primaryInput)}\nConnected to ${node.primaryInput?.connected || "nothing"}`}</title>
|
||||
{/if}
|
||||
{#if node.primaryInput?.connected}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="thumbnail">
|
||||
{#if $nodeGraph.thumbnails.has(node.id)}
|
||||
{@html $nodeGraph.thumbnails.get(node.id)}
|
||||
{/if}
|
||||
<!-- Layer stacking top output -->
|
||||
{#if node.primaryOutput}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
viewBox="0 0 8 12"
|
||||
class="port top"
|
||||
data-port="output"
|
||||
data-datatype={node.primaryOutput.dataType}
|
||||
|
@ -729,13 +797,21 @@
|
|||
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`}
|
||||
bind:this={outputs[nodeIndex][0]}
|
||||
>
|
||||
<title>{dataTypeTooltip(node.primaryOutput)}</title>
|
||||
<path d="M0,2.953,2.521,1.259a2.649,2.649,0,0,1,2.959,0L8,2.953V8H0Z" />
|
||||
<title>{`${dataTypeTooltip(node.primaryOutput)}\nConnected to ${node.primaryOutput.connected || "nothing"}`}</title>
|
||||
{#if node.primaryOutput.connected}
|
||||
<path d="M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z" fill="var(--data-color)" />
|
||||
{#if $nodeGraph.nodes.find((n) => n.id === node.primaryOutput?.connected)?.isLayer}
|
||||
<path d="M0,-3.5h8v8l-2.521,-1.681a2.666,2.666,0,0,0,-2.959,0l-2.52,1.681z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
{:else}
|
||||
<path d="M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
{/if}
|
||||
<!-- Layer stacking bottom input -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
viewBox="0 0 8 12"
|
||||
class="port bottom"
|
||||
data-port="input"
|
||||
data-datatype={stackDataInput.dataType}
|
||||
|
@ -743,19 +819,36 @@
|
|||
style:--data-color-dim={`var(--color-data-${stackDataInput.dataType}-dim)`}
|
||||
bind:this={inputs[nodeIndex][1]}
|
||||
>
|
||||
<title>{dataTypeTooltip(stackDataInput)}</title>
|
||||
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" />
|
||||
<title>{`${dataTypeTooltip(stackDataInput)}\nConnected to ${stackDataInput.connected || "nothing"}`}</title>
|
||||
{#if stackDataInput.connected}
|
||||
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color)" />
|
||||
{#if $nodeGraph.nodes.find((n) => n.id === stackDataInput.connected)?.isLayer}
|
||||
<path d="M0,10.95l2.52,-1.69c0.89,-0.6,2.06,-0.6,2.96,0l2.52,1.69v5.05h-8v-5.05z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
{:else}
|
||||
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
</div>
|
||||
<div class="details">
|
||||
<!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
|
||||
<TextLabel tooltip={editor.instance.inDevelopmentMode() ? `Node ID: ${node.id}` : undefined}>{node.alias || "Layer"}</TextLabel>
|
||||
<span title={editor.instance.inDevelopmentMode() ? `Node ID: ${node.id}` : undefined} bind:offsetWidth={layerNameLabelWidths[String(node.id)]}>
|
||||
{node.alias || "Layer"}
|
||||
</span>
|
||||
</div>
|
||||
<IconButton
|
||||
class={"visibility"}
|
||||
action={(e) => (toggleLayerVisibility(node.id), e?.stopPropagation())}
|
||||
size={24}
|
||||
icon={node.disabled ? "EyeHidden" : "EyeVisible"}
|
||||
tooltip={node.disabled ? "Disabled" : "Enabled"}
|
||||
/>
|
||||
|
||||
<svg class="border-mask" width="0" height="0">
|
||||
<defs>
|
||||
<clipPath id={clipPathId}>
|
||||
<path clip-rule="evenodd" d={layerBorderMask(216)} />
|
||||
<!-- Keep this equation in sync with the equivalent one in the CSS rule for `.layer { width: ... }` below -->
|
||||
<path clip-rule="evenodd" d={layerBorderMask(36 + 72 + 8 + 24 * Math.max(3, labelWidthGridCells) + 8 + 12 + extraWidthToReachGridMultiple)} />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
@ -767,11 +860,11 @@
|
|||
{@const clipPathId = String(Math.random()).substring(2)}
|
||||
<div
|
||||
class="node"
|
||||
class:selected={selected.includes(node.id)}
|
||||
class:selected={$nodeGraph.selected.includes(node.id)}
|
||||
class:previewed={node.previewed}
|
||||
class:disabled={node.disabled}
|
||||
style:--offset-left={(node.position?.x || 0) + (selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0)}
|
||||
style:--offset-top={(node.position?.y || 0) + (selected.includes(node.id) ? draggingNodes?.roundY || 0 : 0)}
|
||||
style:--offset-left={(node.position?.x || 0) + ($nodeGraph.selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0)}
|
||||
style:--offset-top={(node.position?.y || 0) + ($nodeGraph.selected.includes(node.id) ? draggingNodes?.roundY || 0 : 0)}
|
||||
style:--clip-path-id={`url(#${clipPathId})`}
|
||||
style:--data-color={`var(--color-data-${node.primaryOutput?.dataType || "general"})`}
|
||||
style:--data-color-dim={`var(--color-data-${node.primaryOutput?.dataType || "general"}-dim)`}
|
||||
|
@ -810,8 +903,12 @@
|
|||
style:--data-color-dim={`var(--color-data-${node.primaryInput?.dataType}-dim)`}
|
||||
bind:this={inputs[nodeIndex][0]}
|
||||
>
|
||||
<title>{dataTypeTooltip(node.primaryInput)}</title>
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" />
|
||||
<title>{`${dataTypeTooltip(node.primaryInput)}\nConnected to ${node.primaryInput.connected || "nothing"}`}</title>
|
||||
{#if node.primaryInput.connected}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
{/if}
|
||||
{#each node.exposedInputs as parameter, index}
|
||||
|
@ -826,8 +923,12 @@
|
|||
style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`}
|
||||
bind:this={inputs[nodeIndex][index + 1]}
|
||||
>
|
||||
<title>{dataTypeTooltip(parameter)}</title>
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" />
|
||||
<title>{`${dataTypeTooltip(parameter)}\nConnected to ${parameter.connected || "nothing"}`}</title>
|
||||
{#if parameter.connected}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
{/if}
|
||||
{/each}
|
||||
|
@ -845,8 +946,12 @@
|
|||
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`}
|
||||
bind:this={outputs[nodeIndex][0]}
|
||||
>
|
||||
<title>{dataTypeTooltip(node.primaryOutput)}</title>
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" />
|
||||
<title>{`${dataTypeTooltip(node.primaryOutput)}\nConnected to ${node.primaryOutput.connected || "nothing"}`}</title>
|
||||
{#if node.primaryOutput.connected}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
{/if}
|
||||
{#each node.exposedOutputs as parameter, outputIndex}
|
||||
|
@ -860,8 +965,12 @@
|
|||
style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`}
|
||||
bind:this={outputs[nodeIndex][outputIndex + (node.primaryOutput ? 1 : 0)]}
|
||||
>
|
||||
<title>{dataTypeTooltip(parameter)}</title>
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" />
|
||||
<title>{`${dataTypeTooltip(parameter)}\nConnected to ${parameter.connected || "nothing"}`}</title>
|
||||
{#if parameter.connected}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -1097,7 +1206,6 @@
|
|||
}
|
||||
|
||||
.port {
|
||||
fill: var(--data-color);
|
||||
// Double the intended value because of margin collapsing, but for the first and last we divide it by two as intended
|
||||
margin: calc(24px - 8px) 0;
|
||||
width: 8px;
|
||||
|
@ -1112,7 +1220,10 @@
|
|||
|
||||
.layer {
|
||||
border-radius: 8px;
|
||||
width: 216px;
|
||||
--half-visibility-button: 12px;
|
||||
--extra-width-to-reach-grid-multiple: 8px;
|
||||
// Keep this equation in sync with the equivalent one in the Svelte template `<clipPath><path d="layerBorderMask(...)" /></clipPath>` above
|
||||
width: calc(36px + 72px + 8px + 24px * Max(3, var(--label-width)) + 8px + var(--half-visibility-button) + var(--extra-width-to-reach-grid-multiple));
|
||||
|
||||
&::after {
|
||||
border: 1px solid var(--color-5-dullgray);
|
||||
|
@ -1160,25 +1271,33 @@
|
|||
margin: 0 auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 12px;
|
||||
|
||||
&.top {
|
||||
top: -9px;
|
||||
top: -13px;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
bottom: -9px;
|
||||
bottom: -13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-left: 12px;
|
||||
margin: 0 8px;
|
||||
|
||||
.text-label {
|
||||
span {
|
||||
white-space: nowrap;
|
||||
line-height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.visibility {
|
||||
position: absolute;
|
||||
right: calc(-1 * var(--half-visibility-button));
|
||||
}
|
||||
|
||||
.visibility,
|
||||
.input.ports,
|
||||
.input.ports .port {
|
||||
position: absolute;
|
||||
|
@ -1186,6 +1305,10 @@
|
|||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.input.ports .port {
|
||||
left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.node {
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/wasm-communication/editor";
|
||||
import { type FrontendNode, type FrontendNodeLink, type FrontendNodeType, UpdateNodeGraph, UpdateNodeTypes, UpdateNodeThumbnail, UpdateZoomWithScroll } from "@graphite/wasm-communication/messages";
|
||||
import {
|
||||
type FrontendNode,
|
||||
type FrontendNodeLink,
|
||||
type FrontendNodeType,
|
||||
UpdateNodeGraph,
|
||||
UpdateNodeTypes,
|
||||
UpdateNodeThumbnail,
|
||||
UpdateZoomWithScroll,
|
||||
UpdateNodeGraphSelection,
|
||||
} from "@graphite/wasm-communication/messages";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function createNodeGraphState(editor: Editor) {
|
||||
|
@ -11,6 +20,7 @@ export function createNodeGraphState(editor: Editor) {
|
|||
nodeTypes: [] as FrontendNodeType[],
|
||||
zoomWithScroll: false as boolean,
|
||||
thumbnails: new Map<bigint, string>(),
|
||||
selected: [] as bigint[],
|
||||
});
|
||||
|
||||
// Set up message subscriptions on creation
|
||||
|
@ -19,6 +29,7 @@ export function createNodeGraphState(editor: Editor) {
|
|||
state.nodes = updateNodeGraph.nodes;
|
||||
state.links = updateNodeGraph.links;
|
||||
const newThumbnails = new Map<bigint, string>();
|
||||
// Transfer over any preexisting thumbnails from itself
|
||||
state.nodes.forEach((node) => {
|
||||
const thumbnail = state.thumbnails.get(node.id);
|
||||
if (thumbnail) newThumbnails.set(node.id, thumbnail);
|
||||
|
@ -45,6 +56,12 @@ export function createNodeGraphState(editor: Editor) {
|
|||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphSelection, (updateNodeGraphSelection) => {
|
||||
update((state) => {
|
||||
state.selected = updateNodeGraphSelection.selected;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
|
|
@ -17,7 +17,6 @@ import {
|
|||
TriggerOpenDocument,
|
||||
TriggerRevokeBlobUrl,
|
||||
UpdateActiveDocument,
|
||||
UpdateImageData,
|
||||
UpdateOpenDocumentsList,
|
||||
} from "@graphite/wasm-communication/messages";
|
||||
|
||||
|
@ -100,21 +99,6 @@ export function createPortfolioState(editor: Editor) {
|
|||
// Fail silently if there's an error rasterizing the SVG, such as a zero-sized image
|
||||
}
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateImageData, (updateImageData) => {
|
||||
updateImageData.imageData.forEach(async (element) => {
|
||||
const buffer = new Uint8Array(element.imageData.values()).buffer;
|
||||
const blob = new Blob([buffer], { type: element.mime });
|
||||
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
|
||||
// Pre-decode the image so it is ready to be drawn instantly once it's placed into the viewport SVG
|
||||
const image = new Image();
|
||||
image.src = blobURL;
|
||||
await image.decode();
|
||||
|
||||
// editor.instance.setImageBlobURL(updateImageData.documentId, element.path, element.nodeId, blobURL, image.naturalWidth, image.naturalHeight, element.transform);
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerRevokeBlobUrl, async (triggerRevokeBlobUrl) => {
|
||||
URL.revokeObjectURL(triggerRevokeBlobUrl.url);
|
||||
});
|
||||
|
|
|
@ -90,6 +90,8 @@ export class FrontendGraphInput {
|
|||
readonly name!: string;
|
||||
|
||||
readonly resolvedType!: string | undefined;
|
||||
|
||||
readonly connected!: bigint | undefined;
|
||||
}
|
||||
|
||||
export class FrontendGraphOutput {
|
||||
|
@ -98,6 +100,8 @@ export class FrontendGraphOutput {
|
|||
readonly name!: string;
|
||||
|
||||
readonly resolvedType!: string | undefined;
|
||||
|
||||
readonly connected!: bigint | undefined;
|
||||
}
|
||||
|
||||
export class FrontendNode {
|
||||
|
@ -566,72 +570,13 @@ export class TriggerSavePreferences extends JsMessage {
|
|||
|
||||
export class DocumentChanged extends JsMessage {}
|
||||
|
||||
export class UpdateDocumentLayerStructureJs extends JsMessage {
|
||||
constructor(
|
||||
readonly layerId: bigint,
|
||||
readonly children: UpdateDocumentLayerStructureJs[],
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
type DataBuffer = {
|
||||
export type DataBuffer = {
|
||||
pointer: bigint;
|
||||
length: bigint;
|
||||
};
|
||||
|
||||
export function newUpdateDocumentLayerStructure(input: { dataBuffer: DataBuffer }, wasm: WasmRawInstance): UpdateDocumentLayerStructureJs {
|
||||
const pointerNum = Number(input.dataBuffer.pointer);
|
||||
const lengthNum = Number(input.dataBuffer.length);
|
||||
|
||||
const wasmMemoryBuffer = wasm.buffer;
|
||||
|
||||
// Decode the folder structure encoding
|
||||
const encoding = new DataView(wasmMemoryBuffer, pointerNum, lengthNum);
|
||||
|
||||
// The structure section indicates how to read through the upcoming layer list and assign depths to each layer
|
||||
const structureSectionLength = Number(encoding.getBigUint64(0, true));
|
||||
const structureSectionMsbSigned = new DataView(wasmMemoryBuffer, pointerNum + 8, structureSectionLength * 8);
|
||||
|
||||
// The layer IDs section lists each layer ID sequentially in the tree, as it will show up in the panel
|
||||
const layerIdsSection = new DataView(wasmMemoryBuffer, pointerNum + 8 + structureSectionLength * 8);
|
||||
|
||||
let layersEncountered = 0;
|
||||
let currentFolder = new UpdateDocumentLayerStructureJs(BigInt(-1), []);
|
||||
const currentFolderStack = [currentFolder];
|
||||
|
||||
for (let i = 0; i < structureSectionLength; i += 1) {
|
||||
const msbSigned = structureSectionMsbSigned.getBigUint64(i * 8, true);
|
||||
const msbMask = BigInt(1) << BigInt(64 - 1);
|
||||
|
||||
// Set the MSB to 0 to clear the sign and then read the number as usual
|
||||
const numberOfLayersAtThisDepth = msbSigned & ~msbMask;
|
||||
|
||||
// Store child folders in the current folder (until we are interrupted by an indent)
|
||||
for (let j = 0; j < numberOfLayersAtThisDepth; j += 1) {
|
||||
const layerId = layerIdsSection.getBigUint64(layersEncountered * 8, true);
|
||||
layersEncountered += 1;
|
||||
|
||||
const childLayer = new UpdateDocumentLayerStructureJs(layerId, []);
|
||||
currentFolder.children.push(childLayer);
|
||||
}
|
||||
|
||||
// Check the sign of the MSB, where a 1 is a negative (outward) indent
|
||||
const subsequentDirectionOfDepthChange = (msbSigned & msbMask) === BigInt(0);
|
||||
// Inward
|
||||
if (subsequentDirectionOfDepthChange) {
|
||||
currentFolderStack.push(currentFolder);
|
||||
currentFolder = currentFolder.children[currentFolder.children.length - 1];
|
||||
}
|
||||
// Outward
|
||||
else {
|
||||
const popped = currentFolderStack.pop();
|
||||
if (!popped) throw Error("Too many negative indents in the folder structure");
|
||||
if (popped) currentFolder = popped;
|
||||
}
|
||||
}
|
||||
|
||||
return currentFolder;
|
||||
export class UpdateDocumentLayerStructureJs extends JsMessage {
|
||||
readonly dataBuffer!: DataBuffer;
|
||||
}
|
||||
|
||||
export class DisplayEditableTextbox extends JsMessage {
|
||||
|
@ -653,13 +598,6 @@ export class DisplayEditableTextboxTransform extends JsMessage {
|
|||
readonly transform!: number[];
|
||||
}
|
||||
|
||||
export class UpdateImageData extends JsMessage {
|
||||
readonly documentId!: bigint;
|
||||
|
||||
@Type(() => FrontendImageData)
|
||||
readonly imageData!: FrontendImageData[];
|
||||
}
|
||||
|
||||
export class DisplayRemoveEditableTextbox extends JsMessage {}
|
||||
|
||||
export class UpdateDocumentLayerDetails extends JsMessage {
|
||||
|
@ -675,28 +613,20 @@ export class LayerPanelEntry {
|
|||
|
||||
layerClassification!: LayerClassification;
|
||||
|
||||
expanded!: boolean;
|
||||
|
||||
disabled!: boolean;
|
||||
|
||||
parentId!: bigint | undefined;
|
||||
|
||||
id!: bigint;
|
||||
|
||||
@Transform(({ value }: { value: bigint }) => Number(value))
|
||||
depth!: number;
|
||||
|
||||
expanded!: boolean;
|
||||
|
||||
selected!: boolean;
|
||||
|
||||
thumbnail!: string;
|
||||
}
|
||||
|
||||
export type LayerClassification = "Folder" | "Artboard" | "Layer";
|
||||
|
||||
export class FrontendImageData {
|
||||
readonly mime!: string;
|
||||
|
||||
readonly imageData!: Uint8Array;
|
||||
}
|
||||
|
||||
export class DisplayDialogDismiss extends JsMessage {}
|
||||
|
||||
export class Font {
|
||||
|
@ -1373,12 +1303,11 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||
UpdateDocumentArtwork,
|
||||
UpdateDocumentBarLayout,
|
||||
UpdateDocumentLayerDetails,
|
||||
UpdateDocumentLayerStructureJs: newUpdateDocumentLayerStructure,
|
||||
UpdateDocumentLayerStructureJs,
|
||||
UpdateDocumentModeLayout,
|
||||
UpdateDocumentRulers,
|
||||
UpdateDocumentScrollbars,
|
||||
UpdateEyedropperSamplingState,
|
||||
UpdateImageData,
|
||||
UpdateInputHints,
|
||||
UpdateLayersPanelOptionsLayout,
|
||||
UpdateMenuBarLayout,
|
||||
|
|
|
@ -159,27 +159,6 @@ impl JsEditorHandle {
|
|||
|
||||
// Sends a FrontendMessage to JavaScript
|
||||
fn send_frontend_message_to_js(&self, mut message: FrontendMessage) {
|
||||
// Special case for update image data to avoid serialization times.
|
||||
if let FrontendMessage::UpdateImageData { document_id: _, image_data: _ } = message {
|
||||
// for image in image_data {
|
||||
// #[cfg(not(feature = "tauri"))]
|
||||
// {
|
||||
// let transform = if let Some(transform_val) = image.transform {
|
||||
// let transform = js_sys::Float64Array::new_with_length(6);
|
||||
// transform.copy_from(&transform_val);
|
||||
// transform
|
||||
// } else {
|
||||
// js_sys::Float64Array::default()
|
||||
// };
|
||||
// }
|
||||
// #[cfg(feature = "tauri")]
|
||||
// {
|
||||
// let identifier = format!("http://localhost:3001/image/{:?}_{}", image.path, document_id);
|
||||
// fetchImage(image.path.clone(), image.node_id, image.mime, document_id, identifier);
|
||||
// }
|
||||
// }
|
||||
return;
|
||||
}
|
||||
if let FrontendMessage::UpdateDocumentLayerStructure { data_buffer } = message {
|
||||
message = FrontendMessage::UpdateDocumentLayerStructureJs { data_buffer: data_buffer.into() };
|
||||
}
|
||||
|
@ -633,9 +612,8 @@ impl JsEditorHandle {
|
|||
|
||||
/// Notifies the backend that the user selected a node in the node graph
|
||||
#[wasm_bindgen(js_name = selectNodes)]
|
||||
pub fn select_nodes(&self, nodes: Option<Vec<u64>>) {
|
||||
let nodes = nodes.map(|nodes| nodes.into_iter().map(|id| NodeId(id)).collect::<Vec<_>>());
|
||||
let nodes = nodes.unwrap_or_default();
|
||||
pub fn select_nodes(&self, nodes: Vec<u64>) {
|
||||
let nodes = nodes.into_iter().map(|id| NodeId(id)).collect::<Vec<_>>();
|
||||
let message = NodeGraphMessage::SelectedNodesSet { nodes };
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
@ -648,10 +626,10 @@ impl JsEditorHandle {
|
|||
}
|
||||
|
||||
/// Notifies the backend that the user double clicked a node
|
||||
#[wasm_bindgen(js_name = doubleClickNode)]
|
||||
pub fn double_click_node(&self, node: u64) {
|
||||
#[wasm_bindgen(js_name = enterNestedNetwork)]
|
||||
pub fn enter_nested_network(&self, node: u64) {
|
||||
let node = NodeId(node);
|
||||
let message = NodeGraphMessage::DoubleClickNode { node };
|
||||
let message = NodeGraphMessage::EnterNestedNetwork { node };
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
|
|
|
@ -158,6 +158,7 @@ pub struct RenderConfig {
|
|||
}
|
||||
|
||||
pub struct EditorApi<'a, Io> {
|
||||
// TODO: Is `image_frame` still used? I think it's only ever set to None.
|
||||
pub image_frame: Option<ImageFrame<Color>>,
|
||||
pub font_cache: &'a FontCache,
|
||||
pub application_io: &'a Io,
|
||||
|
|
|
@ -60,7 +60,7 @@ impl ClickTarget {
|
|||
|
||||
/// Mutable state used whilst rendering to an SVG
|
||||
pub struct SvgRender {
|
||||
pub svg: SvgSegmentList,
|
||||
pub svg: Vec<SvgSegment>,
|
||||
pub svg_defs: String,
|
||||
pub transform: DAffine2,
|
||||
pub image_data: Vec<(u64, Image<Color>)>,
|
||||
|
@ -70,7 +70,7 @@ pub struct SvgRender {
|
|||
impl SvgRender {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
svg: SvgSegmentList::default(),
|
||||
svg: Vec::default(),
|
||||
svg_defs: String::new(),
|
||||
transform: DAffine2::IDENTITY,
|
||||
image_data: Vec::new(),
|
||||
|
@ -79,8 +79,8 @@ impl SvgRender {
|
|||
}
|
||||
|
||||
pub fn indent(&mut self) {
|
||||
self.svg.push("\n");
|
||||
self.svg.push("\t".repeat(self.indent));
|
||||
self.svg.push("\n".into());
|
||||
self.svg.push("\t".repeat(self.indent).into());
|
||||
}
|
||||
|
||||
/// Add an outer `<svg>...</svg>` tag with a `viewBox` and the `<defs />`
|
||||
|
@ -90,7 +90,7 @@ impl SvgRender {
|
|||
let defs = &self.svg_defs;
|
||||
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{x} {y} {size_x} {size_y}"><defs>{defs}</defs>"#,);
|
||||
self.svg.insert(0, svg_header.into());
|
||||
self.svg.push("</svg>");
|
||||
self.svg.push("</svg>".into());
|
||||
}
|
||||
|
||||
/// Wraps the SVG with `<svg><g transform="...">...</g></svg>`, which allows for rotation
|
||||
|
@ -106,43 +106,43 @@ impl SvgRender {
|
|||
format_transform_matrix(transform)
|
||||
);
|
||||
self.svg.insert(0, svg_header.into());
|
||||
self.svg.push("</g></svg>");
|
||||
self.svg.push("</g></svg>".into());
|
||||
}
|
||||
|
||||
pub fn leaf_tag(&mut self, name: impl Into<SvgSegment>, attributes: impl FnOnce(&mut SvgRenderAttrs)) {
|
||||
self.indent();
|
||||
self.svg.push("<");
|
||||
self.svg.push(name);
|
||||
self.svg.push("<".into());
|
||||
self.svg.push(name.into());
|
||||
attributes(&mut SvgRenderAttrs(self));
|
||||
|
||||
self.svg.push("/>");
|
||||
self.svg.push("/>".into());
|
||||
}
|
||||
|
||||
pub fn leaf_node(&mut self, content: impl Into<SvgSegment>) {
|
||||
self.indent();
|
||||
self.svg.push(content);
|
||||
self.svg.push(content.into());
|
||||
}
|
||||
|
||||
pub fn parent_tag(&mut self, name: impl Into<SvgSegment>, attributes: impl FnOnce(&mut SvgRenderAttrs), inner: impl FnOnce(&mut Self)) {
|
||||
let name = name.into();
|
||||
self.indent();
|
||||
self.svg.push("<");
|
||||
self.svg.push("<".into());
|
||||
self.svg.push(name.clone());
|
||||
// Wraps `self` in a newtype (1-tuple) which is then mutated by the `attributes` closure
|
||||
attributes(&mut SvgRenderAttrs(self));
|
||||
self.svg.push(">");
|
||||
self.svg.push(">".into());
|
||||
let length = self.svg.len();
|
||||
self.indent += 1;
|
||||
inner(self);
|
||||
self.indent -= 1;
|
||||
if self.svg.len() != length {
|
||||
self.indent();
|
||||
self.svg.push("</");
|
||||
self.svg.push("</".into());
|
||||
self.svg.push(name);
|
||||
self.svg.push(">");
|
||||
self.svg.push(">".into());
|
||||
} else {
|
||||
self.svg.pop();
|
||||
self.svg.push("/>");
|
||||
self.svg.push("/>".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -154,8 +154,6 @@ impl Default for SvgRender {
|
|||
}
|
||||
|
||||
pub enum ImageRenderMode {
|
||||
BlobUrl,
|
||||
Canvas,
|
||||
Base64,
|
||||
}
|
||||
|
||||
|
@ -209,10 +207,10 @@ pub trait GraphicElementRendered {
|
|||
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>);
|
||||
fn to_usvg_node(&self) -> usvg::Node {
|
||||
let mut render = SvgRender::new();
|
||||
let render_params = RenderParams::new(crate::vector::style::ViewMode::Normal, ImageRenderMode::BlobUrl, None, false, false, false);
|
||||
let render_params = RenderParams::new(crate::vector::style::ViewMode::Normal, ImageRenderMode::Base64, None, false, false, false);
|
||||
self.render_svg(&mut render, &render_params);
|
||||
render.format_svg(DVec2::ZERO, DVec2::ONE);
|
||||
let svg = render.svg.to_string();
|
||||
let svg = render.svg.to_svg_string();
|
||||
|
||||
let opt = usvg::Options::default();
|
||||
|
||||
|
@ -384,7 +382,7 @@ impl GraphicElementRendered for Artboard {
|
|||
},
|
||||
|render| {
|
||||
// TODO: Use the artboard's layer name
|
||||
render.svg.push("Artboard");
|
||||
render.svg.push("Artboard".into());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -448,22 +446,8 @@ impl GraphicElementRendered for Artboard {
|
|||
impl GraphicElementRendered for ImageFrame<Color> {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
let transform: String = format_transform_matrix(self.transform * render.transform);
|
||||
let uuid = generate_uuid();
|
||||
|
||||
match render_params.image_render_mode {
|
||||
ImageRenderMode::BlobUrl => {
|
||||
render.leaf_tag("image", move |attributes| {
|
||||
attributes.push("width", 1.to_string());
|
||||
attributes.push("height", 1.to_string());
|
||||
attributes.push("preserveAspectRatio", "none");
|
||||
attributes.push("transform", transform);
|
||||
attributes.push("href", SvgSegment::BlobUrl(uuid));
|
||||
if self.alpha_blending.blend_mode != BlendMode::default() {
|
||||
attributes.push("style", self.alpha_blending.blend_mode.render());
|
||||
}
|
||||
});
|
||||
render.image_data.push((uuid, self.image.clone()))
|
||||
}
|
||||
ImageRenderMode::Base64 => {
|
||||
let image = &self.image;
|
||||
if image.data.is_empty() {
|
||||
|
@ -486,9 +470,6 @@ impl GraphicElementRendered for ImageFrame<Color> {
|
|||
}
|
||||
});
|
||||
}
|
||||
ImageRenderMode::Canvas => {
|
||||
todo!("Canvas rendering is not yet implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -683,34 +664,21 @@ impl From<&'static str> for SvgSegment {
|
|||
}
|
||||
}
|
||||
|
||||
/// A list of [`SvgSegment`]s.
|
||||
///
|
||||
/// Can be modified with `list.push("hello".into())`. Use `list.to_string()` to convert the segments into one string.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct SvgSegmentList(Vec<SvgSegment>);
|
||||
|
||||
impl core::ops::Deref for SvgSegmentList {
|
||||
type Target = Vec<SvgSegment>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl core::ops::DerefMut for SvgSegmentList {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
pub trait RenderSvgSegmentList {
|
||||
fn to_svg_string(&self) -> String;
|
||||
}
|
||||
|
||||
impl core::fmt::Display for SvgSegmentList {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
impl RenderSvgSegmentList for Vec<SvgSegment> {
|
||||
fn to_svg_string(&self) -> String {
|
||||
let mut result = String::new();
|
||||
for segment in self.iter() {
|
||||
f.write_str(match segment {
|
||||
result.push_str(match segment {
|
||||
SvgSegment::Slice(x) => x,
|
||||
SvgSegment::String(x) => x,
|
||||
SvgSegment::BlobUrl(_) => "<!-- Blob url not yet loaded -->",
|
||||
})?;
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -718,22 +686,16 @@ pub struct SvgRenderAttrs<'a>(&'a mut SvgRender);
|
|||
|
||||
impl<'a> SvgRenderAttrs<'a> {
|
||||
pub fn push_complex(&mut self, name: impl Into<SvgSegment>, value: impl FnOnce(&mut SvgRender)) {
|
||||
self.0.svg.push(" ");
|
||||
self.0.svg.push(name);
|
||||
self.0.svg.push("=\"");
|
||||
self.0.svg.push(" ".into());
|
||||
self.0.svg.push(name.into());
|
||||
self.0.svg.push("=\"".into());
|
||||
value(self.0);
|
||||
self.0.svg.push("\"");
|
||||
self.0.svg.push("\"".into());
|
||||
}
|
||||
pub fn push(&mut self, name: impl Into<SvgSegment>, value: impl Into<SvgSegment>) {
|
||||
self.push_complex(name, move |renderer| renderer.svg.push(value));
|
||||
self.push_complex(name, move |renderer| renderer.svg.push(value.into()));
|
||||
}
|
||||
pub fn push_val(&mut self, value: impl Into<SvgSegment>) {
|
||||
self.0.svg.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
impl SvgSegmentList {
|
||||
pub fn push(&mut self, value: impl Into<SvgSegment>) {
|
||||
self.0.push(value.into());
|
||||
self.0.svg.push(value.into());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ impl<T, CachedNode> MemoNode<T, CachedNode> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Stores both what a node was called with and what it returned.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IORecord<I, O> {
|
||||
pub input: I,
|
||||
|
|
|
@ -347,6 +347,11 @@ impl DocumentNode {
|
|||
// TODO: Or, more fundamentally separate the concept of a layer from a node.
|
||||
self.name == "Artboard"
|
||||
}
|
||||
|
||||
pub fn is_folder(&self, network: &NodeNetwork) -> bool {
|
||||
let input_connection = self.inputs.get(0).and_then(|input| input.as_node()).and_then(|node_id| network.nodes.get(&node_id));
|
||||
input_connection.map(|node| node.is_layer()).unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the possible inputs to a node.
|
||||
|
@ -485,14 +490,19 @@ impl NodeOutput {
|
|||
|
||||
#[derive(Clone, Debug, Default, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
/// A network of nodes containing each [`DocumentNode`] and its ID, as well as a list of input nodes and [`NodeOutput`]s
|
||||
/// A network (subgraph) of nodes containing each [`DocumentNode`] and its ID, as well as a list of input nodes and [`NodeOutput`]s
|
||||
pub struct NodeNetwork {
|
||||
/// The list of nodes that are imported into this network from the parent network. Each is a reference to the node that the input is connected to.
|
||||
/// Presently, only one is supported— use an Identity node to split an input to multiple user nodes (although this could be changed in the future).
|
||||
pub inputs: Vec<NodeId>,
|
||||
/// The list of data outputs that are exported from this network to the parent network. Each is a reference to the node, and its output index, that is the source of the output data.
|
||||
pub outputs: Vec<NodeOutput>,
|
||||
/// The list of all nodes in this network.
|
||||
pub nodes: HashMap<NodeId, DocumentNode>,
|
||||
/// These nodes are replaced with identity nodes during the graph flattening step
|
||||
/// Nodes that the user has disabled/hidden with the visibility eye icon.
|
||||
/// These nodes get replaced with Identity nodes during the graph flattening step.
|
||||
pub disabled: Vec<NodeId>,
|
||||
/// In the case when a new node is chosen as a temporary output, this stores what it used to be so it can be restored later
|
||||
/// In the case when another node is previewed (chosen by the user as a temporary output), this stores what it used to be so it can be restored later.
|
||||
pub previous_outputs: Option<Vec<NodeOutput>>,
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ use dyn_any::StaticType;
|
|||
use graphene_core::application_io::{ApplicationError, ApplicationIo, ExportFormat, RenderConfig, ResourceFuture, SurfaceHandle, SurfaceHandleFrame, SurfaceId};
|
||||
use graphene_core::raster::Image;
|
||||
use graphene_core::raster::{color::SRGBA8, ImageFrame};
|
||||
use graphene_core::renderer::{format_transform_matrix, GraphicElementRendered, ImageRenderMode, RenderParams, SvgRender};
|
||||
use graphene_core::renderer::{format_transform_matrix, GraphicElementRendered, ImageRenderMode, RenderParams, RenderSvgSegmentList, SvgRender};
|
||||
use graphene_core::transform::Footprint;
|
||||
use graphene_core::Color;
|
||||
use graphene_core::Node;
|
||||
|
@ -303,7 +303,7 @@ fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_p
|
|||
data.render_svg(&mut render, &render_params);
|
||||
render.wrap_with_transform(footprint.transform, Some(footprint.resolution.as_dvec2()));
|
||||
|
||||
RenderOutput::Svg(render.svg.to_string())
|
||||
RenderOutput::Svg(render.svg.to_svg_string())
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "resvg", feature = "vello"))]
|
||||
|
@ -321,7 +321,7 @@ fn render_canvas(
|
|||
let min = footprint.transform.inverse().transform_point2((0., 0.).into());
|
||||
let max = footprint.transform.inverse().transform_point2(resolution.as_dvec2());
|
||||
render.format_svg(min, max);
|
||||
let string = render.svg.to_string();
|
||||
let string = render.svg.to_svg_string();
|
||||
let array = string.as_bytes();
|
||||
let canvas = &surface_handle.surface;
|
||||
canvas.set_width(resolution.x);
|
||||
|
|
|
@ -5,6 +5,7 @@ page_template = "book.html"
|
|||
|
||||
[extra]
|
||||
book = true
|
||||
js = ["video-embed.js"]
|
||||
+++
|
||||
|
||||
<br />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue