mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-26 00:28:11 +08:00
Brush tool live preview (#1116)
* Disable vector preview for brush tool * Fix brush preview * Fix warping * Left and right square brackets to change size * Add linear interpolation * Modfiy existing selected brush layer * Resolve warnings --------- Co-authored-by: Dennis Kobert <dennis@kobert.dev> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Dennis Kobert
Keavon Chambers
parent
69f6ed3a11
commit
f271964bf8
@@ -62,6 +62,9 @@ pub const CREATE_CURVE_THRESHOLD: f64 = 5.;
|
|||||||
// Line tool
|
// Line tool
|
||||||
pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
|
pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
|
||||||
|
|
||||||
|
// Brush tool
|
||||||
|
pub const BRUSH_SIZE_CHANGE_KEYBOARD: f64 = 5.;
|
||||||
|
|
||||||
// Scrollbars
|
// Scrollbars
|
||||||
pub const SCROLLBAR_SPACING: f64 = 0.1;
|
pub const SCROLLBAR_SPACING: f64 = 0.1;
|
||||||
pub const ASYMPTOTIC_EFFECT: f64 = 0.5;
|
pub const ASYMPTOTIC_EFFECT: f64 = 0.5;
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ pub struct FrontendDocumentDetails {
|
|||||||
pub id: u64,
|
pub id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, specta::Type)]
|
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)]
|
||||||
pub struct FrontendImageData {
|
pub struct FrontendImageData {
|
||||||
pub path: Vec<LayerId>,
|
pub path: Vec<LayerId>,
|
||||||
pub mime: String,
|
pub mime: String,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub image_data: std::sync::Arc<Vec<u8>>,
|
pub image_data: std::sync::Arc<Vec<u8>>,
|
||||||
|
pub transform: Option<[f64; 6]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize, specta::Type)]
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize, specta::Type)]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::consts::{BIG_NUDGE_AMOUNT, NUDGE_AMOUNT};
|
use crate::consts::{BIG_NUDGE_AMOUNT, BRUSH_SIZE_CHANGE_KEYBOARD, NUDGE_AMOUNT};
|
||||||
use crate::messages::input_mapper::key_mapping::MappingVariant;
|
use crate::messages::input_mapper::key_mapping::MappingVariant;
|
||||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeyStates};
|
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeyStates};
|
||||||
use crate::messages::input_mapper::utility_types::macros::*;
|
use crate::messages::input_mapper::utility_types::macros::*;
|
||||||
@@ -6,6 +6,7 @@ use crate::messages::input_mapper::utility_types::misc::MappingEntry;
|
|||||||
use crate::messages::input_mapper::utility_types::misc::{KeyMappingEntries, Mapping};
|
use crate::messages::input_mapper::utility_types::misc::{KeyMappingEntries, Mapping};
|
||||||
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
use crate::messages::tool::tool_messages::brush_tool::BrushToolMessageOptionsUpdate;
|
||||||
|
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
|
|
||||||
@@ -209,6 +210,8 @@ pub fn default_mapping() -> Mapping {
|
|||||||
entry!(PointerMove; action_dispatch=BrushToolMessage::PointerMove),
|
entry!(PointerMove; action_dispatch=BrushToolMessage::PointerMove),
|
||||||
entry!(KeyDown(Lmb); action_dispatch=BrushToolMessage::DragStart),
|
entry!(KeyDown(Lmb); action_dispatch=BrushToolMessage::DragStart),
|
||||||
entry!(KeyUp(Lmb); action_dispatch=BrushToolMessage::DragStop),
|
entry!(KeyUp(Lmb); action_dispatch=BrushToolMessage::DragStop),
|
||||||
|
entry!(KeyDown(BracketLeft); action_dispatch=BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ChangeDiameter(-BRUSH_SIZE_CHANGE_KEYBOARD))),
|
||||||
|
entry!(KeyDown(BracketRight); action_dispatch=BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ChangeDiameter(BRUSH_SIZE_CHANGE_KEYBOARD))),
|
||||||
//
|
//
|
||||||
// ToolMessage
|
// ToolMessage
|
||||||
entry!(KeyDown(KeyV); action_dispatch=ToolMessage::ActivateToolSelect),
|
entry!(KeyDown(KeyV); action_dispatch=ToolMessage::ActivateToolSelect),
|
||||||
|
|||||||
@@ -1628,6 +1628,7 @@ impl DocumentMessageHandler {
|
|||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
image_data: data.image_data.clone(),
|
image_data: data.image_data.clone(),
|
||||||
mime: node_graph_frame.mime.clone(),
|
mime: node_graph_frame.mime.clone(),
|
||||||
|
transform: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||||
use crate::messages::input_mapper::utility_types::input_keyboard::MouseMotion;
|
use crate::messages::input_mapper::utility_types::input_keyboard::MouseMotion;
|
||||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetLayout};
|
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetLayout};
|
||||||
|
use crate::messages::layout::utility_types::misc::LayoutTarget;
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput;
|
use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
@@ -9,11 +10,9 @@ use crate::messages::tool::utility_types::{DocumentToolData, EventToMessageMap,
|
|||||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||||
|
|
||||||
use document_legacy::LayerId;
|
use document_legacy::LayerId;
|
||||||
use document_legacy::Operation;
|
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork};
|
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork};
|
||||||
use graph_craft::{concrete, Type, TypeDescriptor};
|
use graph_craft::{concrete, Type, TypeDescriptor};
|
||||||
use graphene_core::vector::style::Stroke;
|
|
||||||
use graphene_core::Cow;
|
use graphene_core::Cow;
|
||||||
|
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
@@ -60,6 +59,7 @@ pub enum BrushToolMessage {
|
|||||||
#[remain::sorted]
|
#[remain::sorted]
|
||||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)]
|
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)]
|
||||||
pub enum BrushToolMessageOptionsUpdate {
|
pub enum BrushToolMessageOptionsUpdate {
|
||||||
|
ChangeDiameter(f64),
|
||||||
Diameter(f64),
|
Diameter(f64),
|
||||||
Flow(f64),
|
Flow(f64),
|
||||||
Hardness(f64),
|
Hardness(f64),
|
||||||
@@ -119,6 +119,18 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for BrushTo
|
|||||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) {
|
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) {
|
||||||
if let ToolMessage::Brush(BrushToolMessage::UpdateOptions(action)) = message {
|
if let ToolMessage::Brush(BrushToolMessage::UpdateOptions(action)) = message {
|
||||||
match action {
|
match action {
|
||||||
|
BrushToolMessageOptionsUpdate::ChangeDiameter(change) => {
|
||||||
|
let needs_rounding = ((self.options.diameter + change.abs() / 2.) % change.abs() - change.abs() / 2.).abs() > 0.5;
|
||||||
|
if needs_rounding && change > 0. {
|
||||||
|
self.options.diameter = (self.options.diameter / change.abs()).ceil() * change.abs();
|
||||||
|
} else if needs_rounding && change < 0. {
|
||||||
|
self.options.diameter = (self.options.diameter / change.abs()).floor() * change.abs();
|
||||||
|
} else {
|
||||||
|
self.options.diameter = (self.options.diameter / change.abs()).round() * change.abs() + change;
|
||||||
|
}
|
||||||
|
self.options.diameter = self.options.diameter.max(1.);
|
||||||
|
self.register_properties(responses, LayoutTarget::ToolOptions);
|
||||||
|
}
|
||||||
BrushToolMessageOptionsUpdate::Diameter(diameter) => self.options.diameter = diameter,
|
BrushToolMessageOptionsUpdate::Diameter(diameter) => self.options.diameter = diameter,
|
||||||
BrushToolMessageOptionsUpdate::Hardness(hardness) => self.options.hardness = hardness,
|
BrushToolMessageOptionsUpdate::Hardness(hardness) => self.options.hardness = hardness,
|
||||||
BrushToolMessageOptionsUpdate::Flow(flow) => self.options.flow = flow,
|
BrushToolMessageOptionsUpdate::Flow(flow) => self.options.flow = flow,
|
||||||
@@ -137,11 +149,13 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for BrushTo
|
|||||||
DragStart,
|
DragStart,
|
||||||
DragStop,
|
DragStop,
|
||||||
Abort,
|
Abort,
|
||||||
|
UpdateOptions,
|
||||||
),
|
),
|
||||||
Drawing => actions!(BrushToolMessageDiscriminant;
|
Drawing => actions!(BrushToolMessageDiscriminant;
|
||||||
DragStop,
|
DragStop,
|
||||||
PointerMove,
|
PointerMove,
|
||||||
Abort,
|
Abort,
|
||||||
|
UpdateOptions,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,6 +180,19 @@ struct BrushToolData {
|
|||||||
path: Option<Vec<LayerId>>,
|
path: Option<Vec<LayerId>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl BrushToolData {
|
||||||
|
fn update_points(&self, responses: &mut VecDeque<Message>) {
|
||||||
|
if let Some(layer_path) = self.path.clone() {
|
||||||
|
responses.add(NodeGraphMessage::SetQualifiedInputValue {
|
||||||
|
layer_path,
|
||||||
|
node_path: vec![0],
|
||||||
|
input_index: 1,
|
||||||
|
value: TaggedValue::VecDVec2(self.points.clone()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Fsm for BrushToolFsmState {
|
impl Fsm for BrushToolFsmState {
|
||||||
type ToolData = BrushToolData;
|
type ToolData = BrushToolData;
|
||||||
type ToolOptions = BrushOptions;
|
type ToolOptions = BrushOptions;
|
||||||
@@ -189,8 +216,15 @@ impl Fsm for BrushToolFsmState {
|
|||||||
match (self, event) {
|
match (self, event) {
|
||||||
(Ready, DragStart) => {
|
(Ready, DragStart) => {
|
||||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
let existing_points = load_existing_points(document);
|
||||||
tool_data.path = Some(document.get_path_for_new_layer());
|
let new_layer = existing_points.is_none();
|
||||||
|
if let Some((layer_path, points)) = existing_points {
|
||||||
|
tool_data.path = Some(layer_path);
|
||||||
|
tool_data.points = points;
|
||||||
|
} else {
|
||||||
|
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||||
|
tool_data.path = Some(document.get_path_for_new_layer());
|
||||||
|
}
|
||||||
|
|
||||||
let pos = transform.inverse().transform_point2(input.mouse.position);
|
let pos = transform.inverse().transform_point2(input.mouse.position);
|
||||||
|
|
||||||
@@ -200,7 +234,11 @@ impl Fsm for BrushToolFsmState {
|
|||||||
tool_data.hardness = tool_options.hardness;
|
tool_data.hardness = tool_options.hardness;
|
||||||
tool_data.flow = tool_options.flow;
|
tool_data.flow = tool_options.flow;
|
||||||
|
|
||||||
add_polyline(tool_data, global_tool_data, responses);
|
if new_layer {
|
||||||
|
add_brush_render(tool_data, global_tool_data, responses);
|
||||||
|
} else {
|
||||||
|
tool_data.update_points(responses);
|
||||||
|
}
|
||||||
|
|
||||||
Drawing
|
Drawing
|
||||||
}
|
}
|
||||||
@@ -208,17 +246,22 @@ impl Fsm for BrushToolFsmState {
|
|||||||
let pos = transform.inverse().transform_point2(input.mouse.position);
|
let pos = transform.inverse().transform_point2(input.mouse.position);
|
||||||
|
|
||||||
if tool_data.points.last() != Some(&pos) {
|
if tool_data.points.last() != Some(&pos) {
|
||||||
|
// Linear interpolation for when the mouse has moved a lot between frames
|
||||||
|
if let Some(&last_point) = tool_data.points.last() {
|
||||||
|
let distance = (last_point - pos).length();
|
||||||
|
let extra_points = (distance / (tool_data.diameter / 2.)).floor() as usize;
|
||||||
|
tool_data.points.extend((0..extra_points).map(|i| last_point.lerp(pos, (i as f64 + 1.) / (extra_points as f64 + 1.))));
|
||||||
|
}
|
||||||
|
|
||||||
tool_data.points.push(pos);
|
tool_data.points.push(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
add_polyline(tool_data, global_tool_data, responses);
|
tool_data.update_points(responses);
|
||||||
|
|
||||||
Drawing
|
Drawing
|
||||||
}
|
}
|
||||||
(Drawing, DragStop) | (Drawing, Abort) => {
|
(Drawing, DragStop) | (Drawing, Abort) => {
|
||||||
if !tool_data.points.is_empty() {
|
if !tool_data.points.is_empty() {
|
||||||
responses.push_back(remove_preview(tool_data));
|
|
||||||
add_brush_render(tool_data, global_tool_data, responses);
|
|
||||||
responses.push_back(DocumentMessage::CommitTransaction.into());
|
responses.push_back(DocumentMessage::CommitTransaction.into());
|
||||||
} else {
|
} else {
|
||||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||||
@@ -250,21 +293,6 @@ impl Fsm for BrushToolFsmState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_preview(data: &BrushToolData) -> Message {
|
|
||||||
Operation::DeleteLayer { path: data.path.clone().unwrap() }.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_polyline(data: &BrushToolData, tool_data: &DocumentToolData, responses: &mut VecDeque<Message>) {
|
|
||||||
let layer_path = data.path.clone().unwrap();
|
|
||||||
let subpath = bezier_rs::Subpath::from_anchors(data.points.iter().copied(), false);
|
|
||||||
graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses);
|
|
||||||
|
|
||||||
responses.add(GraphOperationMessage::StrokeSet {
|
|
||||||
layer: layer_path,
|
|
||||||
stroke: Stroke::new(tool_data.primary_color.apply_opacity(data.flow as f32 / 100.), data.diameter),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_brush_render(data: &BrushToolData, tool_data: &DocumentToolData, responses: &mut VecDeque<Message>) {
|
fn add_brush_render(data: &BrushToolData, tool_data: &DocumentToolData, responses: &mut VecDeque<Message>) {
|
||||||
let layer_path = data.path.clone().unwrap();
|
let layer_path = data.path.clone().unwrap();
|
||||||
let brush_node = DocumentNode {
|
let brush_node = DocumentNode {
|
||||||
@@ -288,3 +316,22 @@ fn add_brush_render(data: &BrushToolData, tool_data: &DocumentToolData, response
|
|||||||
network.push_output_node();
|
network.push_output_node();
|
||||||
graph_modification_utils::new_custom_layer(network, layer_path.clone(), responses);
|
graph_modification_utils::new_custom_layer(network, layer_path.clone(), responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_existing_points(document: &DocumentMessageHandler) -> Option<(Vec<LayerId>, Vec<DVec2>)> {
|
||||||
|
if document.selected_layers().count() != 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let layer_path = document.selected_layers().next()?.to_vec();
|
||||||
|
let network = document.document_legacy.layer(&layer_path).ok().and_then(|layer| layer.as_node_graph().ok())?;
|
||||||
|
let brush_node = network.nodes.get(&0)?;
|
||||||
|
if brush_node.implementation != DocumentNodeImplementation::Unresolved("graphene_std::brush::BrushNode".into()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let points_input = brush_node.inputs.get(1)?;
|
||||||
|
let NodeInput::Value {
|
||||||
|
tagged_value: TaggedValue::VecDVec2(points),
|
||||||
|
..
|
||||||
|
} = points_input else { return None };
|
||||||
|
|
||||||
|
Some((layer_path, points.clone()))
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use document_legacy::LayerId;
|
|||||||
use document_legacy::Operation;
|
use document_legacy::Operation;
|
||||||
use graphene_core::vector::style::Stroke;
|
use graphene_core::vector::style::Stroke;
|
||||||
|
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::DVec2;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -217,9 +217,9 @@ fn remove_preview(data: &FreehandToolData) -> Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn add_polyline(data: &FreehandToolData, tool_data: &DocumentToolData, responses: &mut VecDeque<Message>) {
|
fn add_polyline(data: &FreehandToolData, tool_data: &DocumentToolData, responses: &mut VecDeque<Message>) {
|
||||||
let layer_path = data.path.clone().unwrap();
|
|
||||||
let subpath = bezier_rs::Subpath::from_anchors(data.points.iter().copied(), false);
|
let subpath = bezier_rs::Subpath::from_anchors(data.points.iter().copied(), false);
|
||||||
let position = subpath.bounding_box().unwrap_or_default().into_iter().sum::<DVec2>() / 2.;
|
|
||||||
|
let layer_path = data.path.clone().unwrap();
|
||||||
graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses);
|
graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses);
|
||||||
|
|
||||||
responses.add(GraphOperationMessage::StrokeSet {
|
responses.add(GraphOperationMessage::StrokeSet {
|
||||||
|
|||||||
@@ -971,12 +971,6 @@ fn rerender_selected_layers(tool_data: &mut SelectToolData, responses: &mut VecD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rerender_duplicated_layers(tool_data: &mut SelectToolData, responses: &mut VecDeque<Message>) {
|
|
||||||
for layer_path in tool_data.not_duplicated_layers.iter().flatten() {
|
|
||||||
responses.add(DocumentMessage::NodeGraphFrameGenerate { layer_path: layer_path.clone() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Majorly clean up these next five functions
|
// TODO: Majorly clean up these next five functions
|
||||||
|
|
||||||
fn drag_shallowest_manipulation(
|
fn drag_shallowest_manipulation(
|
||||||
|
|||||||
@@ -262,10 +262,10 @@ fn add_spline(tool_data: &SplineToolData, global_tool_data: &DocumentToolData, s
|
|||||||
}
|
}
|
||||||
|
|
||||||
let subpath = bezier_rs::Subpath::new_cubic_spline(points);
|
let subpath = bezier_rs::Subpath::new_cubic_spline(points);
|
||||||
let position = subpath.bounding_box().unwrap_or_default().into_iter().sum::<DVec2>() / 2.;
|
|
||||||
|
|
||||||
let layer_path = tool_data.path.clone().unwrap();
|
let layer_path = tool_data.path.clone().unwrap();
|
||||||
graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses);
|
graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses);
|
||||||
|
|
||||||
responses.add(GraphOperationMessage::StrokeSet {
|
responses.add(GraphOperationMessage::StrokeSet {
|
||||||
layer: layer_path.clone(),
|
layer: layer_path.clone(),
|
||||||
stroke: Stroke::new(global_tool_data.primary_color, tool_data.weight),
|
stroke: Stroke::new(global_tool_data.primary_color, tool_data.weight),
|
||||||
|
|||||||
@@ -269,9 +269,17 @@ impl NodeGraphExecutor {
|
|||||||
// Attempt to downcast to an image frame
|
// Attempt to downcast to an image frame
|
||||||
let ImageFrame { image, transform } = dyn_any::downcast(boxed_node_graph_output).map(|image_frame| *image_frame)?;
|
let ImageFrame { image, transform } = dyn_any::downcast(boxed_node_graph_output).map(|image_frame| *image_frame)?;
|
||||||
|
|
||||||
|
// Don't update the frame's transform if the new transform is DAffine2::ZERO.
|
||||||
|
let transform = (!transform.abs_diff_eq(DAffine2::ZERO, f64::EPSILON)).then_some(transform.to_cols_array());
|
||||||
|
|
||||||
// If no image was generated, clear the frame
|
// If no image was generated, clear the frame
|
||||||
if image.width == 0 || image.height == 0 {
|
if image.width == 0 || image.height == 0 {
|
||||||
responses.push_back(DocumentMessage::FrameClear.into());
|
responses.push_back(DocumentMessage::FrameClear.into());
|
||||||
|
|
||||||
|
// Update the transform based on the graph output
|
||||||
|
if let Some(transform) = transform {
|
||||||
|
responses.push_back(Operation::SetLayerTransform { path: layer_path.clone(), transform }.into());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Update the image data
|
// Update the image data
|
||||||
let (image_data, _size) = Self::encode_img(image, None, image::ImageOutputFormat::Bmp)?;
|
let (image_data, _size) = Self::encode_img(image, None, image::ImageOutputFormat::Bmp)?;
|
||||||
@@ -289,17 +297,10 @@ impl NodeGraphExecutor {
|
|||||||
path: layer_path.clone(),
|
path: layer_path.clone(),
|
||||||
image_data,
|
image_data,
|
||||||
mime,
|
mime,
|
||||||
|
transform,
|
||||||
}];
|
}];
|
||||||
responses.push_back(FrontendMessage::UpdateImageData { document_id, image_data }.into());
|
responses.push_back(FrontendMessage::UpdateImageData { document_id, image_data }.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't update the frame's transform if the new transform is DAffine2::ZERO.
|
|
||||||
if !transform.abs_diff_eq(DAffine2::ZERO, f64::EPSILON) {
|
|
||||||
// Update the transform based on the graph output
|
|
||||||
let transform = transform.to_cols_array();
|
|
||||||
responses.push_back(Operation::SetLayerTransform { path: layer_path.clone(), transform }.into());
|
|
||||||
responses.push_back(Operation::SetLayerVisibility { path: layer_path, visible: true }.into());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -98,11 +98,13 @@ fn handle_message(message: String) -> String {
|
|||||||
for image in image_data {
|
for image in image_data {
|
||||||
let path = image.path.clone();
|
let path = image.path.clone();
|
||||||
let mime = image.mime.clone();
|
let mime = image.mime.clone();
|
||||||
|
let transform = image.transform.clone();
|
||||||
images.insert(format!("{:?}_{}", &image.path, document_id), image);
|
images.insert(format!("{:?}_{}", &image.path, document_id), image);
|
||||||
stub_data.push(FrontendImageData {
|
stub_data.push(FrontendImageData {
|
||||||
path,
|
path,
|
||||||
mime,
|
mime,
|
||||||
image_data: Arc::new(Vec::new()),
|
image_data: Arc::new(Vec::new()),
|
||||||
|
transform,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
FrontendMessage::UpdateImageData { document_id, image_data: stub_data }
|
FrontendMessage::UpdateImageData { document_id, image_data: stub_data }
|
||||||
|
|||||||
@@ -529,6 +529,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
|
opacity: 1;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export function createPortfolioState(editor: Editor) {
|
|||||||
image.src = blobURL;
|
image.src = blobURL;
|
||||||
await image.decode();
|
await image.decode();
|
||||||
|
|
||||||
editor.instance.setImageBlobURL(updateImageData.documentId, element.path, blobURL, image.naturalWidth, image.naturalHeight);
|
editor.instance.setImageBlobURL(updateImageData.documentId, element.path, blobURL, image.naturalWidth, image.naturalHeight, element.transform);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
editor.subscriptions.subscribeJsMessage(TriggerNodeGraphFrameGenerate, async (triggerNodeGraphFrameGenerate) => {
|
editor.subscriptions.subscribeJsMessage(TriggerNodeGraphFrameGenerate, async (triggerNodeGraphFrameGenerate) => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export type Editor = Readonly<ReturnType<typeof createEditor>>;
|
|||||||
let wasmImport: WasmRawInstance | undefined;
|
let wasmImport: WasmRawInstance | undefined;
|
||||||
let editorInstance: WasmEditorInstance | undefined;
|
let editorInstance: WasmEditorInstance | undefined;
|
||||||
|
|
||||||
export async function updateImage(path: BigUint64Array, mime: string, imageData: Uint8Array, documentId: bigint): Promise<void> {
|
export async function updateImage(path: BigUint64Array, mime: string, imageData: Uint8Array, transform: Float64Array, documentId: bigint): Promise<void> {
|
||||||
const blob = new Blob([imageData], { type: mime });
|
const blob = new Blob([imageData], { type: mime });
|
||||||
|
|
||||||
const blobURL = URL.createObjectURL(blob);
|
const blobURL = URL.createObjectURL(blob);
|
||||||
@@ -21,7 +21,7 @@ export async function updateImage(path: BigUint64Array, mime: string, imageData:
|
|||||||
image.src = blobURL;
|
image.src = blobURL;
|
||||||
await image.decode();
|
await image.decode();
|
||||||
|
|
||||||
editorInstance?.setImageBlobURL(documentId, path, blobURL, image.naturalWidth, image.naturalHeight);
|
editorInstance?.setImageBlobURL(documentId, path, blobURL, image.naturalWidth, image.naturalHeight,transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchImage(path: BigUint64Array, mime: string, documentId: bigint, url: string): Promise<void> {
|
export async function fetchImage(path: BigUint64Array, mime: string, documentId: bigint, url: string): Promise<void> {
|
||||||
@@ -35,7 +35,7 @@ export async function fetchImage(path: BigUint64Array, mime: string, documentId:
|
|||||||
image.src = blobURL;
|
image.src = blobURL;
|
||||||
await image.decode();
|
await image.decode();
|
||||||
|
|
||||||
editorInstance?.setImageBlobURL(documentId, path, blobURL, image.naturalWidth, image.naturalHeight);
|
editorInstance?.setImageBlobURL(documentId, path, blobURL, image.naturalWidth, image.naturalHeight, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tauri = "__TAURI_METADATA__" in window && import("@tauri-apps/api");
|
const tauri = "__TAURI_METADATA__" in window && import("@tauri-apps/api");
|
||||||
|
|||||||
@@ -768,6 +768,8 @@ export class ImaginateImageData {
|
|||||||
readonly mime!: string;
|
readonly mime!: string;
|
||||||
|
|
||||||
readonly imageData!: Uint8Array;
|
readonly imageData!: Uint8Array;
|
||||||
|
|
||||||
|
readonly transform!: Float64Array ;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DisplayDialogDismiss extends JsMessage {}
|
export class DisplayDialogDismiss extends JsMessage {}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ pub fn set_random_seed(seed: u64) {
|
|||||||
/// This avoids creating a json with a list millions of numbers long.
|
/// This avoids creating a json with a list millions of numbers long.
|
||||||
#[wasm_bindgen(module = "@graphite/wasm-communication/editor")]
|
#[wasm_bindgen(module = "@graphite/wasm-communication/editor")]
|
||||||
extern "C" {
|
extern "C" {
|
||||||
fn updateImage(path: Vec<u64>, mime: String, imageData: &[u8], document_id: u64);
|
fn updateImage(path: Vec<u64>, mime: String, imageData: &[u8], transform: js_sys::Float64Array, document_id: u64);
|
||||||
fn fetchImage(path: Vec<u64>, mime: String, document_id: u64, identifier: String);
|
fn fetchImage(path: Vec<u64>, mime: String, document_id: u64, identifier: String);
|
||||||
//fn dispatchTauri(message: String) -> String;
|
//fn dispatchTauri(message: String) -> String;
|
||||||
fn dispatchTauri(message: String);
|
fn dispatchTauri(message: String);
|
||||||
@@ -114,7 +114,16 @@ impl JsEditorHandle {
|
|||||||
if let FrontendMessage::UpdateImageData { document_id, image_data } = message {
|
if let FrontendMessage::UpdateImageData { document_id, image_data } = message {
|
||||||
for image in image_data {
|
for image in image_data {
|
||||||
#[cfg(not(feature = "tauri"))]
|
#[cfg(not(feature = "tauri"))]
|
||||||
updateImage(image.path, image.mime, &image.image_data, document_id);
|
{
|
||||||
|
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()
|
||||||
|
};
|
||||||
|
updateImage(image.path, image.mime, &image.image_data, transform, document_id);
|
||||||
|
}
|
||||||
#[cfg(feature = "tauri")]
|
#[cfg(feature = "tauri")]
|
||||||
fetchImage(image.path.clone(), image.mime, document_id, format!("http://localhost:3001/image/{:?}_{}", &image.path, document_id));
|
fetchImage(image.path.clone(), image.mime, document_id, format!("http://localhost:3001/image/{:?}_{}", &image.path, document_id));
|
||||||
}
|
}
|
||||||
@@ -495,15 +504,22 @@ impl JsEditorHandle {
|
|||||||
|
|
||||||
/// Sends the blob URL generated by JS to the Image layer
|
/// Sends the blob URL generated by JS to the Image layer
|
||||||
#[wasm_bindgen(js_name = setImageBlobURL)]
|
#[wasm_bindgen(js_name = setImageBlobURL)]
|
||||||
pub fn set_image_blob_url(&self, document_id: u64, layer_path: Vec<LayerId>, blob_url: String, width: f64, height: f64) {
|
pub fn set_image_blob_url(&self, document_id: u64, layer_path: Vec<LayerId>, blob_url: String, width: f64, height: f64, transform: Option<js_sys::Float64Array>) {
|
||||||
let resolution = (width, height);
|
let resolution = (width, height);
|
||||||
let message = PortfolioMessage::SetImageBlobUrl {
|
let message = PortfolioMessage::SetImageBlobUrl {
|
||||||
document_id,
|
document_id,
|
||||||
layer_path,
|
layer_path: layer_path.clone(),
|
||||||
blob_url,
|
blob_url,
|
||||||
resolution,
|
resolution,
|
||||||
};
|
};
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
|
|
||||||
|
if let Some(array) = transform.filter(|array| array.length() == 6) {
|
||||||
|
let mut transform: [f64; 6] = [0.; 6];
|
||||||
|
array.copy_to(&mut transform);
|
||||||
|
let message = document_legacy::Operation::SetLayerTransform { path: layer_path, transform };
|
||||||
|
self.dispatch(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends the blob URL generated by JS to the Imaginate layer in the respective document
|
/// Sends the blob URL generated by JS to the Imaginate layer in the respective document
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use core::marker::PhantomData;
|
use core::marker::PhantomData;
|
||||||
use dyn_any::{DynAny, StaticType, StaticTypeSized};
|
use dyn_any::{StaticType, StaticTypeSized};
|
||||||
|
|
||||||
use crate::Node;
|
use crate::Node;
|
||||||
|
|
||||||
|
|||||||
@@ -274,7 +274,8 @@ fn blend_image_tuple<MapFn>(images: (ImageFrame, ImageFrame), map_fn: &'any_inpu
|
|||||||
where
|
where
|
||||||
MapFn: for<'any_input> Node<'any_input, (Color, Color), Output = Color> + 'input + Clone,
|
MapFn: for<'any_input> Node<'any_input, (Color, Color), Output = Color> + 'input + Clone,
|
||||||
{
|
{
|
||||||
let (mut background, foreground) = images;
|
let (background, foreground) = images;
|
||||||
|
|
||||||
let node = BlendImageNode::new(ClonedNode::new(background), ValueNode::new(map_fn.clone()));
|
let node = BlendImageNode::new(ClonedNode::new(background), ValueNode::new(map_fn.clone()));
|
||||||
node.eval(foreground)
|
node.eval(foreground)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use graph_craft::document::value::UpcastNode;
|
|||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
use graph_craft::executor::Executor;
|
use graph_craft::executor::Executor;
|
||||||
use graph_craft::proto::{ConstructionArgs, ProtoNetwork, ProtoNode, TypingContext};
|
use graph_craft::proto::{ConstructionArgs, ProtoNetwork, ProtoNode, TypingContext};
|
||||||
use graph_craft::{Type, TypeDescriptor};
|
use graph_craft::Type;
|
||||||
use graphene_std::any::{Any, TypeErasedPinned, TypeErasedPinnedRef};
|
use graphene_std::any::{Any, TypeErasedPinned, TypeErasedPinnedRef};
|
||||||
|
|
||||||
use crate::node_registry;
|
use crate::node_registry;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use graph_craft::proto::NodeConstructor;
|
|||||||
|
|
||||||
use graphene_core::{concrete, fn_type, generic, value_fn};
|
use graphene_core::{concrete, fn_type, generic, value_fn};
|
||||||
use graphene_std::memo::{CacheNode, LetNode};
|
use graphene_std::memo::{CacheNode, LetNode};
|
||||||
use graphene_std::raster::{BlendImageTupleNode, MapImageFrameNode};
|
use graphene_std::raster::BlendImageTupleNode;
|
||||||
|
|
||||||
use crate::executor::NodeContainer;
|
use crate::executor::NodeContainer;
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use proc_macro::TokenStream;
|
|||||||
use proc_macro2::Span;
|
use proc_macro2::Span;
|
||||||
use quote::{format_ident, ToTokens};
|
use quote::{format_ident, ToTokens};
|
||||||
use syn::{
|
use syn::{
|
||||||
parse_macro_input, punctuated::Punctuated, token::Comma, FnArg, GenericParam, Ident, ItemFn, Lifetime, Pat, PatIdent, PatType, PathArguments, PredicateType, ReturnType, Token, TraitBound, Type,
|
parse_macro_input, punctuated::Punctuated, token::Comma, FnArg, GenericParam, Ident, ItemFn, Lifetime, Pat, PatIdent, PathArguments, PredicateType, ReturnType, Token, TraitBound, Type, TypeParam,
|
||||||
TypeParam, TypeParamBound, WhereClause, WherePredicate,
|
TypeParamBound, WhereClause, WherePredicate,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
@@ -51,7 +51,7 @@ pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
.filter(|gen| {
|
.filter(|gen| {
|
||||||
if let GenericParam::Type(ty) = gen {
|
if let GenericParam::Type(ty) = gen {
|
||||||
!function.sig.inputs.iter().take(1).any(|param_ty| match param_ty {
|
!function.sig.inputs.iter().take(1).any(|param_ty| match param_ty {
|
||||||
FnArg::Typed(pat_ty) => pat_ty.ty.to_token_stream().to_string() == ty.ident.to_string(),
|
FnArg::Typed(pat_ty) => ty.ident == pat_ty.ty.to_token_stream().to_string(),
|
||||||
_ => false,
|
_ => false,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user