mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-30 18:48:12 +08:00
Integrate Stable Diffusion with the Imaginate layer (#784)
* Add AI Artist layer * WIP add a button to download the rendered folder under an AI Artist layer * Successfully download the correct image * Break out image downloading JS into helper function * Change file download from using data URLs to blob URLs * WIP rasterize to blob * Remove dimensions from AI Artist layer * Successfully draw rasterized image on layer after calculation * Working txt2img generation based on user prompt * Add img2img and the main parameters * Fix ability to rasterize multi-depth documents with blob URL images by switching them to base64 * Fix test * Rasterize with artboard background color * Allow aspect ratio stretch of AI Artist images * Add automatic resolution choosing * Add a terminate button, and make the lifecycle more robust * Add negative prompt * Add range bounds for parameter inputs * Add seed * Add tiling and restore faces * Add server status check, server hostname customization, and resizing layer to fit AI Artist resolution * Fix background color of infinite canvas rasterization * Escape prompt text sent in the JSON * Revoke blob URLs when cleared/replaced to reduce memory leak * Fix welcome screen logo color * Add PreferencesMessageHandler * Add persistent storage of preferences * Fix crash introduced in previous commit when moving mouse on page load * Add tooltips to the AI Artist layer properties * Integrate AI Artist tool into the raster section of the tool shelf * Add a refresh button to the connection status * Fix crash when generating and switching to a different document tab * Add persistent image storage to AI Artist layers and fix duplication bugs * Add a generate with random seed button * Simplify and standardize message names * Majorly improve robustness of networking code * Fix race condition causing default server hostname to show disconnected when app loads with AI Artist layer selected (probably, not confirmed fixed) * Clean up messages and function calls by changing arguments into structs * Update API to more recent server commit * Add support for picking the sampling method * Add machinery for filtering selected layers with type * Replace placeholder button icons * Improve the random icon by tilting the dice * Use selected_layers() instead of repeating that code * Fix borrow error * Change message flow in progress towards fixing #797 * Allow loading image on non-active document (fixes #797) * Reduce code duplication with rasterization * Add AI Artist tool and layer icons, and remove ugly node layer icon style * Rename "AI Artist" codename to "Imaginate" feature name Co-authored-by: otdavies <oliver@psyfer.io> Co-authored-by: 0hypercube <0hypercube@gmail.com>
This commit is contained in:
co-authored by
otdavies
0hypercube
parent
562217015d
commit
fe1a03fac7
@@ -85,7 +85,7 @@ impl MessageHandler<ArtboardMessage, &FontCache> for ArtboardMessageHandler {
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
let render_data = RenderData::new(ViewMode::Normal, font_cache, None, false);
|
||||
let render_data = RenderData::new(ViewMode::Normal, font_cache, None);
|
||||
responses.push_back(
|
||||
FrontendMessage::UpdateDocumentArtboards {
|
||||
svg: self.artboards_graphene_document.render_root(render_data),
|
||||
|
||||
@@ -8,7 +8,6 @@ use graphene::layers::blend_mode::BlendMode;
|
||||
use graphene::layers::style::ViewMode;
|
||||
use graphene::LayerId;
|
||||
use graphene::Operation as DocumentOperation;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[remain::sorted]
|
||||
@@ -75,6 +74,9 @@ pub enum DocumentMessage {
|
||||
affected_folder_path: Vec<LayerId>,
|
||||
},
|
||||
GroupSelectedLayers,
|
||||
ImaginateClear,
|
||||
ImaginateGenerate,
|
||||
ImaginateTerminate,
|
||||
LayerChanged {
|
||||
affected_layer_path: Vec<LayerId>,
|
||||
},
|
||||
@@ -120,6 +122,12 @@ pub enum DocumentMessage {
|
||||
SetBlendModeForSelectedLayers {
|
||||
blend_mode: BlendMode,
|
||||
},
|
||||
SetImageBlobUrl {
|
||||
layer_path: Vec<LayerId>,
|
||||
blob_url: String,
|
||||
resolution: (f64, f64),
|
||||
document_id: u64,
|
||||
},
|
||||
SetLayerExpansion {
|
||||
layer_path: Vec<LayerId>,
|
||||
set_expanded: bool,
|
||||
@@ -140,7 +148,7 @@ pub enum DocumentMessage {
|
||||
SetSnapping {
|
||||
snap: bool,
|
||||
},
|
||||
SetTexboxEditability {
|
||||
SetTextboxEditability {
|
||||
path: Vec<LayerId>,
|
||||
editable: bool,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use super::utility_types::error::EditorError;
|
||||
use super::utility_types::misc::DocumentRenderMode;
|
||||
use crate::application::generate_uuid;
|
||||
use crate::consts::{ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR};
|
||||
use crate::messages::frontend::utility_types::ExportBounds;
|
||||
use crate::messages::frontend::utility_types::{FileType, FrontendImageData};
|
||||
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||
@@ -14,12 +16,14 @@ use crate::messages::portfolio::document::utility_types::layer_panel::{LayerMeta
|
||||
use crate::messages::portfolio::document::utility_types::misc::DocumentMode;
|
||||
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentSave, FlipAxis};
|
||||
use crate::messages::portfolio::document::utility_types::vectorize_layer_metadata;
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graphene::color::Color;
|
||||
use graphene::document::Document as GrapheneDocument;
|
||||
use graphene::document::{pick_layer_safe_imaginate_resolution, Document as GrapheneDocument};
|
||||
use graphene::layers::blend_mode::BlendMode;
|
||||
use graphene::layers::folder_layer::FolderLayer;
|
||||
use graphene::layers::imaginate_layer::{ImaginateBaseImage, ImaginateGenerationParameters, ImaginateStatus};
|
||||
use graphene::layers::layer_info::{LayerDataType, LayerDataTypeDiscriminant};
|
||||
use graphene::layers::style::{Fill, RenderData, ViewMode};
|
||||
use graphene::layers::text_layer::{Font, FontCache};
|
||||
@@ -87,16 +91,21 @@ impl Default for DocumentMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCache)> for DocumentMessageHandler {
|
||||
impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &PersistentData, &PreferencesMessageHandler)> for DocumentMessageHandler {
|
||||
#[remain::check]
|
||||
fn process_message(&mut self, message: DocumentMessage, (ipp, font_cache): (&InputPreprocessorMessageHandler, &FontCache), responses: &mut VecDeque<Message>) {
|
||||
fn process_message(
|
||||
&mut self,
|
||||
message: DocumentMessage,
|
||||
(document_id, ipp, persistent_data, preferences): (u64, &InputPreprocessorMessageHandler, &PersistentData, &PreferencesMessageHandler),
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
use DocumentMessage::*;
|
||||
|
||||
#[remain::sorted]
|
||||
match message {
|
||||
// Sub-messages
|
||||
#[remain::unsorted]
|
||||
DispatchOperation(op) => match self.graphene_document.handle_operation(*op, font_cache) {
|
||||
DispatchOperation(op) => match self.graphene_document.handle_operation(*op, &persistent_data.font_cache) {
|
||||
Ok(Some(document_responses)) => {
|
||||
for response in document_responses {
|
||||
match &response {
|
||||
@@ -130,7 +139,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
},
|
||||
#[remain::unsorted]
|
||||
Artboard(message) => {
|
||||
self.artboard_message_handler.process_message(message, font_cache, responses);
|
||||
self.artboard_message_handler.process_message(message, &persistent_data.font_cache, responses);
|
||||
}
|
||||
#[remain::unsorted]
|
||||
Navigation(message) => {
|
||||
@@ -138,25 +147,23 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
}
|
||||
#[remain::unsorted]
|
||||
Overlays(message) => {
|
||||
self.overlays_message_handler.process_message(message, (self.overlays_visible, font_cache, ipp), responses);
|
||||
self.overlays_message_handler
|
||||
.process_message(message, (self.overlays_visible, &persistent_data.font_cache, ipp), responses);
|
||||
}
|
||||
#[remain::unsorted]
|
||||
TransformLayer(message) => {
|
||||
self.transform_layer_handler
|
||||
.process_message(message, (&mut self.layer_metadata, &mut self.graphene_document, ipp, font_cache), responses);
|
||||
.process_message(message, (&mut self.layer_metadata, &mut self.graphene_document, ipp, &persistent_data.font_cache), responses);
|
||||
}
|
||||
#[remain::unsorted]
|
||||
PropertiesPanel(message) => {
|
||||
self.properties_panel_message_handler.process_message(
|
||||
message,
|
||||
PropertiesPanelMessageHandlerData {
|
||||
artwork_document: &self.graphene_document,
|
||||
artboard_document: &self.artboard_message_handler.artboards_graphene_document,
|
||||
selected_layers: &mut self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then(|| path.as_slice())),
|
||||
font_cache,
|
||||
},
|
||||
responses,
|
||||
);
|
||||
let properties_panel_message_handler_data = PropertiesPanelMessageHandlerData {
|
||||
artwork_document: &self.graphene_document,
|
||||
artboard_document: &self.artboard_message_handler.artboards_graphene_document,
|
||||
selected_layers: &mut self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then_some(path.as_slice())),
|
||||
};
|
||||
self.properties_panel_message_handler
|
||||
.process_message(message, (persistent_data, properties_panel_message_handler_data), responses);
|
||||
}
|
||||
|
||||
// Messages
|
||||
@@ -166,20 +173,20 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
}
|
||||
AddSelectedLayers { additional_layers } => {
|
||||
for layer_path in &additional_layers {
|
||||
responses.extend(self.select_layer(layer_path, font_cache));
|
||||
responses.extend(self.select_layer(layer_path, &persistent_data.font_cache));
|
||||
}
|
||||
|
||||
// TODO: Correctly update layer panel in clear_selection instead of here
|
||||
responses.push_back(FolderChanged { affected_folder_path: vec![] }.into());
|
||||
responses.push_back(BroadcastEvent::SelectionChanged.into());
|
||||
|
||||
self.update_layer_tree_options_bar_widgets(responses, font_cache);
|
||||
self.update_layer_tree_options_bar_widgets(responses, &persistent_data.font_cache);
|
||||
}
|
||||
AlignSelectedLayers { axis, aggregate } => {
|
||||
self.backup(responses);
|
||||
let (paths, boxes): (Vec<_>, Vec<_>) = self
|
||||
.selected_layers()
|
||||
.filter_map(|path| self.graphene_document.viewport_bounding_box(path, font_cache).ok()?.map(|b| (path, b)))
|
||||
.filter_map(|path| self.graphene_document.viewport_bounding_box(path, &persistent_data.font_cache).ok()?.map(|b| (path, b)))
|
||||
.unzip();
|
||||
|
||||
let axis = match axis {
|
||||
@@ -187,7 +194,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
AlignAxis::Y => DVec2::Y,
|
||||
};
|
||||
let lerp = |bbox: &[DVec2; 2]| bbox[0].lerp(bbox[1], 0.5);
|
||||
if let Some(combined_box) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers(), font_cache) {
|
||||
if let Some(combined_box) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers(), &persistent_data.font_cache) {
|
||||
let aggregated = match aggregate {
|
||||
AlignAggregate::Min => combined_box[0],
|
||||
AlignAggregate::Max => combined_box[1],
|
||||
@@ -308,21 +315,21 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
scale_factor,
|
||||
bounds,
|
||||
} => {
|
||||
// Allows the user's transform to be restored
|
||||
let old_transform = self.graphene_document.root.transform;
|
||||
// Reset the root's transform (required to avoid any rotation by the user)
|
||||
self.graphene_document.root.transform = DAffine2::IDENTITY;
|
||||
GrapheneDocument::mark_children_as_dirty(&mut self.graphene_document.root);
|
||||
|
||||
// Calculate the bounding box of the region to be exported
|
||||
use crate::messages::frontend::utility_types::ExportBounds;
|
||||
let bbox = match bounds {
|
||||
ExportBounds::AllArtwork => self.all_layer_bounds(font_cache),
|
||||
ExportBounds::Selection => self.selected_visible_layers_bounding_box(font_cache),
|
||||
ExportBounds::Artboard(id) => self.artboard_message_handler.artboards_graphene_document.layer(&[id]).ok().and_then(|layer| layer.aabb(font_cache)),
|
||||
let bounds = match bounds {
|
||||
ExportBounds::AllArtwork => self.all_layer_bounds(&persistent_data.font_cache),
|
||||
ExportBounds::Selection => self.selected_visible_layers_bounding_box(&persistent_data.font_cache),
|
||||
ExportBounds::Artboard(id) => self
|
||||
.artboard_message_handler
|
||||
.artboards_graphene_document
|
||||
.layer(&[id])
|
||||
.ok()
|
||||
.and_then(|layer| layer.aabb(&persistent_data.font_cache)),
|
||||
}
|
||||
.unwrap_or_default();
|
||||
let size = bbox[1] - bbox[0];
|
||||
let size = bounds[1] - bounds[0];
|
||||
|
||||
let document = self.render_document(bounds, persistent_data, DocumentRenderMode::Root);
|
||||
|
||||
let file_suffix = &format!(".{file_type:?}").to_lowercase();
|
||||
let name = match file_name.ends_with(FILE_SAVE_SUFFIX) {
|
||||
@@ -330,16 +337,6 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
false => file_name + file_suffix,
|
||||
};
|
||||
|
||||
let render_data = RenderData::new(ViewMode::Normal, font_cache, None, true);
|
||||
let rendered = self.graphene_document.render_root(render_data);
|
||||
let document = format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}" width="{}px" height="{}">{}{}</svg>"#,
|
||||
bbox[0].x, bbox[0].y, size.x, size.y, size.x, size.y, "\n", rendered
|
||||
);
|
||||
|
||||
self.graphene_document.root.transform = old_transform;
|
||||
GrapheneDocument::mark_children_as_dirty(&mut self.graphene_document.root);
|
||||
|
||||
if file_type == FileType::Svg {
|
||||
responses.push_back(FrontendMessage::TriggerFileDownload { document, name }.into());
|
||||
} else {
|
||||
@@ -354,7 +351,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
FlipAxis::X => DVec2::new(-1., 1.),
|
||||
FlipAxis::Y => DVec2::new(1., -1.),
|
||||
};
|
||||
if let Some([min, max]) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers(), font_cache) {
|
||||
if let Some([min, max]) = self.graphene_document.combined_viewport_bounding_box(self.selected_layers(), &persistent_data.font_cache) {
|
||||
let center = (max + min) / 2.;
|
||||
let bbox_trans = DAffine2::from_translation(-center);
|
||||
for path in self.selected_layers() {
|
||||
@@ -403,12 +400,73 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
ImaginateClear => {
|
||||
let mut selected_imaginate_layers = self.selected_layers_with_type(LayerDataTypeDiscriminant::Imaginate);
|
||||
// Get what is hopefully the only selected Imaginate layer
|
||||
let layer_path = selected_imaginate_layers.next();
|
||||
// Abort if we didn't have any Imaginate layer, or if there are additional ones also selected
|
||||
if layer_path.is_none() || selected_imaginate_layers.next().is_some() {
|
||||
return;
|
||||
}
|
||||
let layer_path = layer_path.unwrap();
|
||||
|
||||
let layer = self.graphene_document.layer(layer_path).expect("Clearing Imaginate image for invalid layer");
|
||||
let previous_blob_url = &layer.as_imaginate().unwrap().blob_url;
|
||||
|
||||
if let Some(url) = previous_blob_url {
|
||||
responses.push_back(FrontendMessage::TriggerRevokeBlobUrl { url: url.clone() }.into());
|
||||
}
|
||||
responses.push_back(DocumentOperation::ImaginateClear { path: layer_path.into() }.into());
|
||||
}
|
||||
ImaginateGenerate => {
|
||||
if let Some(message) = self.call_imaginate(document_id, preferences, persistent_data) {
|
||||
// TODO: Eventually remove this after a message system ordering architectural change
|
||||
// This message is a workaround for the fact that, when `imaginate.ts` calls...
|
||||
// `editor.instance.setImaginateGeneratingStatus(layerPath, 0, true);`
|
||||
// ...execution transfers from the Rust part of the call stack into the JS part of the call stack (before the Rust message queue is empty,
|
||||
// and there is a Properties panel refresh queued next). Then the JS calls that line shown above and enters the Rust part of the callstack
|
||||
// again, so it's gone through JS (user initiation) -> Rust (process the button press) -> JS (beginning server request) -> Rust (set
|
||||
// progress percentage to 0). As that call stack returns back from the Rust and back from the JS, it returns to the Rust and finishes
|
||||
// processing the queue. That's where it then processes the Properties panel refresh that sent the "Ready" or "Done" state that existed
|
||||
// before pressing the Generate button causing it to show "0%". So "Ready" or "Done" immediately overwrites the "0%". This block below,
|
||||
// therefore, adds a redundant call to set it to 0% progress so the message execution order ends with this as the final percentage shown
|
||||
// to the user.
|
||||
responses.push_back(
|
||||
DocumentOperation::ImaginateSetGeneratingStatus {
|
||||
path: self.selected_layers_with_type(LayerDataTypeDiscriminant::Imaginate).next().unwrap().to_vec(),
|
||||
percent: Some(0.),
|
||||
status: ImaginateStatus::Beginning,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
responses.push_back(message);
|
||||
}
|
||||
}
|
||||
ImaginateTerminate => {
|
||||
let hostname = preferences.imaginate_server_hostname.clone();
|
||||
|
||||
let layer_path = {
|
||||
let mut selected_imaginate_layers = self.selected_layers_with_type(LayerDataTypeDiscriminant::Imaginate);
|
||||
|
||||
// Get what is hopefully the only selected Imaginate layer
|
||||
match selected_imaginate_layers.next() {
|
||||
// Continue only if there are no additional Imaginate layers also selected
|
||||
Some(layer_path) if selected_imaginate_layers.next().is_none() => Some(layer_path.to_owned()),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(layer_path) = layer_path {
|
||||
responses.push_back(FrontendMessage::TriggerImaginateTerminate { document_id, layer_path, hostname }.into());
|
||||
}
|
||||
}
|
||||
LayerChanged { affected_layer_path } => {
|
||||
if let Ok(layer_entry) = self.layer_panel_entry(affected_layer_path.clone(), font_cache) {
|
||||
if let Ok(layer_entry) = self.layer_panel_entry(affected_layer_path.clone(), &persistent_data.font_cache) {
|
||||
responses.push_back(FrontendMessage::UpdateDocumentLayerDetails { data: layer_entry }.into());
|
||||
}
|
||||
responses.push_back(PropertiesPanelMessage::CheckSelectedWasUpdated { path: affected_layer_path }.into());
|
||||
self.update_layer_tree_options_bar_widgets(responses, font_cache);
|
||||
self.update_layer_tree_options_bar_widgets(responses, &persistent_data.font_cache);
|
||||
}
|
||||
MoveSelectedLayersTo {
|
||||
folder_path,
|
||||
@@ -466,6 +524,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
);
|
||||
responses.push_back(
|
||||
FrontendMessage::UpdateImageData {
|
||||
document_id,
|
||||
image_data: vec![FrontendImageData { path: path.clone(), image_data, mime }],
|
||||
}
|
||||
.into(),
|
||||
@@ -490,7 +549,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
}
|
||||
RenameLayer { layer_path, new_name } => responses.push_back(DocumentOperation::RenameLayer { layer_path, new_name }.into()),
|
||||
RenderDocument => {
|
||||
let render_data = RenderData::new(self.view_mode, font_cache, Some(ipp.document_bounds()), false);
|
||||
let render_data = RenderData::new(self.view_mode, &persistent_data.font_cache, Some(ipp.document_bounds()));
|
||||
responses.push_back(
|
||||
FrontendMessage::UpdateDocumentArtwork {
|
||||
svg: self.graphene_document.render_root(render_data),
|
||||
@@ -503,7 +562,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
let scale = 0.5 + ASYMPTOTIC_EFFECT + document_transform_scale * SCALE_EFFECT;
|
||||
let viewport_size = ipp.viewport_bounds.size();
|
||||
let viewport_mid = ipp.viewport_bounds.center();
|
||||
let [bounds1, bounds2] = self.document_bounds(font_cache).unwrap_or([viewport_mid; 2]);
|
||||
let [bounds1, bounds2] = self.document_bounds(&persistent_data.font_cache).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);
|
||||
@@ -621,17 +680,44 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
}
|
||||
SetBlendModeForSelectedLayers { blend_mode } => {
|
||||
self.backup(responses);
|
||||
for path in self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) {
|
||||
responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into());
|
||||
for path in self.selected_layers() {
|
||||
responses.push_back(DocumentOperation::SetLayerBlendMode { path: path.to_vec(), blend_mode }.into());
|
||||
}
|
||||
}
|
||||
SetImageBlobUrl {
|
||||
layer_path,
|
||||
blob_url,
|
||||
resolution,
|
||||
document_id,
|
||||
} => {
|
||||
let layer = self.graphene_document.layer(&layer_path).expect("Setting blob URL for invalid layer");
|
||||
|
||||
// Revoke the old blob URL
|
||||
match &layer.data {
|
||||
LayerDataType::Imaginate(imaginate) => {
|
||||
if let Some(url) = &imaginate.blob_url {
|
||||
responses.push_back(FrontendMessage::TriggerRevokeBlobUrl { url: url.clone() }.into());
|
||||
}
|
||||
}
|
||||
LayerDataType::Image(_) => {}
|
||||
other => panic!("Setting blob URL for invalid layer type, which must be an `Imaginate` or `Image`. Found: `{:?}`", other),
|
||||
}
|
||||
|
||||
responses.push_back(
|
||||
PortfolioMessage::DocumentPassMessage {
|
||||
document_id,
|
||||
message: DocumentOperation::SetLayerBlobUrl { layer_path, blob_url, resolution }.into(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
SetLayerExpansion { layer_path, set_expanded } => {
|
||||
self.layer_metadata_mut(&layer_path).expanded = set_expanded;
|
||||
responses.push_back(DocumentStructureChanged.into());
|
||||
responses.push_back(LayerChanged { affected_layer_path: layer_path }.into())
|
||||
}
|
||||
SetLayerName { layer_path, name } => {
|
||||
if let Some(layer) = self.layer_panel_entry_from_path(&layer_path, font_cache) {
|
||||
if let Some(layer) = self.layer_panel_entry_from_path(&layer_path, &persistent_data.font_cache) {
|
||||
// Only save the history state if the name actually changed to something different
|
||||
if layer.name != name {
|
||||
self.backup(responses);
|
||||
@@ -664,7 +750,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
SetSnapping { snap } => {
|
||||
self.snapping_enabled = snap;
|
||||
}
|
||||
SetTexboxEditability { path, editable } => {
|
||||
SetTextboxEditability { path, editable } => {
|
||||
let text = self.graphene_document.layer(&path).unwrap().as_text().unwrap();
|
||||
responses.push_back(DocumentOperation::SetTextEditability { path, editable }.into());
|
||||
if editable {
|
||||
@@ -760,7 +846,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
responses.push_front(NavigationMessage::SetCanvasZoom { zoom_factor: 2. }.into());
|
||||
}
|
||||
ZoomCanvasToFitAll => {
|
||||
if let Some(bounds) = self.document_bounds(font_cache) {
|
||||
if let Some(bounds) = self.document_bounds(&persistent_data.font_cache) {
|
||||
responses.push_back(
|
||||
NavigationMessage::FitViewportToBounds {
|
||||
bounds,
|
||||
@@ -812,6 +898,99 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
}
|
||||
|
||||
impl DocumentMessageHandler {
|
||||
pub fn call_imaginate(&mut self, document_id: u64, preferences: &PreferencesMessageHandler, persistent_data: &PersistentData) -> Option<Message> {
|
||||
let layer_path = {
|
||||
let mut selected_imaginate_layers = self.selected_layers_with_type(LayerDataTypeDiscriminant::Imaginate);
|
||||
|
||||
// Get what is hopefully the only selected Imaginate layer
|
||||
match selected_imaginate_layers.next() {
|
||||
// Continue only if there are no additional Imaginate layers also selected
|
||||
Some(layer_path) if selected_imaginate_layers.next().is_none() => layer_path.to_owned(),
|
||||
_ => return None,
|
||||
}
|
||||
};
|
||||
|
||||
// Prepare the Imaginate parameters and base image
|
||||
|
||||
let layer = self.graphene_document.layer(&layer_path).unwrap();
|
||||
let imaginate_layer = layer.as_imaginate().unwrap();
|
||||
|
||||
let parameters = ImaginateGenerationParameters {
|
||||
seed: imaginate_layer.seed,
|
||||
samples: imaginate_layer.samples,
|
||||
sampling_method: imaginate_layer.sampling_method.api_value().to_string(),
|
||||
denoising_strength: imaginate_layer.use_img2img.then_some(imaginate_layer.denoising_strength),
|
||||
cfg_scale: imaginate_layer.cfg_scale,
|
||||
prompt: imaginate_layer.prompt.clone(),
|
||||
negative_prompt: imaginate_layer.negative_prompt.clone(),
|
||||
resolution: pick_layer_safe_imaginate_resolution(layer, &persistent_data.font_cache),
|
||||
restore_faces: imaginate_layer.restore_faces,
|
||||
tiling: imaginate_layer.tiling,
|
||||
};
|
||||
let base_image = if imaginate_layer.use_img2img {
|
||||
// Calculate the bounding box of the region to be exported
|
||||
let bounds = layer.aabb(&persistent_data.font_cache).unwrap_or_default();
|
||||
let size = bounds[1] - bounds[0];
|
||||
|
||||
let svg = self.render_document(bounds, persistent_data, DocumentRenderMode::OnlyBelowLayerInFolder(&layer_path));
|
||||
|
||||
Some(ImaginateBaseImage { svg, size })
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Some(
|
||||
FrontendMessage::TriggerImaginateGenerate {
|
||||
parameters,
|
||||
base_image,
|
||||
hostname: preferences.imaginate_server_hostname.clone(),
|
||||
refresh_frequency: preferences.imaginate_refresh_frequency,
|
||||
document_id,
|
||||
layer_path,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_document(&mut self, bounds: [DVec2; 2], persistent_data: &PersistentData, render_mode: DocumentRenderMode) -> String {
|
||||
// Remove the artwork and artboard pan/tilt/zoom to render it without the user's viewport navigation, and save it to be restored at the end
|
||||
|
||||
let old_artwork_transform = self.graphene_document.root.transform;
|
||||
self.graphene_document.root.transform = DAffine2::IDENTITY;
|
||||
GrapheneDocument::mark_children_as_dirty(&mut self.graphene_document.root);
|
||||
|
||||
let old_artboard_transform = self.artboard_message_handler.artboards_graphene_document.root.transform;
|
||||
self.artboard_message_handler.artboards_graphene_document.root.transform = DAffine2::IDENTITY;
|
||||
GrapheneDocument::mark_children_as_dirty(&mut self.artboard_message_handler.artboards_graphene_document.root);
|
||||
|
||||
// Render the document SVG code
|
||||
|
||||
let size = bounds[1] - bounds[0];
|
||||
let render_data = RenderData::new(ViewMode::Normal, &persistent_data.font_cache, None);
|
||||
|
||||
let artwork = match render_mode {
|
||||
DocumentRenderMode::Root => self.graphene_document.render_root(render_data),
|
||||
DocumentRenderMode::OnlyBelowLayerInFolder(below_layer_path) => self.graphene_document.render_layers_below(below_layer_path, render_data).unwrap(),
|
||||
};
|
||||
let artboards = self.artboard_message_handler.artboards_graphene_document.render_root(render_data);
|
||||
let outside_artboards_color = if self.artboard_message_handler.artboard_ids.is_empty() { "#ffffff" } else { "#000000" };
|
||||
let outside_artboards = format!(r#"<rect x="{}" y="{}" width="100%" height="100%" fill="{}" />"#, bounds[0].x, bounds[0].y, outside_artboards_color);
|
||||
let svg = format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}" width="{}" height="{}">{}{}{}{}</svg>"#,
|
||||
bounds[0].x, bounds[0].y, size.x, size.y, size.x, size.y, "\n", outside_artboards, artboards, artwork
|
||||
);
|
||||
|
||||
// Transform the artwork and artboard back to their original scales
|
||||
|
||||
self.graphene_document.root.transform = old_artwork_transform;
|
||||
GrapheneDocument::mark_children_as_dirty(&mut self.graphene_document.root);
|
||||
|
||||
self.artboard_message_handler.artboards_graphene_document.root.transform = old_artboard_transform;
|
||||
GrapheneDocument::mark_children_as_dirty(&mut self.artboard_message_handler.artboards_graphene_document.root);
|
||||
|
||||
svg
|
||||
}
|
||||
|
||||
pub fn serialize_document(&self) -> String {
|
||||
let val = serde_json::to_string(self);
|
||||
// We fully expect the serialization to succeed
|
||||
@@ -881,11 +1060,20 @@ impl DocumentMessageHandler {
|
||||
}
|
||||
|
||||
pub fn selected_layers(&self) -> impl Iterator<Item = &[LayerId]> {
|
||||
self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then(|| path.as_slice()))
|
||||
self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then_some(path.as_slice()))
|
||||
}
|
||||
|
||||
pub fn selected_layers_with_type(&self, discriminant: LayerDataTypeDiscriminant) -> impl Iterator<Item = &[LayerId]> {
|
||||
self.selected_layers().filter(move |path| {
|
||||
self.graphene_document
|
||||
.layer(path)
|
||||
.map(|layer| LayerDataTypeDiscriminant::from(&layer.data) == discriminant)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn non_selected_layers(&self) -> impl Iterator<Item = &[LayerId]> {
|
||||
self.layer_metadata.iter().filter_map(|(path, data)| (!data.selected).then(|| path.as_slice()))
|
||||
self.layer_metadata.iter().filter_map(|(path, data)| (!data.selected).then_some(path.as_slice()))
|
||||
}
|
||||
|
||||
pub fn selected_layers_without_children(&self) -> Vec<&[LayerId]> {
|
||||
@@ -1014,7 +1202,7 @@ impl DocumentMessageHandler {
|
||||
|
||||
/// Returns an unsorted list of all layer paths including folders at all levels, except the document's top-level root folder itself
|
||||
pub fn all_layers(&self) -> impl Iterator<Item = &[LayerId]> {
|
||||
self.layer_metadata.keys().filter_map(|path| (!path.is_empty()).then(|| path.as_slice()))
|
||||
self.layer_metadata.keys().filter_map(|path| (!path.is_empty()).then_some(path.as_slice()))
|
||||
}
|
||||
|
||||
/// Returns the paths to all layers in order
|
||||
@@ -1022,7 +1210,7 @@ impl DocumentMessageHandler {
|
||||
// Compute the indices for each layer to be able to sort them
|
||||
let mut layers_with_indices: Vec<(&[LayerId], Vec<usize>)> = paths
|
||||
// 'path.len() > 0' filters out root layer since it has no indices
|
||||
.filter_map(|path| (!path.is_empty()).then(|| path))
|
||||
.filter_map(|path| (!path.is_empty()).then_some(path))
|
||||
.filter_map(|path| {
|
||||
// TODO: `indices_for_path` can return an error. We currently skip these layers and log a warning. Once this problem is solved this code can be simplified.
|
||||
match self.graphene_document.indices_for_path(path) {
|
||||
@@ -1093,7 +1281,7 @@ impl DocumentMessageHandler {
|
||||
Some((document, layer_metadata)) => {
|
||||
// Update the currently displayed layer on the Properties panel if the selection changes after an undo action
|
||||
// Also appropriately update the Properties panel if an undo action results in a layer being deleted
|
||||
let prev_selected_paths: Vec<Vec<LayerId>> = layer_metadata.iter().filter_map(|(layer_id, metadata)| metadata.selected.then(|| layer_id.clone())).collect();
|
||||
let prev_selected_paths: Vec<Vec<LayerId>> = layer_metadata.iter().filter_map(|(layer_id, metadata)| metadata.selected.then_some(layer_id.clone())).collect();
|
||||
|
||||
if prev_selected_paths != selected_paths {
|
||||
responses.push_back(BroadcastEvent::SelectionChanged.into());
|
||||
@@ -1123,7 +1311,7 @@ impl DocumentMessageHandler {
|
||||
Some((document, layer_metadata)) => {
|
||||
// Update currently displayed layer on property panel if selection changes after redo action
|
||||
// Also appropriately update property panel if redo action results in a layer being added
|
||||
let next_selected_paths: Vec<Vec<LayerId>> = layer_metadata.iter().filter_map(|(layer_id, metadata)| metadata.selected.then(|| layer_id.clone())).collect();
|
||||
let next_selected_paths: Vec<Vec<LayerId>> = layer_metadata.iter().filter_map(|(layer_id, metadata)| metadata.selected.then_some(layer_id.clone())).collect();
|
||||
|
||||
if next_selected_paths != selected_paths {
|
||||
responses.push_back(BroadcastEvent::SelectionChanged.into());
|
||||
@@ -1231,24 +1419,33 @@ impl DocumentMessageHandler {
|
||||
}
|
||||
|
||||
/// Loads layer resources such as creating the blob URLs for the images and loading all of the fonts in the document
|
||||
pub fn load_layer_resources(&self, responses: &mut VecDeque<Message>, root: &LayerDataType, mut path: Vec<LayerId>) {
|
||||
pub fn load_layer_resources(&self, responses: &mut VecDeque<Message>, root: &LayerDataType, mut path: Vec<LayerId>, document_id: u64) {
|
||||
fn walk_layers(data: &LayerDataType, path: &mut Vec<LayerId>, image_data: &mut Vec<FrontendImageData>, fonts: &mut HashSet<Font>) {
|
||||
match data {
|
||||
LayerDataType::Folder(f) => {
|
||||
for (id, layer) in f.layer_ids.iter().zip(f.layers().iter()) {
|
||||
LayerDataType::Folder(folder) => {
|
||||
for (id, layer) in folder.layer_ids.iter().zip(folder.layers().iter()) {
|
||||
path.push(*id);
|
||||
walk_layers(&layer.data, path, image_data, fonts);
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
LayerDataType::Text(txt) => {
|
||||
fonts.insert(txt.font.clone());
|
||||
LayerDataType::Text(text) => {
|
||||
fonts.insert(text.font.clone());
|
||||
}
|
||||
LayerDataType::Image(img) => image_data.push(FrontendImageData {
|
||||
LayerDataType::Image(image) => image_data.push(FrontendImageData {
|
||||
path: path.clone(),
|
||||
image_data: img.image_data.clone(),
|
||||
mime: img.mime.clone(),
|
||||
image_data: image.image_data.clone(),
|
||||
mime: image.mime.clone(),
|
||||
}),
|
||||
LayerDataType::Imaginate(imaginate) => {
|
||||
if let Some(data) = &imaginate.image_data {
|
||||
image_data.push(FrontendImageData {
|
||||
path: path.clone(),
|
||||
image_data: data.image_data.clone(),
|
||||
mime: imaginate.mime.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1257,7 +1454,7 @@ impl DocumentMessageHandler {
|
||||
let mut fonts = HashSet::new();
|
||||
walk_layers(root, &mut path, &mut image_data, &mut fonts);
|
||||
if !image_data.is_empty() {
|
||||
responses.push_front(FrontendMessage::UpdateImageData { image_data }.into());
|
||||
responses.push_front(FrontendMessage::UpdateImageData { document_id, image_data }.into());
|
||||
}
|
||||
for font in fonts {
|
||||
responses.push_front(FrontendMessage::TriggerFontLoad { font, is_default: false }.into());
|
||||
|
||||
@@ -50,11 +50,9 @@ impl Default for NavigationMessageHandler {
|
||||
|
||||
impl MessageHandler<NavigationMessage, (&Document, &InputPreprocessorMessageHandler)> for NavigationMessageHandler {
|
||||
#[remain::check]
|
||||
fn process_message(&mut self, message: NavigationMessage, data: (&Document, &InputPreprocessorMessageHandler), responses: &mut VecDeque<Message>) {
|
||||
fn process_message(&mut self, message: NavigationMessage, (document, ipp): (&Document, &InputPreprocessorMessageHandler), responses: &mut VecDeque<Message>) {
|
||||
use NavigationMessage::*;
|
||||
|
||||
let (document, ipp) = data;
|
||||
|
||||
#[remain::sorted]
|
||||
match message {
|
||||
DecreaseCanvasZoom { center_on_mouse } => {
|
||||
|
||||
@@ -31,7 +31,7 @@ impl MessageHandler<OverlaysMessage, (bool, &FontCache, &InputPreprocessorMessag
|
||||
responses.push_back(
|
||||
FrontendMessage::UpdateDocumentOverlays {
|
||||
svg: if overlays_visible {
|
||||
let render_data = RenderData::new(ViewMode::Normal, font_cache, Some(ipp.document_bounds()), false);
|
||||
let render_data = RenderData::new(ViewMode::Normal, font_cache, Some(ipp.document_bounds()));
|
||||
self.overlays_graphene_document.render_root(render_data)
|
||||
} else {
|
||||
String::from("")
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::messages::layout::utility_types::widgets::assist_widgets::PivotPositi
|
||||
use crate::messages::portfolio::document::utility_types::misc::TargetDocument;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graphene::layers::imaginate_layer::ImaginateSamplingMethod;
|
||||
use graphene::layers::style::{Fill, Stroke};
|
||||
use graphene::LayerId;
|
||||
|
||||
@@ -26,6 +27,19 @@ pub enum PropertiesPanelMessage {
|
||||
ModifyTransform { value: f64, transform_op: TransformOp },
|
||||
ResendActiveProperties,
|
||||
SetActiveLayers { paths: Vec<Vec<LayerId>>, document: TargetDocument },
|
||||
SetImaginateCfgScale { cfg_scale: f64 },
|
||||
SetImaginateDenoisingStrength { denoising_strength: f64 },
|
||||
SetImaginateNegativePrompt { negative_prompt: String },
|
||||
SetImaginatePrompt { prompt: String },
|
||||
SetImaginateRestoreFaces { restore_faces: bool },
|
||||
SetImaginateSamples { samples: u32 },
|
||||
SetImaginateSamplingMethod { method: ImaginateSamplingMethod },
|
||||
SetImaginateScaleFromResolution,
|
||||
SetImaginateSeed { seed: u64 },
|
||||
SetImaginateSeedRandomize,
|
||||
SetImaginateSeedRandomizeAndGenerate,
|
||||
SetImaginateTiling { tiling: bool },
|
||||
SetImaginateUseImg2Img { use_img2img: bool },
|
||||
SetPivot { new_position: PivotPosition },
|
||||
UpdateSelectedDocumentProperties,
|
||||
}
|
||||
|
||||
+63
-6
@@ -1,9 +1,11 @@
|
||||
use super::utility_functions::{register_artboard_layer_properties, register_artwork_layer_properties};
|
||||
use super::utility_types::PropertiesPanelMessageHandlerData;
|
||||
use crate::application::generate_uuid;
|
||||
use crate::messages::layout::utility_types::layout_widget::{Layout, WidgetLayout};
|
||||
use crate::messages::layout::utility_types::misc::LayoutTarget;
|
||||
use crate::messages::portfolio::document::properties_panel::utility_functions::apply_transform_operation;
|
||||
use crate::messages::portfolio::document::utility_types::misc::TargetDocument;
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graphene::{LayerId, Operation};
|
||||
@@ -15,14 +17,13 @@ pub struct PropertiesPanelMessageHandler {
|
||||
active_selection: Option<(Vec<LayerId>, TargetDocument)>,
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerData<'a>> for PropertiesPanelMessageHandler {
|
||||
impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPanelMessageHandlerData<'a>)> for PropertiesPanelMessageHandler {
|
||||
#[remain::check]
|
||||
fn process_message(&mut self, message: PropertiesPanelMessage, data: PropertiesPanelMessageHandlerData, responses: &mut VecDeque<Message>) {
|
||||
fn process_message(&mut self, message: PropertiesPanelMessage, (persistent_data, data): (&PersistentData, PropertiesPanelMessageHandlerData), responses: &mut VecDeque<Message>) {
|
||||
let PropertiesPanelMessageHandlerData {
|
||||
artwork_document,
|
||||
artboard_document,
|
||||
selected_layers,
|
||||
font_cache,
|
||||
} = data;
|
||||
let get_document = |document_selector: TargetDocument| match document_selector {
|
||||
TargetDocument::Artboard => artboard_document,
|
||||
@@ -81,7 +82,7 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
|
||||
let (path, target_document) = self.active_selection.as_ref().expect("Received update for properties panel with no active layer");
|
||||
let layer = get_document(*target_document).layer(path).unwrap();
|
||||
|
||||
let transform = apply_transform_operation(layer, transform_op, value, font_cache);
|
||||
let transform = apply_transform_operation(layer, transform_op, value, &persistent_data.font_cache);
|
||||
|
||||
responses.push_back(self.create_document_operation(Operation::SetLayerTransform { path: path.clone(), transform }));
|
||||
}
|
||||
@@ -136,8 +137,8 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
|
||||
if let Some((path, target_document)) = self.active_selection.clone() {
|
||||
let layer = get_document(target_document).layer(&path).unwrap();
|
||||
match target_document {
|
||||
TargetDocument::Artboard => register_artboard_layer_properties(layer, responses, font_cache),
|
||||
TargetDocument::Artwork => register_artwork_layer_properties(layer, responses, font_cache),
|
||||
TargetDocument::Artboard => register_artboard_layer_properties(layer, responses, persistent_data),
|
||||
TargetDocument::Artwork => register_artwork_layer_properties(layer, responses, persistent_data),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,6 +149,62 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
SetImaginatePrompt { prompt } => {
|
||||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
responses.push_back(Operation::ImaginateSetPrompt { path, prompt }.into());
|
||||
}
|
||||
SetImaginateNegativePrompt { negative_prompt } => {
|
||||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
responses.push_back(Operation::ImaginateSetNegativePrompt { path, negative_prompt }.into());
|
||||
}
|
||||
SetImaginateDenoisingStrength { denoising_strength } => {
|
||||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
responses.push_back(Operation::ImaginateSetDenoisingStrength { path, denoising_strength }.into());
|
||||
}
|
||||
SetImaginateSamples { samples } => {
|
||||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
responses.push_back(Operation::ImaginateSetSamples { path, samples }.into());
|
||||
}
|
||||
SetImaginateSamplingMethod { method } => {
|
||||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
responses.push_back(Operation::SetImaginateSamplingMethod { path, method }.into());
|
||||
}
|
||||
SetImaginateScaleFromResolution => {
|
||||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
|
||||
responses.push_back(Operation::ImaginateSetScaleFromResolution { path }.into());
|
||||
}
|
||||
SetImaginateSeed { seed } => {
|
||||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
responses.push_back(Operation::ImaginateSetSeed { path, seed }.into());
|
||||
}
|
||||
SetImaginateSeedRandomize => {
|
||||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
let seed = generate_uuid();
|
||||
responses.push_back(Operation::ImaginateSetSeed { path, seed }.into());
|
||||
}
|
||||
SetImaginateSeedRandomizeAndGenerate => {
|
||||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
let seed = generate_uuid();
|
||||
responses.push_back(Operation::ImaginateSetSeed { path, seed }.into());
|
||||
responses.push_back(DocumentMessage::ImaginateGenerate.into());
|
||||
}
|
||||
SetImaginateCfgScale { cfg_scale } => {
|
||||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
responses.push_back(Operation::ImaginateSetCfgScale { path, cfg_scale }.into());
|
||||
}
|
||||
SetImaginateUseImg2Img { use_img2img } => {
|
||||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
responses.push_back(Operation::ImaginateSetUseImg2Img { path, use_img2img }.into());
|
||||
}
|
||||
SetImaginateRestoreFaces { restore_faces } => {
|
||||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
responses.push_back(Operation::ImaginateSetRestoreFaces { path, restore_faces }.into());
|
||||
}
|
||||
SetImaginateTiling { tiling } => {
|
||||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
responses.push_back(Operation::ImaginateSetTiling { path, tiling }.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,17 +2,22 @@ use super::utility_types::TransformOp;
|
||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||
use crate::messages::layout::utility_types::misc::LayoutTarget;
|
||||
use crate::messages::layout::utility_types::widgets::assist_widgets::PivotAssist;
|
||||
use crate::messages::layout::utility_types::widgets::button_widgets::PopoverButton;
|
||||
use crate::messages::layout::utility_types::widgets::input_widgets::{ColorInput, FontInput, NumberInput, RadioEntryData, RadioInput, TextAreaInput, TextInput};
|
||||
use crate::messages::layout::utility_types::widgets::label_widgets::{IconLabel, IconStyle, Separator, SeparatorDirection, SeparatorType, TextLabel};
|
||||
use crate::messages::layout::utility_types::widgets::button_widgets::{IconButton, PopoverButton, TextButton};
|
||||
use crate::messages::layout::utility_types::widgets::input_widgets::{
|
||||
CheckboxInput, ColorInput, DropdownEntryData, DropdownInput, FontInput, NumberInput, RadioEntryData, RadioInput, TextAreaInput, TextInput,
|
||||
};
|
||||
use crate::messages::layout::utility_types::widgets::label_widgets::{IconLabel, Separator, SeparatorDirection, SeparatorType, TextLabel};
|
||||
use crate::messages::portfolio::utility_types::{ImaginateServerStatus, PersistentData};
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene::color::Color;
|
||||
use graphene::document::pick_layer_safe_imaginate_resolution;
|
||||
use graphene::layers::imaginate_layer::{ImaginateLayer, ImaginateSamplingMethod, ImaginateStatus};
|
||||
use graphene::layers::layer_info::{Layer, LayerDataType, LayerDataTypeDiscriminant};
|
||||
use graphene::layers::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke};
|
||||
use graphene::layers::text_layer::{FontCache, TextLayer};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::f64::consts::PI;
|
||||
use std::rc::Rc;
|
||||
|
||||
@@ -34,12 +39,12 @@ pub fn apply_transform_operation(layer: &Layer, transform_op: TransformOp, value
|
||||
transformation(layer.transform, value / scale).to_cols_array()
|
||||
}
|
||||
|
||||
pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>, font_cache: &FontCache) {
|
||||
pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>, persistent_data: &PersistentData) {
|
||||
let options_bar = vec![LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
WidgetHolder::new(Widget::IconLabel(IconLabel {
|
||||
icon: "NodeArtboard".into(),
|
||||
icon_style: IconStyle::Node,
|
||||
tooltip: "Artboard".into(),
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Related,
|
||||
@@ -81,7 +86,7 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ
|
||||
} else {
|
||||
panic!("Artboard must have a solid fill")
|
||||
};
|
||||
let pivot = layer.transform.transform_vector2(layer.layerspace_pivot(font_cache));
|
||||
let pivot = layer.transform.transform_vector2(layer.layerspace_pivot(&persistent_data.font_cache));
|
||||
|
||||
vec![LayoutGroup::Section {
|
||||
name: "Artboard".into(),
|
||||
@@ -139,7 +144,7 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: Some(layer.bounding_transform(font_cache).scale_x()),
|
||||
value: Some(layer.bounding_transform(&persistent_data.font_cache).scale_x()),
|
||||
label: "W".into(),
|
||||
unit: " px".into(),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
@@ -156,7 +161,7 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: Some(layer.bounding_transform(font_cache).scale_y()),
|
||||
value: Some(layer.bounding_transform(&persistent_data.font_cache).scale_y()),
|
||||
label: "H".into(),
|
||||
unit: " px".into(),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
@@ -219,25 +224,29 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ
|
||||
);
|
||||
}
|
||||
|
||||
pub fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>, font_cache: &FontCache) {
|
||||
pub fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>, persistent_data: &PersistentData) {
|
||||
let options_bar = vec![LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
match &layer.data {
|
||||
LayerDataType::Folder(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
|
||||
icon: "NodeFolder".into(),
|
||||
icon_style: IconStyle::Node,
|
||||
tooltip: "Folder".into(),
|
||||
})),
|
||||
LayerDataType::Shape(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
|
||||
icon: "NodeShape".into(),
|
||||
icon_style: IconStyle::Node,
|
||||
tooltip: "Shape".into(),
|
||||
})),
|
||||
LayerDataType::Text(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
|
||||
icon: "NodeText".into(),
|
||||
icon_style: IconStyle::Node,
|
||||
tooltip: "Text".into(),
|
||||
})),
|
||||
LayerDataType::Image(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
|
||||
icon: "NodeImage".into(),
|
||||
icon_style: IconStyle::Node,
|
||||
tooltip: "Image".into(),
|
||||
})),
|
||||
LayerDataType::Imaginate(_) => WidgetHolder::new(Widget::IconLabel(IconLabel {
|
||||
icon: "NodeImaginate".into(),
|
||||
tooltip: "Imaginate".into(),
|
||||
})),
|
||||
},
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
@@ -272,24 +281,31 @@ pub fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque
|
||||
let properties_body = match &layer.data {
|
||||
LayerDataType::Shape(shape) => {
|
||||
if let Some(fill_layout) = node_section_fill(shape.style.fill()) {
|
||||
vec![node_section_transform(layer, font_cache), fill_layout, node_section_stroke(&shape.style.stroke().unwrap_or_default())]
|
||||
vec![
|
||||
node_section_transform(layer, persistent_data),
|
||||
fill_layout,
|
||||
node_section_stroke(&shape.style.stroke().unwrap_or_default()),
|
||||
]
|
||||
} else {
|
||||
vec![node_section_transform(layer, font_cache), node_section_stroke(&shape.style.stroke().unwrap_or_default())]
|
||||
vec![node_section_transform(layer, persistent_data), node_section_stroke(&shape.style.stroke().unwrap_or_default())]
|
||||
}
|
||||
}
|
||||
LayerDataType::Text(text) => {
|
||||
vec![
|
||||
node_section_transform(layer, font_cache),
|
||||
node_section_transform(layer, persistent_data),
|
||||
node_section_font(text),
|
||||
node_section_fill(text.path_style.fill()).expect("Text should have fill"),
|
||||
node_section_stroke(&text.path_style.stroke().unwrap_or_default()),
|
||||
]
|
||||
}
|
||||
LayerDataType::Image(_) => {
|
||||
vec![node_section_transform(layer, font_cache)]
|
||||
vec![node_section_transform(layer, persistent_data)]
|
||||
}
|
||||
_ => {
|
||||
vec![]
|
||||
LayerDataType::Imaginate(imaginate) => {
|
||||
vec![node_section_transform(layer, persistent_data), node_section_imaginate(imaginate, layer, persistent_data, responses)]
|
||||
}
|
||||
LayerDataType::Folder(_) => {
|
||||
vec![node_section_transform(layer, persistent_data)]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -309,8 +325,8 @@ pub fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque
|
||||
);
|
||||
}
|
||||
|
||||
fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutGroup {
|
||||
let pivot = layer.transform.transform_vector2(layer.layerspace_pivot(font_cache));
|
||||
fn node_section_transform(layer: &Layer, persistent_data: &PersistentData) -> LayoutGroup {
|
||||
let pivot = layer.transform.transform_vector2(layer.layerspace_pivot(&persistent_data.font_cache));
|
||||
LayoutGroup::Section {
|
||||
name: "Transform".into(),
|
||||
layout: vec![
|
||||
@@ -442,7 +458,7 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutGroup
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: Some(layer.bounding_transform(font_cache).scale_x()),
|
||||
value: Some(layer.bounding_transform(&persistent_data.font_cache).scale_x()),
|
||||
label: "W".into(),
|
||||
unit: " px".into(),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
@@ -459,7 +475,7 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutGroup
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: Some(layer.bounding_transform(font_cache).scale_y()),
|
||||
value: Some(layer.bounding_transform(&persistent_data.font_cache).scale_y()),
|
||||
label: "H".into(),
|
||||
unit: " px".into(),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
@@ -477,6 +493,524 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutGroup
|
||||
}
|
||||
}
|
||||
|
||||
fn node_section_imaginate(imaginate_layer: &ImaginateLayer, layer: &Layer, persistent_data: &PersistentData, responses: &mut VecDeque<Message>) -> LayoutGroup {
|
||||
LayoutGroup::Section {
|
||||
name: "Imaginate".into(),
|
||||
layout: vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: {
|
||||
let tooltip = "Connection status to the server that computes generated images".to_string();
|
||||
|
||||
vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Server".into(),
|
||||
tooltip: tooltip.clone(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
size: 24,
|
||||
icon: "Settings".into(),
|
||||
tooltip: "Preferences: Imaginate".into(),
|
||||
on_update: WidgetCallback::new(|_| DialogMessage::RequestPreferencesDialog.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Related,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: {
|
||||
match &persistent_data.imaginate_server_status {
|
||||
ImaginateServerStatus::Unknown => {
|
||||
responses.push_back(PortfolioMessage::ImaginateCheckServerStatus.into());
|
||||
"Checking...".into()
|
||||
}
|
||||
ImaginateServerStatus::Checking => "Checking...".into(),
|
||||
ImaginateServerStatus::Unavailable => "Unavailable".into(),
|
||||
ImaginateServerStatus::Connected => "Connected".into(),
|
||||
}
|
||||
},
|
||||
bold: true,
|
||||
tooltip,
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Related,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
size: 24,
|
||||
icon: "Reload".into(),
|
||||
tooltip: "Refresh connection status".into(),
|
||||
on_update: WidgetCallback::new(|_| PortfolioMessage::ImaginateCheckServerStatus.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: {
|
||||
let tooltip = "When generating, the percentage represents how many sampling steps have so far been processed out of the target number".to_string();
|
||||
|
||||
vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Progress".into(),
|
||||
tooltip: tooltip.clone(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: {
|
||||
// Since we don't serialize the status, we need to derive from other state whether the Idle state is actually supposed to be the Terminated state
|
||||
let mut interpreted_status = imaginate_layer.status.clone();
|
||||
if imaginate_layer.status == ImaginateStatus::Idle
|
||||
&& imaginate_layer.blob_url.is_some()
|
||||
&& imaginate_layer.percent_complete > 0.
|
||||
&& imaginate_layer.percent_complete < 100.
|
||||
{
|
||||
interpreted_status = ImaginateStatus::Terminated;
|
||||
}
|
||||
|
||||
match interpreted_status {
|
||||
ImaginateStatus::Idle => match imaginate_layer.blob_url {
|
||||
Some(_) => "Done".into(),
|
||||
None => "Ready".into(),
|
||||
},
|
||||
ImaginateStatus::Beginning => "Beginning...".into(),
|
||||
ImaginateStatus::Uploading(percent) => format!("Uploading Base Image: {:.0}%", percent),
|
||||
ImaginateStatus::Generating => format!("Generating: {:.0}%", imaginate_layer.percent_complete),
|
||||
ImaginateStatus::Terminating => "Terminating...".into(),
|
||||
ImaginateStatus::Terminated => format!("{:.0}% (Terminated)", imaginate_layer.percent_complete),
|
||||
}
|
||||
},
|
||||
bold: true,
|
||||
tooltip,
|
||||
..Default::default()
|
||||
})),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: [
|
||||
vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Image".into(),
|
||||
tooltip: "Buttons that control the image generation process".into(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
],
|
||||
{
|
||||
match imaginate_layer.status {
|
||||
ImaginateStatus::Beginning | ImaginateStatus::Uploading(_) => vec![WidgetHolder::new(Widget::TextButton(TextButton {
|
||||
label: "Beginning...".into(),
|
||||
tooltip: "Sending image generation request to the server".into(),
|
||||
disabled: true,
|
||||
..Default::default()
|
||||
}))],
|
||||
ImaginateStatus::Generating => vec![WidgetHolder::new(Widget::TextButton(TextButton {
|
||||
label: "Terminate".into(),
|
||||
tooltip: "Cancel in-progress image generation and keep the latest progress".into(),
|
||||
on_update: WidgetCallback::new(|_| DocumentMessage::ImaginateTerminate.into()),
|
||||
..Default::default()
|
||||
}))],
|
||||
ImaginateStatus::Terminating => vec![WidgetHolder::new(Widget::TextButton(TextButton {
|
||||
label: "Terminating...".into(),
|
||||
tooltip: "Waiting on the final image generated after termination".into(),
|
||||
disabled: true,
|
||||
..Default::default()
|
||||
}))],
|
||||
ImaginateStatus::Idle | ImaginateStatus::Terminated => vec![
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
size: 24,
|
||||
icon: "Random".into(),
|
||||
tooltip: "Generate with a random seed".into(),
|
||||
on_update: WidgetCallback::new(|_| PropertiesPanelMessage::SetImaginateSeedRandomizeAndGenerate.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Related,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||
label: "Generate".into(),
|
||||
tooltip: "Fill layer frame by generating a new image".into(),
|
||||
on_update: WidgetCallback::new(|_| DocumentMessage::ImaginateGenerate.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Related,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||
label: "Clear".into(),
|
||||
tooltip: "Remove generated image from the layer frame".into(),
|
||||
disabled: imaginate_layer.blob_url == None,
|
||||
on_update: WidgetCallback::new(|_| DocumentMessage::ImaginateClear.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
],
|
||||
}
|
||||
},
|
||||
]
|
||||
.concat(),
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: {
|
||||
let tooltip = "Seed determines the random outcome, enabling limitless unique variations".to_string();
|
||||
|
||||
vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Seed".into(),
|
||||
tooltip: tooltip.clone(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
size: 24,
|
||||
icon: "Regenerate".into(),
|
||||
tooltip: "Set a new random seed".into(),
|
||||
on_update: WidgetCallback::new(|_| PropertiesPanelMessage::SetImaginateSeedRandomize.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Related,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: Some(imaginate_layer.seed as f64),
|
||||
min: Some(-1.),
|
||||
tooltip,
|
||||
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::SetImaginateSeed {
|
||||
seed: number_input.value.unwrap().round() as u64,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: {
|
||||
let tooltip = "
|
||||
Width and height of the image that will be generated. Larger resolutions take longer to compute.\n\
|
||||
\n\
|
||||
512x512 yields optimal results because the AI is trained to understand that scale best. Larger sizes may tend to integrate the prompt's subject more than once. Small sizes are often incoherent. Put the layer in a folder and resize that to keep resolution unchanged.\n\
|
||||
\n\
|
||||
Dimensions must be a multiple of 64, so these are set by rounding the layer dimensions. A resolution exceeding 1 megapixel is reduced below that limit because larger sizes may exceed available GPU memory on the server.
|
||||
".trim().to_string();
|
||||
|
||||
vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Resolution".into(),
|
||||
tooltip: tooltip.clone(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::IconButton(IconButton {
|
||||
size: 24,
|
||||
icon: "Rescale".into(),
|
||||
tooltip: "Set the layer scale to this resolution".into(),
|
||||
on_update: WidgetCallback::new(|_| PropertiesPanelMessage::SetImaginateScaleFromResolution.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Related,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: {
|
||||
let (width, height) = pick_layer_safe_imaginate_resolution(layer, &persistent_data.font_cache);
|
||||
format!("{} W x {} H", width, height)
|
||||
},
|
||||
tooltip,
|
||||
bold: true,
|
||||
..Default::default()
|
||||
})),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: {
|
||||
let tooltip = "Number of iterations to improve the image generation quality, with diminishing returns around 40".to_string();
|
||||
vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Sampling Steps".into(),
|
||||
tooltip: tooltip.clone(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: Some(imaginate_layer.samples.into()),
|
||||
min: Some(0.),
|
||||
max: Some(150.),
|
||||
tooltip,
|
||||
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::SetImaginateSamples {
|
||||
samples: number_input.value.unwrap().round() as u32,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: {
|
||||
let tooltip = "
|
||||
Algorithm used to generate the image during each sampling step.\n\
|
||||
\n\
|
||||
'DPM Fast' and 'DPM Adaptive' do not support live refreshing updates.
|
||||
"
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let sampling_methods = ImaginateSamplingMethod::list();
|
||||
let mut entries = Vec::with_capacity(sampling_methods.len());
|
||||
for method in sampling_methods {
|
||||
entries.push(DropdownEntryData {
|
||||
label: method.to_string(),
|
||||
on_update: WidgetCallback::new(move |_| PropertiesPanelMessage::SetImaginateSamplingMethod { method }.into()),
|
||||
..DropdownEntryData::default()
|
||||
});
|
||||
}
|
||||
let entries = vec![entries];
|
||||
|
||||
vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Sampling Method".into(),
|
||||
tooltip: tooltip.clone(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::DropdownInput(DropdownInput {
|
||||
entries,
|
||||
selected_index: Some(imaginate_layer.sampling_method as u32),
|
||||
tooltip,
|
||||
..Default::default()
|
||||
})),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: {
|
||||
let tooltip = "Generate an image based upon the artwork beneath this frame in the containing folder".to_string();
|
||||
|
||||
vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Use Base Image".into(),
|
||||
tooltip: tooltip.clone(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::CheckboxInput(CheckboxInput {
|
||||
checked: imaginate_layer.use_img2img,
|
||||
tooltip,
|
||||
on_update: WidgetCallback::new(move |checkbox_input: &CheckboxInput| PropertiesPanelMessage::SetImaginateUseImg2Img { use_img2img: checkbox_input.checked }.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: {
|
||||
let tooltip = "Strength of the artistic liberties allowing changes from the base image. The image is unaltered at 0 and completely different at 1.".to_string();
|
||||
vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Image Creativity".into(),
|
||||
tooltip: tooltip.clone(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: Some(imaginate_layer.denoising_strength),
|
||||
min: Some(0.),
|
||||
max: Some(1.),
|
||||
disabled: !imaginate_layer.use_img2img,
|
||||
tooltip,
|
||||
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::SetImaginateDenoisingStrength {
|
||||
denoising_strength: number_input.value.unwrap(),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: {
|
||||
let tooltip =
|
||||
"Amplification of the text prompt's influence over the outcome. Lower values are more creative and exploratory. Higher values are more literal and uninspired.".to_string();
|
||||
|
||||
vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Text Rigidness".into(),
|
||||
tooltip: tooltip.to_string(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: Some(imaginate_layer.cfg_scale),
|
||||
min: Some(0.),
|
||||
max: Some(30.),
|
||||
tooltip,
|
||||
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::SetImaginateCfgScale {
|
||||
cfg_scale: number_input.value.unwrap(),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Text Prompt".into(),
|
||||
tooltip: "
|
||||
Description of the desired image subject and style.\n\
|
||||
\n\
|
||||
Include an artist name like \"Rembrandt\" or art medium like \"watercolor\" or \"photography\" to influence the look. List multiple to meld styles.\n\
|
||||
\n\
|
||||
To boost the importance of a word or phrase, wrap it in quotes ending with a colon and a multiplier, for example:\n\
|
||||
\"(colorless:0.7) green (ideas sleep:1.3) furiously\"
|
||||
"
|
||||
.trim()
|
||||
.into(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::TextAreaInput(TextAreaInput {
|
||||
value: imaginate_layer.prompt.clone(),
|
||||
on_update: WidgetCallback::new(move |text_area_input: &TextAreaInput| {
|
||||
PropertiesPanelMessage::SetImaginatePrompt {
|
||||
prompt: text_area_input.value.clone(),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Neg. Prompt".into(),
|
||||
tooltip: "A negative text prompt can be used to list things like objects or colors to avoid".into(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::TextAreaInput(TextAreaInput {
|
||||
value: imaginate_layer.negative_prompt.clone(),
|
||||
on_update: WidgetCallback::new(move |text_area_input: &TextAreaInput| {
|
||||
PropertiesPanelMessage::SetImaginateNegativePrompt {
|
||||
negative_prompt: text_area_input.value.clone(),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: {
|
||||
let tooltip = "Postprocess human (or human-like) faces to look subtly less distorted".to_string();
|
||||
|
||||
vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Improve Faces".into(),
|
||||
tooltip: tooltip.clone(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::CheckboxInput(CheckboxInput {
|
||||
checked: imaginate_layer.restore_faces,
|
||||
tooltip,
|
||||
on_update: WidgetCallback::new(move |checkbox_input: &CheckboxInput| {
|
||||
PropertiesPanelMessage::SetImaginateRestoreFaces {
|
||||
restore_faces: checkbox_input.checked,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: {
|
||||
let tooltip = "Generate the image so its edges loop seamlessly to make repeatable patterns or textures".to_string();
|
||||
|
||||
vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Tiling".into(),
|
||||
tooltip: tooltip.clone(),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::CheckboxInput(CheckboxInput {
|
||||
checked: imaginate_layer.tiling,
|
||||
tooltip,
|
||||
on_update: WidgetCallback::new(move |checkbox_input: &CheckboxInput| PropertiesPanelMessage::SetImaginateTiling { tiling: checkbox_input.checked }.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
]
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn node_section_font(layer: &TextLayer) -> LayoutGroup {
|
||||
let font = layer.font.clone();
|
||||
let size = layer.size;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use graphene::document::Document as GrapheneDocument;
|
||||
use graphene::layers::text_layer::FontCache;
|
||||
use graphene::LayerId;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -8,7 +7,6 @@ pub struct PropertiesPanelMessageHandlerData<'a> {
|
||||
pub artwork_document: &'a GrapheneDocument,
|
||||
pub artboard_document: &'a GrapheneDocument,
|
||||
pub selected_layers: &'a mut dyn Iterator<Item = &'a [LayerId]>,
|
||||
pub font_cache: &'a FontCache,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -72,7 +72,7 @@ impl LayerPanelEntry {
|
||||
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();
|
||||
let mut thumbnail = String::new();
|
||||
let mut svg_defs = String::new();
|
||||
let render_data = RenderData::new(ViewMode::Normal, font_cache, None, false);
|
||||
let render_data = RenderData::new(ViewMode::Normal, font_cache, None);
|
||||
layer.data.clone().render(&mut thumbnail, &mut svg_defs, &mut vec![transform], render_data);
|
||||
let transform = transform.to_cols_array().iter().map(ToString::to_string).collect::<Vec<_>>().join(",");
|
||||
let thumbnail = if let [(x_min, y_min), (x_max, y_max)] = arr.as_slice() {
|
||||
|
||||
@@ -44,12 +44,11 @@ pub enum DocumentMode {
|
||||
|
||||
impl fmt::Display for DocumentMode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let text = match self {
|
||||
DocumentMode::DesignMode => "Design Mode".to_string(),
|
||||
DocumentMode::SelectMode => "Select Mode".to_string(),
|
||||
DocumentMode::GuideMode => "Guide Mode".to_string(),
|
||||
};
|
||||
write!(f, "{}", text)
|
||||
match self {
|
||||
DocumentMode::DesignMode => write!(f, "Design Mode"),
|
||||
DocumentMode::SelectMode => write!(f, "Select Mode"),
|
||||
DocumentMode::GuideMode => write!(f, "Guide Mode"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,33 +62,7 @@ impl DocumentMode {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, Serialize, Deserialize)]
|
||||
pub enum Platform {
|
||||
#[default]
|
||||
Unknown,
|
||||
Windows,
|
||||
Mac,
|
||||
Linux,
|
||||
}
|
||||
|
||||
impl Platform {
|
||||
pub fn as_keyboard_platform_layout(&self) -> KeyboardPlatformLayout {
|
||||
match self {
|
||||
Platform::Mac => KeyboardPlatformLayout::Mac,
|
||||
Platform::Unknown => {
|
||||
warn!("The platform has not been set, remember to send `GlobalsMessage::SetPlatform` during editor initialization.");
|
||||
KeyboardPlatformLayout::Standard
|
||||
}
|
||||
_ => KeyboardPlatformLayout::Standard,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, Serialize, Deserialize)]
|
||||
pub enum KeyboardPlatformLayout {
|
||||
/// Standard keyboard mapping used by Windows and Linux
|
||||
#[default]
|
||||
Standard,
|
||||
/// Keyboard mapping used by Macs where Command is sometimes used in favor of Control
|
||||
Mac,
|
||||
pub enum DocumentRenderMode<'a> {
|
||||
Root,
|
||||
OnlyBelowLayerInFolder(&'a [LayerId]),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user