Fix transforms, Brush tool, and G/R/S (#1473)

* Transform fixes

* Fix the desert artwork

* Change artboard icon

* Better handling when transforming brush strokes

* Code review pass

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube 2023-11-27 04:54:06 +00:00 committed by GitHub
parent 5ee79031ab
commit b2ca643e6e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 308 additions and 238 deletions

View file

@ -156,6 +156,8 @@ impl Document {
pub fn click(&self, viewport_location: DVec2, network: &NodeNetwork) -> Option<LayerNodeIdentifier> {
self.click_xray(viewport_location).find(|&layer| !is_artboard(layer, network))
}
/// 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))

View file

@ -222,7 +222,16 @@ fn sibling_below<'a>(graph: &'a NodeNetwork, node: &DocumentNode) -> Option<(&'a
// transforms
impl DocumentMetadata {
/// Update the cached transforms of the layers
pub fn update_transforms(&mut self, new_transforms: HashMap<LayerNodeIdentifier, DAffine2>, new_upstream_transforms: HashMap<NodeId, DAffine2>) {
pub fn update_transforms(&mut self, mut new_transforms: HashMap<LayerNodeIdentifier, DAffine2>, new_upstream_transforms: HashMap<NodeId, DAffine2>) {
let mut stack = vec![(LayerNodeIdentifier::ROOT, DAffine2::IDENTITY)];
while let Some((layer, transform)) = stack.pop() {
for child in layer.children(self) {
let Some(new_transform) = new_transforms.get_mut(&child) else { continue };
*new_transform = transform * *new_transform;
stack.push((child, *new_transform));
}
}
self.transforms = new_transforms;
self.upstream_transforms = new_upstream_transforms;
}
@ -235,6 +244,10 @@ impl DocumentMetadata {
})
}
pub fn local_transform(&self, layer: LayerNodeIdentifier) -> DAffine2 {
self.transform_to_document(layer.parent(self).unwrap_or_default()).inverse() * self.transform_to_document(layer)
}
pub fn transform_to_viewport(&self, layer: LayerNodeIdentifier) -> DAffine2 {
self.document_to_viewport * self.transform_to_document(layer)
}
@ -299,11 +312,16 @@ impl DocumentMetadata {
self.bounding_box_with_transform(layer, self.transform_to_viewport(layer))
}
/// Calculates the document bounds used for scrolling and centring (the layer bounds or the artboard (if applicable))
pub fn document_bounds(&self) -> Option<[DVec2; 2]> {
/// Calculates the document bounds in viewport space
pub fn document_bounds_viewport_space(&self) -> Option<[DVec2; 2]> {
self.all_layers().filter_map(|layer| self.bounding_box_viewport(layer)).reduce(Quad::combine_bounds)
}
/// Calculates the document bounds in document space
pub fn document_bounds_document_space(&self) -> Option<[DVec2; 2]> {
self.all_layers().filter_map(|layer| self.bounding_box_document(layer)).reduce(Quad::combine_bounds)
}
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> graphene_core::vector::Subpath {
let Some(click_targets) = self.click_targets.get(&layer) else {
return graphene_core::vector::Subpath::new();
@ -546,8 +564,8 @@ impl From<NodeId> for LayerNodeIdentifier {
}
impl From<LayerNodeIdentifier> for NodeId {
fn from(identifer: LayerNodeIdentifier) -> Self {
identifer.to_node()
fn from(identifier: LayerNodeIdentifier) -> Self {
identifier.to_node()
}
}

View file

@ -49,6 +49,7 @@ pub enum LayerDataTypeDiscriminant {
Folder,
Shape,
Layer,
Artboard,
}
impl fmt::Display for LayerDataTypeDiscriminant {
@ -57,6 +58,7 @@ impl fmt::Display for LayerDataTypeDiscriminant {
LayerDataTypeDiscriminant::Folder => write!(f, "Folder"),
LayerDataTypeDiscriminant::Shape => write!(f, "Shape"),
LayerDataTypeDiscriminant::Layer => write!(f, "Layer"),
LayerDataTypeDiscriminant::Artboard => write!(f, "Artboard"),
}
}
}

View file

@ -1,5 +1,6 @@
use crate::consts::{DEFAULT_FONT_FAMILY, DEFAULT_FONT_STYLE};
use crate::messages::debug::utility_types::MessageLoggingVerbosity;
use crate::messages::dialog::DialogData;
use crate::messages::prelude::*;
use graphene_core::text::Font;
@ -112,11 +113,11 @@ impl Dispatcher {
self.message_handlers.debug_message_handler.process_message(message, &mut queue, ());
}
Dialog(message) => {
self.message_handlers.dialog_message_handler.process_message(
message,
&mut queue,
(&self.message_handlers.portfolio_message_handler, &self.message_handlers.preferences_message_handler),
);
let data = DialogData {
portfolio: &self.message_handlers.portfolio_message_handler,
preferences: &self.message_handlers.preferences_message_handler,
};
self.message_handlers.dialog_message_handler.process_message(message, &mut queue, data);
}
Frontend(message) => {
// Handle these messages immediately by returning early

View file

@ -11,9 +11,14 @@ pub struct DialogMessageHandler {
preferences_dialog: PreferencesDialogMessageHandler,
}
impl MessageHandler<DialogMessage, (&PortfolioMessageHandler, &PreferencesMessageHandler)> for DialogMessageHandler {
pub struct DialogData<'a> {
pub portfolio: &'a PortfolioMessageHandler,
pub preferences: &'a PreferencesMessageHandler,
}
impl MessageHandler<DialogMessage, DialogData<'_>> for DialogMessageHandler {
#[remain::check]
fn process_message(&mut self, message: DialogMessage, responses: &mut VecDeque<Message>, (portfolio, preferences): (&PortfolioMessageHandler, &PreferencesMessageHandler)) {
fn process_message(&mut self, message: DialogMessage, responses: &mut VecDeque<Message>, DialogData { portfolio, preferences }: DialogData) {
#[remain::sorted]
match message {
#[remain::unsorted]

View file

@ -16,4 +16,4 @@ pub mod simple_dialogs;
#[doc(inline)]
pub use dialog_message::{DialogMessage, DialogMessageDiscriminant};
#[doc(inline)]
pub use dialog_message_handler::DialogMessageHandler;
pub use dialog_message_handler::*;

View file

@ -1,9 +1,10 @@
use crate::consts::VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*;
use graphene_core::uuid::generate_uuid;
use glam::{IVec2, UVec2};
use glam::{DVec2, IVec2, UVec2};
/// A dialog to allow users to set some initial options about a new document.
#[derive(Debug, Clone, Default)]
@ -24,13 +25,18 @@ impl MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessageHa
NewDocumentDialogMessage::Submit => {
responses.add(PortfolioMessage::NewDocumentWithName { name: self.name.clone() });
if !self.infinite && self.dimensions.x > 0 && self.dimensions.y > 0 {
let create_artboard = !self.infinite && self.dimensions.x > 0 && self.dimensions.y > 0;
if create_artboard {
let id = generate_uuid();
responses.add(GraphOperationMessage::NewArtboard {
id,
artboard: graphene_core::Artboard::new(IVec2::ZERO, self.dimensions.as_ivec2()),
});
responses.add(DocumentMessage::ZoomCanvasToFitAll);
responses.add(NavigationMessage::FitViewportToBounds {
bounds: [DVec2::ZERO, self.dimensions.as_dvec2()],
padding_scale_factor: Some(VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR),
prevent_zoom_past_100: true,
});
}
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::UpdateNewNodeGraph);

View file

@ -158,7 +158,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
}
#[remain::unsorted]
Navigation(message) => {
let document_bounds = self.metadata().document_bounds();
let document_bounds = self.metadata().document_bounds_viewport_space();
self.navigation_handler.process_message(
message,
responses,
@ -306,7 +306,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
responses.add(BroadcastEvent::DocumentIsDirty);
}
DeselectAllLayers => {
responses.add_front(SetSelectedLayers { replacement_selected_layers: vec![] });
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
self.layer_range_selection_reference = None;
}
DirtyRenderDocument => {
@ -623,7 +623,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
let viewport_size = ipp.viewport_bounds.size();
let viewport_mid = ipp.viewport_bounds.center();
let [bounds1, bounds2] = self.metadata().document_bounds().unwrap_or([viewport_mid; 2]);
let [bounds1, bounds2] = self.metadata().document_bounds_viewport_space().unwrap_or([viewport_mid; 2]);
let bounds1 = bounds1.min(viewport_mid) - viewport_size * scale;
let bounds2 = bounds2.max(viewport_mid) + viewport_size * scale;
let bounds_length = (bounds2 - bounds1) * (1. + SCROLLBAR_SPACING);
@ -657,20 +657,20 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
})
}
SelectAllLayers => {
let all = self.all_layers().map(|path| path.to_vec()).collect();
responses.add_front(SetSelectedLayers { replacement_selected_layers: all });
let all = self.metadata().all_layers().map(|layer| layer.to_node()).collect();
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: all });
}
SelectedLayersLower => {
responses.add_front(DocumentMessage::SelectedLayersReorder { relative_index_offset: -1 });
responses.add(DocumentMessage::SelectedLayersReorder { relative_index_offset: 1 });
}
SelectedLayersLowerToBack => {
responses.add_front(DocumentMessage::SelectedLayersReorder { relative_index_offset: isize::MIN });
responses.add(DocumentMessage::SelectedLayersReorder { relative_index_offset: isize::MAX });
}
SelectedLayersRaise => {
responses.add_front(DocumentMessage::SelectedLayersReorder { relative_index_offset: 1 });
responses.add(DocumentMessage::SelectedLayersReorder { relative_index_offset: -1 });
}
SelectedLayersRaiseToFront => {
responses.add_front(DocumentMessage::SelectedLayersReorder { relative_index_offset: isize::MAX });
responses.add(DocumentMessage::SelectedLayersReorder { relative_index_offset: isize::MIN });
}
SelectedLayersReorder { relative_index_offset } => {
self.selected_layers_reorder(relative_index_offset, responses);
@ -813,7 +813,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
}
SetViewMode { view_mode } => {
self.view_mode = view_mode;
responses.add_front(DocumentMessage::DirtyRenderDocument);
responses.add_front(NodeGraphMessage::RunDocumentGraph);
}
StartTransaction => self.backup(responses),
ToggleLayerExpansion { layer } => {
@ -876,7 +876,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
responses.add_front(NavigationMessage::SetCanvasZoom { zoom_factor: 2. });
}
ZoomCanvasToFitAll => {
if let Some(bounds) = self.metadata().document_bounds() {
if let Some(bounds) = self.metadata().document_bounds_document_space() {
responses.add(NavigationMessage::FitViewportToBounds {
bounds,
padding_scale_factor: Some(VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR),

View file

@ -68,12 +68,10 @@ impl MessageHandler<NavigationMessage, (&Document, Option<[DVec2; 2]>, &InputPre
responses.add(SetCanvasZoom { zoom_factor: new_scale });
}
FitViewportToBounds {
bounds: [bounds_corner_a, bounds_corner_b],
bounds: [pos1, pos2],
padding_scale_factor,
prevent_zoom_past_100,
} => {
let pos1 = document.metadata.document_to_viewport.inverse().transform_point2(bounds_corner_a);
let pos2 = document.metadata.document_to_viewport.inverse().transform_point2(bounds_corner_b);
let v1 = document.metadata.document_to_viewport.inverse().transform_point2(DVec2::ZERO);
let v2 = document.metadata.document_to_viewport.inverse().transform_point2(ipp.viewport_bounds.size());
@ -98,8 +96,9 @@ impl MessageHandler<NavigationMessage, (&Document, Option<[DVec2; 2]>, &InputPre
}
FitViewportToSelection => {
if let Some(bounds) = selection_bounds {
let transform = document.metadata.document_to_viewport.inverse();
responses.add(FitViewportToBounds {
bounds,
bounds: [transform.transform_point2(bounds[0]), transform.transform_point2(bounds[1])],
padding_scale_factor: Some(VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR),
prevent_zoom_past_100: false,
})
@ -382,16 +381,18 @@ impl NavigationMessageHandler {
}
pub fn calculate_offset_transform(&self, viewport_center: DVec2) -> DAffine2 {
let scaled_centre = viewport_center / self.snapped_scale();
// Try to avoid fractional coordinates to reduce anti aliasing.
let scale = self.snapped_scale();
let rounded_pan = ((self.pan + viewport_center) * scale).round() / scale - viewport_center;
let rounded_pan = ((self.pan + scaled_centre) * scale).round() / scale - scaled_centre;
// TODO: replace with DAffine2::from_scale_angle_translation and fix the errors
let offset_transform = DAffine2::from_translation(viewport_center);
let offset_transform = DAffine2::from_translation(scaled_centre);
let scale_transform = DAffine2::from_scale(DVec2::splat(scale));
let angle_transform = DAffine2::from_angle(self.snapped_angle());
let translation_transform = DAffine2::from_translation(rounded_pan);
scale_transform * offset_transform * angle_transform * offset_transform.inverse() * translation_transform
scale_transform * offset_transform * angle_transform * translation_transform
}
fn create_document_transform(&self, viewport_center: DVec2, responses: &mut VecDeque<Message>) {
@ -403,7 +404,7 @@ impl NavigationMessageHandler {
let new_viewport_bounds = viewport_bounds / zoom_factor;
let delta_size = viewport_bounds - new_viewport_bounds;
let mouse_fraction = mouse / viewport_bounds;
let delta = delta_size * (-mouse_fraction);
let delta = delta_size * (DVec2::splat(0.5) - mouse_fraction);
NavigationMessage::TranslateCanvas { delta }.into()
}

View file

@ -282,7 +282,7 @@ impl<'a> ModifyInputsContext<'a> {
};
let metadata = output_node.metadata.clone();
let new_input = output_node.inputs[0].clone();
let new_input = output_node.inputs.first().cloned().filter(|input| input.as_node().is_some());
let node_id = generate_uuid();
output_node.metadata.position.x += 8;
@ -292,7 +292,7 @@ impl<'a> ModifyInputsContext<'a> {
warn!("Node type \"{name}\" doesn't exist");
return;
};
let mut new_document_node = node_type.to_document_node_default_inputs([Some(new_input)], metadata);
let mut new_document_node = node_type.to_document_node_default_inputs([new_input], metadata);
update_input(&mut new_document_node.inputs, node_id, self.document_metadata);
self.network.nodes.insert(node_id, new_document_node);
}
@ -575,7 +575,10 @@ impl MessageHandler<GraphOperationMessage, (&mut Document, &mut NodeGraphMessage
transform_in,
skip_rerender,
} => {
let parent_transform = document.metadata.document_to_viewport * document.multiply_transforms(&layer[..layer.len() - 1]).unwrap_or_default();
let layer_identifier = LayerNodeIdentifier::new(*layer.last().unwrap(), &document.document_network);
let parent_transform = document
.metadata
.transform_to_viewport(layer_identifier.parent(&document.metadata).unwrap_or(LayerNodeIdentifier::ROOT));
let bounds = LayerBounds::new(document, &layer);
if let Some(mut modify_inputs) = ModifyInputsContext::new_layer(&layer, document, node_graph, responses) {
modify_inputs.transform_change(transform, transform_in, parent_transform, bounds, skip_rerender);
@ -597,8 +600,12 @@ impl MessageHandler<GraphOperationMessage, (&mut Document, &mut NodeGraphMessage
transform_in,
skip_rerender,
} => {
let parent_transform = document.metadata.document_to_viewport * document.multiply_transforms(&layer[..layer.len() - 1]).unwrap_or_default();
let current_transform = Some(document.metadata.transform_to_viewport(LayerNodeIdentifier::new(*layer.last().unwrap(), &document.document_network)));
let layer_identifier = LayerNodeIdentifier::new(*layer.last().unwrap(), &document.document_network);
let parent_transform = document
.metadata
.transform_to_viewport(layer_identifier.parent(&document.metadata).unwrap_or(LayerNodeIdentifier::ROOT));
let current_transform = Some(document.metadata.transform_to_viewport(layer_identifier));
let bounds = LayerBounds::new(document, &layer);
if let Some(mut modify_inputs) = ModifyInputsContext::new_layer(&layer, document, node_graph, responses) {
modify_inputs.transform_set(transform, transform_in, parent_transform, current_transform, bounds, skip_rerender);

View file

@ -34,7 +34,7 @@ impl OriginalTransforms {
match self {
OriginalTransforms::Layer(layer_map) => {
for &layer in selected {
layer_map.entry(layer).or_insert_with(|| document.metadata.transform_to_document(layer));
layer_map.entry(layer).or_insert_with(|| document.metadata.local_transform(layer));
}
}
OriginalTransforms::Path(path_map) => {
@ -349,7 +349,7 @@ impl<'a> Selected<'a> {
let xy_summation = self
.selected
.iter()
.filter_map(|&layer| graph_modification_utils::get_viewport_pivot(layer, self.document))
.map(|&layer| graph_modification_utils::get_viewport_pivot(layer, self.document))
.reduce(|a, b| a + b)
.unwrap_or_default();
@ -366,6 +366,40 @@ impl<'a> Selected<'a> {
(min + max) / 2.
}
fn transform_layer(document: &Document, layer: LayerNodeIdentifier, original_transform: Option<&DAffine2>, transformation: DAffine2, responses: &mut VecDeque<Message>) {
let Some(&original_transform) = original_transform else { return };
let parent = layer.parent(&document.metadata);
let to = parent.map(|parent| document.metadata.transform_to_viewport(parent)).unwrap_or(document.metadata.document_to_viewport);
let new = to.inverse() * transformation * to * original_transform;
responses.add(GraphOperationMessage::TransformSet {
layer: layer.to_path(),
transform: new,
transform_in: TransformIn::Local,
skip_rerender: false,
});
}
fn transform_path(document: &Document, layer: LayerNodeIdentifier, initial_points: Option<&Vec<(ManipulatorPointId, DVec2)>>, transformation: DAffine2, responses: &mut VecDeque<Message>) {
let viewspace = document.metadata.transform_to_viewport(layer);
let layerspace_rotation = viewspace.inverse() * transformation;
let Some(initial_points) = initial_points else {
return;
};
for (point_id, position) in initial_points {
let viewport_point = viewspace.transform_point2(*position);
let new_pos_viewport = layerspace_rotation.transform_point2(viewport_point);
let point = *point_id;
let position = new_pos_viewport;
responses.add(GraphOperationMessage::Vector {
layer: layer.to_path(),
modification: VectorDataModification::SetManipulatorPosition { point, position },
});
}
}
pub fn update_transforms(&mut self, delta: DAffine2) {
if !self.selected.is_empty() {
let pivot = DAffine2::from_translation(*self.pivot);
@ -374,56 +408,13 @@ impl<'a> Selected<'a> {
// TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481
for layer_ancestors in self.document.metadata.shallowest_unique_layers(self.selected.iter().copied()) {
let layer = *layer_ancestors.last().unwrap();
let parent = layer.parent(&self.document.metadata);
if *self.tool_type == ToolType::Select {
let original_layer_transforms = match self.original_transforms {
OriginalTransforms::Layer(layer_map) => *layer_map.get(&layer).unwrap(),
OriginalTransforms::Path(_path_map) => {
warn!("Found Path variant in original_transforms, returning identity transform for layer {layer:?}");
DAffine2::IDENTITY
}
};
let to = parent
.map(|parent| self.document.metadata.transform_to_viewport(parent))
.unwrap_or(self.document.metadata.document_to_viewport);
let new = to.inverse() * transformation * to * original_layer_transforms;
self.responses.add(GraphOperationMessage::TransformSet {
layer: layer.to_path(),
transform: new,
transform_in: TransformIn::Local,
skip_rerender: false,
});
match &self.original_transforms {
OriginalTransforms::Layer(layer_transforms) => Self::transform_layer(self.document, layer, layer_transforms.get(&layer), transformation, self.responses),
OriginalTransforms::Path(path_transforms) => Self::transform_path(self.document, layer, path_transforms.get(&layer), transformation, self.responses),
}
if *self.tool_type == ToolType::Path {
let viewspace = self.document.metadata.transform_to_viewport(layer);
let layerspace_rotation = viewspace.inverse() * transformation;
let initial_points = match self.original_transforms {
OriginalTransforms::Layer(_layer_map) => {
warn!("Found Layer variant in original_transforms when Path wanted, returning identity transform for layer");
None
}
OriginalTransforms::Path(path_map) => path_map.get(&layer),
};
let Some(original) = initial_points else {
warn!("Initial Points empty, it should not be possible to reach here without points");
continue;
};
for (point_id, position) in original {
let viewport_point = viewspace.transform_point2(*position);
let new_pos_viewport = layerspace_rotation.transform_point2(viewport_point);
let point = *point_id;
let position = new_pos_viewport;
self.responses.add(GraphOperationMessage::Vector {
layer: layer.to_path(),
modification: VectorDataModification::SetManipulatorPosition { point, position },
});
}
}
self.responses.add(BroadcastEvent::DocumentIsDirty);
}
self.responses.add(BroadcastEvent::DocumentIsDirty);
}
}

View file

@ -77,14 +77,16 @@ pub fn get_pivot(layer: LayerNodeIdentifier, document: &Document) -> Option<DVec
}
}
pub fn get_document_pivot(layer: LayerNodeIdentifier, document: &Document) -> Option<DVec2> {
pub fn get_document_pivot(layer: LayerNodeIdentifier, document: &Document) -> DVec2 {
let [min, max] = document.metadata.nonzero_bounding_box(layer);
get_pivot(layer, document).map(|pivot| document.metadata.transform_to_document(layer).transform_point2(min + (max - min) * pivot))
let pivot = get_pivot(layer, document).unwrap_or(DVec2::splat(0.5));
document.metadata.transform_to_document(layer).transform_point2(min + (max - min) * pivot)
}
pub fn get_viewport_pivot(layer: LayerNodeIdentifier, document: &Document) -> Option<DVec2> {
pub fn get_viewport_pivot(layer: LayerNodeIdentifier, document: &Document) -> DVec2 {
let [min, max] = document.metadata.nonzero_bounding_box(layer);
get_pivot(layer, document).map(|pivot| document.metadata.transform_to_viewport(layer).transform_point2(min + (max - min) * pivot))
let pivot = get_pivot(layer, document).unwrap_or(DVec2::splat(0.5));
document.metadata.transform_to_viewport(layer).transform_point2(min + (max - min) * pivot)
}
/// Get the currently mirrored handles for a particular layer from the shape node

View file

@ -65,17 +65,16 @@ impl Pivot {
// If just one layer is selected we can use its inner transform (as it accounts for rotation)
if selected_layers_count == 1 {
if let Some(normalized_pivot) = graph_modification_utils::get_pivot(first, &document.document_legacy) {
self.normalized_pivot = normalized_pivot;
self.transform_from_normalized = Self::get_layer_pivot_transform(first, document);
self.pivot = Some(self.transform_from_normalized.transform_point2(normalized_pivot));
}
let normalized_pivot = graph_modification_utils::get_pivot(first, &document.document_legacy).unwrap_or(DVec2::splat(0.5));
self.normalized_pivot = normalized_pivot;
self.transform_from_normalized = Self::get_layer_pivot_transform(first, document);
self.pivot = Some(self.transform_from_normalized.transform_point2(normalized_pivot));
} else {
// If more than one layer is selected we use the AABB with the mean of the pivots
let xy_summation = document
.document_legacy
.selected_visible_layers()
.filter_map(|layer| graph_modification_utils::get_viewport_pivot(layer, &document.document_legacy))
.map(|layer| graph_modification_utils::get_viewport_pivot(layer, &document.document_legacy))
.reduce(|a, b| a + b)
.unwrap_or_default();

View file

@ -1,13 +1,13 @@
use super::tool_prelude::*;
use crate::messages::portfolio::document::node_graph::transform_utils::get_current_transform;
use crate::messages::portfolio::document::node_graph::resolve_document_node_type;
use crate::messages::portfolio::document::node_graph::transform_utils::{get_current_normalized_pivot, get_current_transform};
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
use crate::messages::tool::common_functionality::graph_modification_utils;
use document_legacy::layers::layer_layer::CachedOutputData;
use document_legacy::LayerId;
use document_legacy::document_metadata::LayerNodeIdentifier;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeInput, NodeNetwork};
use graphene_core::raster::{BlendMode, ImageFrame};
use graph_craft::document::{DocumentNodeMetadata, NodeInput};
use graphene_core::raster::BlendMode;
use graphene_core::uuid::generate_uuid;
use graphene_core::vector::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle};
use graphene_core::Color;
@ -289,20 +289,23 @@ impl ToolTransition for BrushTool {
#[derive(Clone, Debug, Default)]
struct BrushToolData {
strokes: Vec<BrushStroke>,
layer_path: Vec<LayerId>,
layer: Option<LayerNodeIdentifier>,
transform: DAffine2,
}
impl BrushToolData {
fn load_existing_strokes(&mut self, document: &DocumentMessageHandler) -> Option<&Vec<LayerId>> {
fn load_existing_strokes(&mut self, document: &DocumentMessageHandler) -> Option<LayerNodeIdentifier> {
self.transform = DAffine2::IDENTITY;
if document.selected_layers().count() != 1 {
if document.metadata().selected_layers().count() != 1 {
return None;
}
self.layer_path = document.selected_layers().next()?.to_vec();
let layer = document.document_legacy.layer(&self.layer_path).ok().and_then(|layer| layer.as_layer().ok())?;
let network = &layer.network;
for (node, _node_id) in network.primary_flow() {
let Some(layer) = document.metadata().selected_layers().next() else {
return None;
};
self.layer = Some(layer);
for (node, node_id) in document.network().primary_flow_from_node(Some(layer.to_node())) {
if node.name == "Brush" {
let points_input = node.inputs.get(2)?;
let NodeInput::Value {
@ -314,21 +317,22 @@ impl BrushToolData {
};
self.strokes = strokes.clone();
return Some(&self.layer_path);
return Some(layer);
} else if node.name == "Transform" {
self.transform = get_current_transform(&node.inputs) * self.transform;
let upstream = document.metadata().upstream_transform(node_id);
let pivot = DAffine2::from_translation(upstream.transform_point2(get_current_normalized_pivot(&node.inputs)));
self.transform = pivot * get_current_transform(&node.inputs) * pivot.inverse() * self.transform;
}
}
self.transform = DAffine2::IDENTITY;
matches!(layer.cached_output_data, CachedOutputData::BlobURL(_) | CachedOutputData::SurfaceId(_)).then_some(&self.layer_path)
None
}
fn update_strokes(&self, responses: &mut VecDeque<Message>) {
let layer = self.layer_path.clone();
let Some(layer) = self.layer else { return };
let strokes = self.strokes.clone();
responses.add(GraphOperationMessage::Brush { layer, strokes });
responses.add(GraphOperationMessage::Brush { layer: layer.to_path(), strokes });
}
}
@ -341,27 +345,27 @@ impl Fsm for BrushToolFsmState {
document, global_tool_data, input, ..
} = tool_action_data;
let document_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
let layer_position = tool_data.transform.inverse().transform_point2(document_position);
let ToolMessage::Brush(event) = event else {
return self;
};
match (self, event) {
(BrushToolFsmState::Ready, BrushToolMessage::DragStart) => {
responses.add(DocumentMessage::StartTransaction);
let layer_path = tool_data.load_existing_strokes(document);
let new_layer = layer_path.is_none();
if new_layer {
responses.add(DocumentMessage::DeselectAllLayers);
tool_data.layer_path = document.get_path_for_new_layer();
}
let layer_position = tool_data.transform.inverse().transform_point2(document_position);
let loaded_layer = tool_data.load_existing_strokes(document);
let layer = loaded_layer.unwrap_or_else(|| new_brush_layer(document, responses));
tool_data.layer = Some(layer);
let parent = layer.parent(document.metadata()).unwrap_or_default();
let parent_transform = document.metadata().transform_to_viewport(parent).inverse().transform_point2(input.mouse.position);
let layer_position = tool_data.transform.inverse().transform_point2(parent_transform);
let layer_document_scale = document.metadata().transform_to_document(parent) * tool_data.transform;
// TODO: Also scale it based on the input image ('Background' parameter).
// TODO: Resizing the input image results in a different brush size from the chosen diameter.
let layer_scale = 0.0001_f64 // Safety against division by zero
.max((tool_data.transform.matrix2 * glam::DVec2::X).length())
.max((tool_data.transform.matrix2 * glam::DVec2::Y).length());
.max((layer_document_scale.matrix2 * glam::DVec2::X).length())
.max((layer_document_scale.matrix2 * glam::DVec2::Y).length());
// Start a new stroke with a single sample
let blend_mode = match tool_options.draw_mode {
@ -381,17 +385,20 @@ impl Fsm for BrushToolFsmState {
},
});
if new_layer {
add_brush_render(tool_options, tool_data, responses);
}
tool_data.update_strokes(responses);
BrushToolFsmState::Drawing
}
(BrushToolFsmState::Drawing, BrushToolMessage::PointerMove) => {
if let Some(stroke) = tool_data.strokes.last_mut() {
stroke.trace.push(BrushInputSample { position: layer_position })
if let Some(layer) = tool_data.layer {
if let Some(stroke) = tool_data.strokes.last_mut() {
let parent = layer.parent(document.metadata()).unwrap_or_default();
let parent_position = document.metadata().transform_to_viewport(parent).inverse().transform_point2(input.mouse.position);
let layer_position = tool_data.transform.inverse().transform_point2(parent_position);
stroke.trace.push(BrushInputSample { position: layer_position })
}
}
tool_data.update_strokes(responses);
@ -438,11 +445,24 @@ impl Fsm for BrushToolFsmState {
}
}
fn add_brush_render(_tool_options: &BrushOptions, data: &BrushToolData, responses: &mut VecDeque<Message>) {
let mut network = NodeNetwork::default();
let output_node = network.push_output_node();
if let Some(node) = network.nodes.get_mut(&output_node) {
node.inputs.push(NodeInput::value(TaggedValue::ImageFrame(ImageFrame::empty()), true))
}
graph_modification_utils::new_custom_layer(network, data.layer_path.clone(), responses);
fn new_brush_layer(document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier {
responses.add(DocumentMessage::DeselectAllLayers);
let brush_node = resolve_document_node_type("Brush")
.expect("Brush node does not exist")
.to_document_node_default_inputs([], DocumentNodeMetadata::position((-8, 0)));
let cull_node = resolve_document_node_type("Cull")
.expect("Cull node does not exist")
.to_document_node_default_inputs([Some(NodeInput::node(1, 0))], DocumentNodeMetadata::default());
let id = generate_uuid();
responses.add(GraphOperationMessage::NewCustomLayer {
id,
nodes: HashMap::from([(1, brush_node), (0, cull_node)]),
parent: document.new_layer_parent(),
insert_index: -1,
});
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![id] });
LayerNodeIdentifier::new_unchecked(id)
}

View file

@ -2,6 +2,7 @@ use crate::consts::SLOWING_DIVISOR;
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
use crate::messages::portfolio::document::utility_types::transformation::{Axis, OriginalTransforms, Selected, TransformOperation, Typing};
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::utility_types::{ToolData, ToolType};
@ -23,10 +24,12 @@ pub struct TransformLayerMessageHandler {
original_transforms: OriginalTransforms,
pivot: DVec2,
}
impl TransformLayerMessageHandler {
pub fn is_transforming(&self) -> bool {
self.transform_operation != TransformOperation::None
}
pub fn hints(&self, responses: &mut VecDeque<Message>) {
let axis_constraint = match self.transform_operation {
TransformOperation::Grabbing(grabbing) => grabbing.constraint,
@ -40,18 +43,17 @@ impl TransformLayerMessageHandler {
type TransformData<'a> = (&'a DocumentMessageHandler, &'a InputPreprocessorMessageHandler, &'a ToolData, &'a mut ShapeState);
impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformLayerMessageHandler {
#[remain::check]
fn process_message(&mut self, message: TransformLayerMessage, responses: &mut VecDeque<Message>, (document, ipp, tool_data, shape_editor): TransformData) {
fn process_message(&mut self, message: TransformLayerMessage, responses: &mut VecDeque<Message>, (document, input, tool_data, shape_editor): TransformData) {
use TransformLayerMessage::*;
let using_path_tool = tool_data.active_tool_type == ToolType::Path;
let selected_layers = document.layer_metadata.iter().filter_map(|(layer_path, data)| data.selected.then_some(layer_path)).collect::<Vec<_>>();
let selected_layers_n = document.metadata().selected_layers().collect::<Vec<_>>();
let selected_layers = document.metadata().selected_layers().collect::<Vec<_>>();
let mut selected = Selected::new(
&mut self.original_transforms,
&mut self.pivot,
&selected_layers_n,
&selected_layers,
responses,
&document.document_legacy,
Some(shape_editor),
@ -65,33 +67,26 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
}
if using_path_tool {
if let Ok(layer) = document.document_legacy.layer(selected_layers[0]) {
if let Some(vector_data) = layer.as_vector_data() {
*selected.original_transforms = OriginalTransforms::default();
let viewspace = &mut document.document_legacy.generate_transform_relative_to_viewport(selected_layers[0]).ok().unwrap_or_default();
if let Some(subpaths) = selected_layers.first().and_then(|&layer| graph_modification_utils::get_subpaths(layer, &document.document_legacy)) {
*selected.original_transforms = OriginalTransforms::default();
let viewspace = document.metadata().transform_to_viewport(selected_layers[0]);
let mut point_count: usize = 0;
let count_point = |position| {
point_count += 1;
position
};
let get_location = |point: &ManipulatorPointId| {
vector_data
.manipulator_from_id(point.group)
.and_then(|manipulator_group| point.manipulator_type.get_position(manipulator_group))
.map(|position| viewspace.transform_point2(position))
};
let points = shape_editor.selected_points();
let mut point_count: usize = 0;
let get_location = |point: &ManipulatorPointId| {
graph_modification_utils::get_manipulator_from_id(subpaths, point.group)
.and_then(|manipulator_group| point.manipulator_type.get_position(manipulator_group))
.map(|position| viewspace.transform_point2(position))
};
let points = shape_editor.selected_points();
*selected.pivot = points.filter_map(get_location).map(count_point).sum::<DVec2>() / point_count as f64;
}
*selected.pivot = points.filter_map(get_location).inspect(|_| point_count += 1).sum::<DVec2>() / point_count as f64;
}
} else {
*selected.pivot = selected.mean_average_of_pivots();
}
*mouse_position = ipp.mouse.position;
*start_mouse = ipp.mouse.position;
*mouse_position = input.mouse.position;
*start_mouse = input.mouse.position;
selected.original_transforms.clear();
};
@ -106,9 +101,7 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
responses.add(ToolMessage::UpdateHints);
responses.add(BroadcastEvent::DocumentIsDirty);
for layer_path in document.selected_layers() {
responses.add(DocumentMessage::InputFrameRasterizeRegionBelowLayer { layer_path: layer_path.to_vec() });
}
responses.add(NodeGraphMessage::RunDocumentGraph);
}
BeginGrab => {
if let TransformOperation::Grabbing(_) = self.transform_operation {
@ -175,9 +168,9 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
ConstrainX => self.transform_operation.constrain_axis(Axis::X, &mut selected, self.snap),
ConstrainY => self.transform_operation.constrain_axis(Axis::Y, &mut selected, self.snap),
PointerMove { slow_key, snap_key } => {
self.slow = ipp.keyboard.get(slow_key as usize);
self.slow = input.keyboard.get(slow_key as usize);
let new_snap = ipp.keyboard.get(snap_key as usize);
let new_snap = input.keyboard.get(snap_key as usize);
if new_snap != self.snap {
self.snap = new_snap;
let axis_constraint = match self.transform_operation {
@ -189,7 +182,7 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
}
if self.typing.digits.is_empty() {
let delta_pos = ipp.mouse.position - self.mouse_position;
let delta_pos = input.mouse.position - self.mouse_position;
match self.transform_operation {
TransformOperation::None => unreachable!(),
@ -201,7 +194,7 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
}
TransformOperation::Rotating(rotation) => {
let start_offset = *selected.pivot - self.mouse_position;
let end_offset = *selected.pivot - ipp.mouse.position;
let end_offset = *selected.pivot - input.mouse.position;
let angle = start_offset.angle_between(end_offset);
let change = if self.slow { angle / SLOWING_DIVISOR } else { angle };
@ -212,7 +205,7 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
TransformOperation::Scaling(scale) => {
let change = {
let previous_frame_dist = (self.mouse_position - *selected.pivot).length();
let current_frame_dist = (ipp.mouse.position - *selected.pivot).length();
let current_frame_dist = (input.mouse.position - *selected.pivot).length();
let start_transform_dist = (self.start_mouse - *selected.pivot).length();
(current_frame_dist - previous_frame_dist) / start_transform_dist
@ -225,7 +218,7 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
}
};
}
self.mouse_position = ipp.mouse.position;
self.mouse_position = input.mouse.position;
}
SelectionChanged => {
let target_layers = document.metadata().selected_layers().collect();

View file

@ -14,11 +14,12 @@ use graph_craft::graphene_compiler::Compiler;
use graph_craft::imaginate_input::ImaginatePreferences;
use graph_craft::{concrete, Type};
use graphene_core::application_io::{ApplicationIo, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
use graphene_core::raster::Image;
use graphene_core::raster::{Image, ImageFrame};
use graphene_core::renderer::{ClickTarget, GraphicElementRendered, SvgSegment, SvgSegmentList};
use graphene_core::text::FontCache;
use graphene_core::transform::{Footprint, Transform};
use graphene_core::vector::style::ViewMode;
use graphene_core::vector::VectorData;
use graphene_core::{Color, SurfaceFrame, SurfaceId};
use graphene_std::wasm_application_io::{WasmApplicationIo, WasmEditorApi};
@ -71,8 +72,7 @@ pub(crate) struct GenerationRequest {
generation_id: u64,
graph: NodeNetwork,
path: Vec<LayerId>,
transform: DAffine2,
viewport_resolution: UVec2,
render_config: RenderConfig,
}
pub(crate) struct GenerationResponse {
@ -146,12 +146,12 @@ impl NodeRuntime {
NodeRuntimeMessage::GenerationRequest(GenerationRequest {
generation_id,
graph,
transform,
render_config,
path,
viewport_resolution,
..
}) => {
let (result, monitor_nodes) = self.execute_network(&path, graph, transform, viewport_resolution).await;
let transform = render_config.viewport.transform;
let (result, monitor_nodes) = self.execute_network(&path, graph, render_config).await;
let mut responses = VecDeque::new();
if let Some(ref monitor_nodes) = monitor_nodes {
self.update_thumbnails(&path, monitor_nodes, &mut responses);
@ -173,7 +173,7 @@ impl NodeRuntime {
}
}
async fn execute_network<'a>(&'a mut self, path: &[LayerId], graph: NodeNetwork, transform: DAffine2, viewport_resolution: UVec2) -> (Result<TaggedValue, String>, Option<MonitorNodes>) {
async fn execute_network<'a>(&'a mut self, path: &[LayerId], graph: NodeNetwork, render_config: RenderConfig) -> (Result<TaggedValue, String>, Option<MonitorNodes>) {
if self.wasm_io.is_none() {
self.wasm_io = Some(WasmApplicationIo::new().await);
}
@ -182,17 +182,7 @@ impl NodeRuntime {
application_io: self.wasm_io.as_ref().unwrap(),
node_graph_message_sender: &self.sender,
imaginate_preferences: &self.imaginate_preferences,
render_config: RenderConfig {
viewport: Footprint {
transform,
resolution: viewport_resolution,
..Default::default()
},
#[cfg(any(feature = "resvg", feature = "vello"))]
export_format: graphene_core::application_io::ExportFormat::Canvas,
#[cfg(not(any(feature = "resvg", feature = "vello")))]
export_format: graphene_core::application_io::ExportFormat::Svg,
},
render_config,
image_frame: None,
};
@ -325,11 +315,19 @@ impl NodeRuntime {
warn!("Failed to introspect monitor node for upstream transforms");
continue;
};
let Some(graphic_element_data) = value.downcast_ref::<graphene_core::vector::VectorData>() else {
warn!("Failed to downcast transform input to vector data");
let Some(transform) = value
.downcast_ref::<graphene_core::memo::IORecord<Footprint, VectorData>>()
.map(|vector_data| vector_data.output.transform())
.or_else(|| {
value
.downcast_ref::<graphene_core::memo::IORecord<Footprint, ImageFrame<Color>>>()
.map(|image| image.output.transform())
})
else {
warn!("Failed to downcast transform input");
continue;
};
self.upstream_transforms.insert(node_id, graphic_element_data.transform());
self.upstream_transforms.insert(node_id, transform);
}
}
}
@ -399,14 +397,13 @@ impl Default for NodeGraphExecutor {
impl NodeGraphExecutor {
/// Execute the network by flattening it and creating a borrow stack.
fn queue_execution(&self, network: NodeNetwork, layer_path: Vec<LayerId>, transform: DAffine2, viewport_resolution: UVec2) -> u64 {
fn queue_execution(&self, network: NodeNetwork, layer_path: Vec<LayerId>, render_config: RenderConfig) -> u64 {
let generation_id = generate_uuid();
let request = GenerationRequest {
path: layer_path,
graph: network,
generation_id,
transform,
viewport_resolution,
render_config,
};
self.sender.send(NodeRuntimeMessage::GenerationRequest(request)).expect("Failed to send generation request");
@ -499,11 +496,21 @@ impl NodeGraphExecutor {
layer_layer.network.clone()
};
// Construct the input image frame
let document_transform = document.document_legacy.metadata.document_to_viewport;
let render_config = RenderConfig {
viewport: Footprint {
transform: document.document_legacy.metadata.document_to_viewport,
resolution: viewport_resolution,
..Default::default()
},
#[cfg(any(feature = "resvg", feature = "vello"))]
export_format: graphene_core::application_io::ExportFormat::Canvas,
#[cfg(not(any(feature = "resvg", feature = "vello")))]
export_format: graphene_core::application_io::ExportFormat::Svg,
view_mode: document.view_mode,
};
// Execute the node graph
let generation_id = self.queue_execution(network, layer_path.clone(), document_transform, viewport_resolution);
let generation_id = self.queue_execution(network, layer_path.clone(), render_config);
self.futures.insert(generation_id, ExecutionContext { layer_path });
@ -542,7 +549,9 @@ impl NodeGraphExecutor {
.to_string(),
tooltip: format!("Layer id: {node_id}"),
visible: !document.document_network.disabled.contains(&layer.to_node()),
layer_type: if document.metadata.is_folder(layer) {
layer_type: if document.metadata.is_artboard(layer) {
LayerDataTypeDiscriminant::Artboard
} else if document.metadata.is_folder(layer) {
LayerDataTypeDiscriminant::Folder
} else {
LayerDataTypeDiscriminant::Layer

View file

Before

Width:  |  Height:  |  Size: 267 B

After

Width:  |  Height:  |  Size: 267 B

Before After
Before After

View file

@ -4,14 +4,8 @@
import { beginDraggingElement } from "@graphite/io-managers/drag";
import { platformIsMac } from "@graphite/utility-functions/platform";
import type { Editor } from "@graphite/wasm-communication/editor";
import {
type LayerPanelEntry,
defaultWidgetLayout,
patchWidgetLayout,
UpdateDocumentLayerDetails,
UpdateDocumentLayerTreeStructureJs,
UpdateLayerTreeOptionsLayout,
} from "@graphite/wasm-communication/messages";
import { defaultWidgetLayout, patchWidgetLayout, UpdateDocumentLayerDetails, UpdateDocumentLayerTreeStructureJs, UpdateLayerTreeOptionsLayout } from "@graphite/wasm-communication/messages";
import type { LayerType, LayerPanelEntry } from "@graphite/wasm-communication/messages";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
@ -152,6 +146,10 @@
editor.instance.deselectAllLayers();
}
function isGroupOrArtboard(layerType: LayerType) {
return layerType === "Folder" || layerType === "Artboard";
}
function calculateDragIndex(tree: LayoutCol, clientY: number, select?: () => void): DraggingData {
const treeChildren = tree.div()?.children;
const treeOffset = tree.div()?.getBoundingClientRect().top;
@ -195,9 +193,9 @@
}
// Inserting below current row
else if (distance > -closest && distance > -RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT && distance < 0) {
insertFolder = layer.layerType === "Folder" ? layer.path : layer.path.slice(0, layer.path.length - 1);
insertIndex = layer.layerType === "Folder" ? 0 : folderIndex + 1;
highlightFolder = layer.layerType === "Folder";
insertFolder = isGroupOrArtboard(layer.layerType) ? layer.path : layer.path.slice(0, layer.path.length - 1);
insertIndex = isGroupOrArtboard(layer.layerType) ? 0 : folderIndex + 1;
highlightFolder = isGroupOrArtboard("Folder");
closest = -distance;
markerHeight = index === treeChildren.length - 1 ? rect.bottom - INSERT_MARK_OFFSET : rect.bottom;
}
@ -338,7 +336,7 @@
<div class="indent" style:margin-left={layerIndent(listing.entry)} />
{#if listing.entry.layerType === "Folder"}
{#if isGroupOrArtboard(listing.entry.layerType)}
<button class="expand-arrow" class:expanded={listing.entry.layerMetadata.expanded} on:click|stopPropagation={() => handleExpandArrowClick(listing.entry.path)} tabindex="0" />
{/if}
<LayoutRow

View file

@ -93,6 +93,7 @@ import AlignLeft from "@graphite-frontend/assets/icon-16px-solid/align-left.svg"
import AlignRight from "@graphite-frontend/assets/icon-16px-solid/align-right.svg";
import AlignTop from "@graphite-frontend/assets/icon-16px-solid/align-top.svg";
import AlignVerticalCenter from "@graphite-frontend/assets/icon-16px-solid/align-vertical-center.svg";
import Artboard from "@graphite-frontend/assets/icon-16px-solid/artboard.svg";
import BooleanDifference from "@graphite-frontend/assets/icon-16px-solid/boolean-difference.svg";
import BooleanIntersect from "@graphite-frontend/assets/icon-16px-solid/boolean-intersect.svg";
import BooleanSubtractBack from "@graphite-frontend/assets/icon-16px-solid/boolean-subtract-back.svg";
@ -118,7 +119,6 @@ import IconsGrid from "@graphite-frontend/assets/icon-16px-solid/icons-grid.svg"
import Image from "@graphite-frontend/assets/icon-16px-solid/image.svg";
import Layer from "@graphite-frontend/assets/icon-16px-solid/layer.svg";
import License from "@graphite-frontend/assets/icon-16px-solid/license.svg";
import NodeArtboard from "@graphite-frontend/assets/icon-16px-solid/node-artboard.svg";
import NodeBlur from "@graphite-frontend/assets/icon-16px-solid/node-blur.svg";
import NodeBrushwork from "@graphite-frontend/assets/icon-16px-solid/node-brushwork.svg";
import NodeColorCorrection from "@graphite-frontend/assets/icon-16px-solid/node-color-correction.svg";
@ -161,6 +161,7 @@ const SOLID_16PX = {
AlignRight: { svg: AlignRight, size: 16 },
AlignTop: { svg: AlignTop, size: 16 },
AlignVerticalCenter: { svg: AlignVerticalCenter, size: 16 },
Artboard: { svg: Artboard, size: 16 },
BooleanDifference: { svg: BooleanDifference, size: 16 },
BooleanIntersect: { svg: BooleanIntersect, size: 16 },
BooleanSubtractBack: { svg: BooleanSubtractBack, size: 16 },
@ -186,7 +187,6 @@ const SOLID_16PX = {
Image: { svg: Image, size: 16 },
Layer: { svg: Layer, size: 16 },
License: { svg: License, size: 16 },
NodeArtboard: { svg: NodeArtboard, size: 16 },
NodeBlur: { svg: NodeBlur, size: 16 },
NodeBrushwork: { svg: NodeBrushwork, size: 16 },
NodeColorCorrection: { svg: NodeColorCorrection, size: 16 },

View file

@ -698,7 +698,7 @@ export class LayerMetadata {
selected!: boolean;
}
export type LayerType = "Folder" | "Layer";
export type LayerType = "Folder" | "Layer" | "Artboard";
export class ImaginateImageData {
readonly path!: BigUint64Array;

View file

@ -1,6 +1,7 @@
use crate::raster::ImageFrame;
use crate::text::FontCache;
use crate::transform::{Footprint, Transform, TransformMut};
use crate::vector::style::ViewMode;
use crate::{Color, Node};
use dyn_any::{StaticType, StaticTypeSized};
@ -151,6 +152,7 @@ pub enum ExportFormat {
pub struct RenderConfig {
pub viewport: Footprint,
pub export_format: ExportFormat,
pub view_mode: ViewMode,
}
pub struct EditorApi<'a, Io> {

View file

@ -284,6 +284,7 @@ impl GraphicElementRendered for Artboard {
"g",
|attributes| {
attributes.push("class", "artboard");
attributes.push("transform", format_transform_matrix(self.graphic_group.transform));
if self.clip {
let id = format!("artboard-{}", generate_uuid());
let selector = format!("url(#{id})");
@ -298,8 +299,15 @@ impl GraphicElementRendered for Artboard {
}
},
|render| {
let old_opacity = render.opacity;
render.opacity *= self.graphic_group.opacity;
// Contents
self.graphic_group.render_svg(render, render_params);
for element in self.graphic_group.iter() {
render.blend_mode = element.blend_mode;
element.graphic_element_data.render_svg(render, render_params);
}
render.opacity = old_opacity;
},
);
}

View file

@ -349,6 +349,13 @@ impl<P: Hash + Pixel> Hash for ImageFrame<P> {
}
}
impl<P: Pixel> ImageFrame<P> {
/// Compute the pivot in local space with the current transform applied
pub fn local_pivot(&self, normalized_pivot: DVec2) -> DVec2 {
self.transform.transform_point2(normalized_pivot)
}
}
/* This does not work because of missing specialization
* so we have to manually implement this for now
impl<S: Into<P> + Pixel, P: Pixel> From<Image<S>> for Image<P> {

View file

@ -37,11 +37,17 @@ impl<P: Pixel> Transform for ImageFrame<P> {
fn transform(&self) -> DAffine2 {
self.transform
}
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
self.local_pivot(pivot)
}
}
impl<P: Pixel> Transform for &ImageFrame<P> {
fn transform(&self) -> DAffine2 {
self.transform
}
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
(*self).local_pivot(pivot)
}
}
impl<P: Pixel> TransformMut for ImageFrame<P> {
fn transform_mut(&mut self) -> &mut DAffine2 {
@ -69,8 +75,8 @@ impl Transform for GraphicElementData {
GraphicElementData::VectorShape(vector_shape) => vector_shape.transform(),
GraphicElementData::ImageFrame(image_frame) => image_frame.transform(),
GraphicElementData::Text(_) => todo!("Transform of text"),
GraphicElementData::GraphicGroup(_graphic_group) => DAffine2::IDENTITY,
GraphicElementData::Artboard(_artboard) => DAffine2::IDENTITY,
GraphicElementData::GraphicGroup(graphic_group) => graphic_group.transform(),
GraphicElementData::Artboard(artboard) => artboard.graphic_group.transform(),
}
}
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
@ -78,23 +84,17 @@ impl Transform for GraphicElementData {
GraphicElementData::VectorShape(vector_shape) => vector_shape.local_pivot(pivot),
GraphicElementData::ImageFrame(image_frame) => image_frame.local_pivot(pivot),
GraphicElementData::Text(_) => todo!("Transform of text"),
GraphicElementData::GraphicGroup(_graphic_group) => pivot,
GraphicElementData::Artboard(_artboard) => pivot,
GraphicElementData::GraphicGroup(graphic_group) => graphic_group.local_pivot(pivot),
GraphicElementData::Artboard(artboard) => artboard.graphic_group.local_pivot(pivot),
}
}
fn decompose_scale(&self) -> DVec2 {
let standard = || {
DVec2::new(
self.transform().transform_vector2((1., 0.).into()).length(),
self.transform().transform_vector2((0., 1.).into()).length(),
)
};
match self {
GraphicElementData::VectorShape(vector_shape) => vector_shape.decompose_scale(),
GraphicElementData::ImageFrame(image_frame) => image_frame.decompose_scale(),
GraphicElementData::Text(_) => todo!("Transform of text"),
GraphicElementData::GraphicGroup(_graphic_group) => standard(),
GraphicElementData::Artboard(_artboard) => standard(),
GraphicElementData::GraphicGroup(graphic_group) => graphic_group.decompose_scale(),
GraphicElementData::Artboard(artboard) => artboard.graphic_group.decompose_scale(),
}
}
}
@ -104,8 +104,8 @@ impl TransformMut for GraphicElementData {
GraphicElementData::VectorShape(vector_shape) => vector_shape.transform_mut(),
GraphicElementData::ImageFrame(image_frame) => image_frame.transform_mut(),
GraphicElementData::Text(_) => todo!("Transform of text"),
GraphicElementData::GraphicGroup(_graphic_group) => todo!("Mutable transform of graphic group"),
GraphicElementData::Artboard(_artboard) => todo!("Mutable transform of artboard"),
GraphicElementData::GraphicGroup(graphic_group) => graphic_group.transform_mut(),
GraphicElementData::Artboard(artboard) => artboard.graphic_group.transform_mut(),
}
}
}

View file

@ -6,7 +6,6 @@ use graphene_core::application_io::{ApplicationError, ApplicationIo, ExportForma
use graphene_core::raster::Image;
use graphene_core::renderer::{GraphicElementRendered, RenderParams, SvgRender};
use graphene_core::transform::Footprint;
use graphene_core::vector::style::ViewMode;
use graphene_core::Color;
use graphene_core::{
raster::{color::SRGBA8, ImageFrame},
@ -285,7 +284,6 @@ fn decode_image_node<'a: 'input>(data: Arc<[u8]>) -> ImageFrame<Color> {
image
}
pub use graph_craft::document::value::RenderOutput;
pub struct RenderNode<Data, Surface, Parameter> {
data: Data,
surface_handle: Surface,
@ -365,7 +363,7 @@ where
fn eval(&'input self, editor: WasmEditorApi<'a>) -> Self::Output {
Box::pin(async move {
let footprint = editor.render_config.viewport;
let render_params = RenderParams::new(ViewMode::Normal, graphene_core::renderer::ImageRenderMode::Base64, None, false);
let render_params = RenderParams::new(editor.render_config.view_mode, graphene_core::renderer::ImageRenderMode::Base64, None, false);
let output_format = editor.render_config.export_format;
match output_format {
@ -393,7 +391,7 @@ where
use graphene_core::renderer::ImageRenderMode;
let footprint = editor.render_config.viewport;
let render_params = RenderParams::new(ViewMode::Normal, ImageRenderMode::Base64, None, false);
let render_params = RenderParams::new(editor.render_config.view_mode, ImageRenderMode::Base64, None, false);
let output_format = editor.render_config.export_format;
match output_format {

View file

@ -800,6 +800,7 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
},
)],
register_node!(graphene_core::transform::CullNode<_>, input: Footprint, params: [Artboard]),
register_node!(graphene_core::transform::CullNode<_>, input: Footprint, params: [ImageFrame<Color>]),
vec![(
NodeIdentifier::new("graphene_core::transform::CullNode<_>"),
|args| {