Add inpainting and outpainting to Imaginate (#864)

* Do not select layer immediatly on drag

* Add LayerReferenceInput MVP widget

* Properties Panel

* Fix dragging marker flicker

* Change mask shape to outline

* Add mask rendering

* Simplify select code

* Remove colours

* Fix inpaint/outpaint and rearrage widget UX

* Add mask blur and mask starting fill parameters

* Guard for the case when the layer is missing

* Add icon to LayerReferenceInput to finalize its UI

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube 2022-11-21 07:00:38 +00:00 committed by Keavon Chambers
parent 5bf7b9fdf8
commit 9d80defa14
26 changed files with 1211 additions and 544 deletions

View file

@ -8,7 +8,7 @@ use crate::messages::prelude::*;
use crate::messages::tool::utility_types::HintData;
use graphene::color::Color;
use graphene::layers::imaginate_layer::{ImaginateBaseImage, ImaginateGenerationParameters};
use graphene::layers::imaginate_layer::{ImaginateBaseImage, ImaginateGenerationParameters, ImaginateMaskFillContent, ImaginateMaskPaintMode};
use graphene::layers::text_layer::Font;
use graphene::LayerId;
@ -60,6 +60,14 @@ pub enum FrontendMessage {
parameters: ImaginateGenerationParameters,
#[serde(rename = "baseImage")]
base_image: Option<ImaginateBaseImage>,
#[serde(rename = "maskImage")]
mask_image: Option<ImaginateBaseImage>,
#[serde(rename = "maskPaintMode")]
mask_paint_mode: ImaginateMaskPaintMode,
#[serde(rename = "maskBlurPx")]
mask_blur_px: u32,
#[serde(rename = "maskFillContent")]
mask_fill_content: ImaginateMaskFillContent,
hostname: String,
#[serde(rename = "refreshFrequency")]
refresh_frequency: f64,

View file

@ -6,8 +6,10 @@ use crate::messages::prelude::*;
use graphene::color::Color;
use graphene::layers::text_layer::Font;
use graphene::LayerId;
use serde_json::Value;
use std::ops::Not;
#[derive(Debug, Clone, Default)]
pub struct LayoutMessageHandler {
@ -117,6 +119,19 @@ impl<F: Fn(&MessageDiscriminant) -> Vec<KeysGroup>> MessageHandler<LayoutMessage
let callback_message = (invisible.on_update.callback)(&());
responses.push_back(callback_message);
}
Widget::LayerReferenceInput(layer_reference_input) => {
let update_value = value.is_null().not().then(|| {
value
.as_str()
.expect("LayerReferenceInput update was not of type: string")
.split(',')
.map(|id| id.parse::<LayerId>().unwrap())
.collect::<Vec<_>>()
});
layer_reference_input.value = update_value;
let callback_message = (layer_reference_input.on_update.callback)(layer_reference_input);
responses.push_back(callback_message);
}
Widget::NumberInput(number_input) => match value {
Value::Number(num) => {
let update_value = num.as_f64().unwrap();

View file

@ -61,11 +61,12 @@ impl Layout {
let mut tooltip_shortcut = match &mut widget_holder.widget {
Widget::CheckboxInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::ColorInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::IconButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::OptionalInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::DropdownInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::FontInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::IconButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::LayerReferenceInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::NumberInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::OptionalInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::PopoverButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::TextButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::IconLabel(_)
@ -290,6 +291,7 @@ pub enum Widget {
IconButton(IconButton),
IconLabel(IconLabel),
InvisibleStandinInput(InvisibleStandinInput),
LayerReferenceInput(LayerReferenceInput),
NumberInput(NumberInput),
OptionalInput(OptionalInput),
PivotAssist(PivotAssist),

View file

@ -1,7 +1,7 @@
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
use crate::messages::layout::utility_types::layout_widget::WidgetCallback;
use graphene::color::Color;
use graphene::{color::Color, layers::layer_info::LayerDataTypeDiscriminant, LayerId};
use derivative::*;
use serde::{Deserialize, Serialize};
@ -150,6 +150,34 @@ pub struct InvisibleStandinInput {
pub on_update: WidgetCallback<()>,
}
#[derive(Clone, Serialize, Deserialize, Derivative)]
#[derivative(Debug, PartialEq, Default)]
pub struct LayerReferenceInput {
pub value: Option<Vec<LayerId>>,
#[serde(rename = "layerName")]
pub layer_name: Option<String>,
#[serde(rename = "layerType")]
pub layer_type: Option<LayerDataTypeDiscriminant>,
pub disabled: bool,
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Styling
#[serde(rename = "minWidth")]
pub min_width: u32,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<LayerReferenceInput>,
}
#[derive(Clone, Serialize, Deserialize, Derivative)]
#[derivative(Debug, PartialEq, Default)]
pub struct NumberInput {

View file

@ -963,23 +963,55 @@ impl DocumentMessageHandler {
restore_faces: imaginate_layer.restore_faces,
tiling: imaginate_layer.tiling,
};
let base_image = if imaginate_layer.use_img2img {
let mask_paint_mode = imaginate_layer.mask_paint_mode;
let mask_blur_px = imaginate_layer.mask_blur_px;
let mask_fill_content = imaginate_layer.mask_fill_content;
let (base_image, mask_image) = if imaginate_layer.use_img2img {
let mask = imaginate_layer.mask_layer_ref.clone();
// Calculate the size of the region to be exported
let size = DVec2::new(transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length());
let old_transforms = self.remove_document_transform();
let svg = self.render_document(size, transform.inverse(), persistent_data, DocumentRenderMode::OnlyBelowLayerInFolder(&layer_path));
self.restore_document_transform(old_transforms);
Some(ImaginateBaseImage { svg, size })
let mask_image = mask.and_then(|mask_layer_path| match self.graphene_document.layer(&mask_layer_path) {
Ok(_) => {
let svg = self.render_document(size, transform.inverse(), persistent_data, DocumentRenderMode::LayerCutout(&mask_layer_path, Color::WHITE));
Some(ImaginateBaseImage { svg, size })
}
Err(_) => None,
});
if mask_image.is_none() {
return Some(
DialogMessage::DisplayDialogError {
title: "Masking layer is missing".into(),
description: "
It may have been deleted or moved. Please drag a new layer reference\n\
into the 'Masking Layer' parameter input, then generate again."
.trim()
.into(),
}
.into(),
);
}
self.restore_document_transform(old_transforms);
(Some(ImaginateBaseImage { svg, size }), mask_image)
} else {
None
(None, None)
};
Some(
FrontendMessage::TriggerImaginateGenerate {
parameters,
base_image,
mask_image,
mask_paint_mode,
mask_blur_px,
mask_fill_content,
hostname: preferences.imaginate_server_hostname.clone(),
refresh_frequency: preferences.imaginate_refresh_frequency,
document_id,
@ -1042,13 +1074,17 @@ impl DocumentMessageHandler {
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 (artwork, outside) = match render_mode {
DocumentRenderMode::Root => (self.graphene_document.render_root(render_data), None),
DocumentRenderMode::OnlyBelowLayerInFolder(below_layer_path) => (self.graphene_document.render_layers_below(below_layer_path, render_data).unwrap(), None),
DocumentRenderMode::LayerCutout(layer_path, background) => (self.graphene_document.render_layer(layer_path, render_data).unwrap(), Some(background)),
};
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 { "#222222" };
let outside_artboards = format!(r#"<rect x="0" y="0" width="100%" height="100%" fill="{}" />"#, outside_artboards_color);
let outside_artboards_color = outside.map_or_else(
|| if self.artboard_message_handler.artboard_ids.is_empty() { "ffffff" } else { "222222" }.to_string(),
|col| col.rgba_hex(),
);
let outside_artboards = format!(r##"<rect x="0" y="0" width="100%" height="100%" fill="#{}" />"##, outside_artboards_color);
let matrix = transform
.to_cols_array()
.iter()

View file

@ -3,7 +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::imaginate_layer::{ImaginateMaskFillContent, ImaginateMaskPaintMode, ImaginateSamplingMethod};
use graphene::layers::style::{Fill, Stroke};
use graphene::LayerId;
@ -29,6 +29,10 @@ pub enum PropertiesPanelMessage {
SetActiveLayers { paths: Vec<Vec<LayerId>>, document: TargetDocument },
SetImaginateCfgScale { cfg_scale: f64 },
SetImaginateDenoisingStrength { denoising_strength: f64 },
SetImaginateLayerPath { layer_path: Option<Vec<LayerId>> },
SetImaginateMaskBlurPx { mask_blur_px: u32 },
SetImaginateMaskFillContent { mode: ImaginateMaskFillContent },
SetImaginateMaskPaintMode { paint: ImaginateMaskPaintMode },
SetImaginateNegativePrompt { negative_prompt: String },
SetImaginatePrompt { prompt: String },
SetImaginateRestoreFaces { restore_faces: bool },

View file

@ -140,10 +140,11 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
}
ResendActiveProperties => {
if let Some((path, target_document)) = self.active_selection.clone() {
let layer = get_document(target_document).layer(&path).unwrap();
let document = get_document(target_document);
let layer = document.layer(&path).unwrap();
match target_document {
TargetDocument::Artboard => register_artboard_layer_properties(layer, responses, persistent_data),
TargetDocument::Artwork => register_artwork_layer_properties(path, layer, responses, persistent_data, node_graph_message_handler),
TargetDocument::Artwork => register_artwork_layer_properties(document, path, layer, responses, persistent_data, node_graph_message_handler),
}
}
}
@ -166,6 +167,22 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
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());
}
SetImaginateLayerPath { layer_path } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetLayerPath { path, layer_path }.into());
}
SetImaginateMaskBlurPx { mask_blur_px } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetMaskBlurPx { path, mask_blur_px }.into());
}
SetImaginateMaskFillContent { mode } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetMaskFillContent { path, mode }.into());
}
SetImaginateMaskPaintMode { paint } => {
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
responses.push_back(Operation::ImaginateSetMaskPaintMode { path, paint }.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());

View file

@ -61,12 +61,12 @@ impl LayerPanelEntry {
pub fn new(layer_metadata: &LayerMetadata, transform: DAffine2, layer: &Layer, path: Vec<LayerId>, font_cache: &FontCache) -> Self {
let name = layer.name.clone().unwrap_or_else(|| String::from(""));
let tooltip = if cfg!(debug_assertions) {
let joined = &path.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(" / ");
name.clone() + "\nLayer Path: " + joined.as_str()
} else {
name.clone()
};
let mut tooltip = name.clone();
if cfg!(debug_assertions) {
tooltip += "\nLayer Path: ";
tooltip += &path.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(" / ");
tooltip = tooltip.trim().to_string();
}
let arr = layer.data.bounding_box(transform, font_cache).unwrap_or([DVec2::ZERO, DVec2::ZERO]);
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();

View file

@ -1,5 +1,6 @@
pub use super::layer_panel::{LayerMetadata, LayerPanelEntry};
use graphene::color::Color;
use graphene::document::Document as GrapheneDocument;
use graphene::LayerId;
@ -65,4 +66,5 @@ impl DocumentMode {
pub enum DocumentRenderMode<'a> {
Root,
OnlyBelowLayerInFolder(&'a [LayerId]),
LayerCutout(&'a [LayerId], Color),
}

View file

@ -233,6 +233,7 @@ img {
import { defineComponent } from "vue";
import { createClipboardManager } from "@/io-managers/clipboard";
import { createDragManager } from "@/io-managers/drag";
import { createHyperlinkManager } from "@/io-managers/hyperlinks";
import { createInputManager } from "@/io-managers/input";
import { createLocalizationManager } from "@/io-managers/localization";
@ -252,6 +253,7 @@ import MainWindow from "@/components/window/MainWindow.vue";
const managerDestructors: {
createClipboardManager?: () => void;
createDragManager?: () => void;
createHyperlinkManager?: () => void;
createInputManager?: () => void;
createLocalizationManager?: () => void;
@ -302,6 +304,7 @@ export default defineComponent({
// Initialize managers, which are isolated systems that subscribe to backend messages to link them to browser API functionality (like JS events, IndexedDB, etc.)
Object.assign(managerDestructors, {
createClipboardManager: createClipboardManager(this.editor),
createDragManager: createDragManager(),
createHyperlinkManager: createHyperlinkManager(this.editor),
createInputManager: createInputManager(this.editor, this.$el.parentElement, this.dialog, this.portfolio, this.fullscreen),
createLocalizationManager: createLocalizationManager(this.editor),

View file

@ -1,5 +1,5 @@
<template>
<LayoutCol class="layer-tree">
<LayoutCol class="layer-tree" @dragleave="dragInPanel = false">
<LayoutRow class="options-bar" :scrollableX="true">
<WidgetLayout :layout="layerTreeOptionsLayout" />
</LayoutRow>
@ -31,7 +31,8 @@
></button>
<LayoutRow
class="layer"
:class="{ selected: listing.entry.layerMetadata.selected }"
:class="{ selected: fakeHighlight ? fakeHighlight.includes(listing.entry.path) : listing.entry.layerMetadata.selected }"
:data-layer="String(listing.entry.path)"
:data-index="index"
:title="listing.entry.tooltip"
:draggable="draggable"
@ -65,7 +66,11 @@
</LayoutRow>
</LayoutRow>
</LayoutCol>
<div class="insert-mark" v-if="draggingData && !draggingData.highlightFolder" :style="{ left: markIndent(draggingData.insertFolder), top: markTopOffset(draggingData.markerHeight) }"></div>
<div
class="insert-mark"
v-if="draggingData && !draggingData.highlightFolder && dragInPanel"
:style="{ left: markIndent(draggingData.insertFolder), top: markTopOffset(draggingData.markerHeight) }"
></div>
</LayoutRow>
</LayoutCol>
</template>
@ -258,6 +263,7 @@
margin-top: -2px;
height: 5px;
z-index: 1;
pointer-events: none;
}
}
}
@ -266,6 +272,7 @@
<script lang="ts">
import { defineComponent, nextTick } from "vue";
import { beginDraggingElement } from "@/io-managers/drag";
import { platformIsMac } from "@/utility-functions/platform";
import {
type LayerType,
@ -291,7 +298,7 @@ const LAYER_INDENT = 16;
const INSERT_MARK_MARGIN_LEFT = 4 + 32 + LAYER_INDENT;
const INSERT_MARK_OFFSET = 2;
type DraggingData = { insertFolder: BigUint64Array; insertIndex: number; highlightFolder: boolean; markerHeight: number };
type DraggingData = { select?: () => void; insertFolder: BigUint64Array; insertIndex: number; highlightFolder: boolean; markerHeight: number };
export default defineComponent({
inject: ["editor"],
@ -304,6 +311,8 @@ export default defineComponent({
// Interactive dragging
draggable: true,
draggingData: undefined as undefined | DraggingData,
fakeHighlight: undefined as undefined | BigUint64Array[],
dragInPanel: false,
// Layouts
layerTreeOptionsLayout: defaultWidgetLayout(),
@ -371,7 +380,7 @@ export default defineComponent({
async deselectAllLayers() {
this.editor.instance.deselectAllLayers();
},
calculateDragIndex(tree: HTMLDivElement, clientY: number): DraggingData {
calculateDragIndex(tree: HTMLDivElement, clientY: number, select?: () => void): DraggingData {
const treeChildren = tree.children;
const treeOffset = tree.getBoundingClientRect().top;
@ -430,11 +439,21 @@ export default defineComponent({
markerHeight -= treeOffset;
return { insertFolder, insertIndex, highlightFolder, markerHeight };
return { select, insertFolder, insertIndex, highlightFolder, markerHeight };
},
async dragStart(event: DragEvent, listing: LayerListingInfo) {
const layer = listing.entry;
if (!layer.layerMetadata.selected) this.selectLayer(event.ctrlKey, event.metaKey, event.shiftKey, listing, event);
this.dragInPanel = true;
if (!layer.layerMetadata.selected) {
this.fakeHighlight = [layer.path];
}
const select = (): void => {
if (!layer.layerMetadata.selected) this.selectLayer(false, false, false, listing, event);
};
const target = (event.target || undefined) as HTMLElement | undefined;
const draggingELement = (target?.closest("[data-layer]") || undefined) as HTMLElement | undefined;
if (draggingELement) beginDraggingElement(draggingELement);
// Set style of cursor for drag
if (event.dataTransfer) {
@ -443,23 +462,26 @@ export default defineComponent({
}
const tree: HTMLDivElement | undefined = (this.$refs.list as typeof LayoutCol | undefined)?.$el;
if (tree) this.draggingData = this.calculateDragIndex(tree, event.clientY);
if (tree) this.draggingData = this.calculateDragIndex(tree, event.clientY, select);
},
updateInsertLine(event: DragEvent) {
// Stop the drag from being shown as cancelled
event.preventDefault();
this.dragInPanel = true;
const tree: HTMLDivElement | undefined = (this.$refs.list as typeof LayoutCol | undefined)?.$el;
if (tree) this.draggingData = this.calculateDragIndex(tree, event.clientY);
if (tree) this.draggingData = this.calculateDragIndex(tree, event.clientY, this.draggingData?.select);
},
async drop() {
if (this.draggingData) {
const { insertFolder, insertIndex } = this.draggingData;
if (this.draggingData && this.dragInPanel) {
const { select, insertFolder, insertIndex } = this.draggingData;
select?.();
this.editor.instance.moveLayerInTree(insertFolder, insertIndex);
this.draggingData = undefined;
}
this.draggingData = undefined;
this.fakeHighlight = undefined;
this.dragInPanel = false;
},
rebuildLayerTree(updateDocumentLayerTreeStructure: UpdateDocumentLayerTreeStructure) {
const layerWithNameBeingEdited = this.layers.find((layer: LayerListingInfo) => layer.editingName);
@ -494,7 +516,7 @@ export default defineComponent({
recurse(updateDocumentLayerTreeStructure, this.layers, this.layerCache);
},
layerTypeData(layerType: LayerType): LayerTypeData {
return layerTypeData(layerType) || { name: "Error", icon: "NodeText" };
return layerTypeData(layerType) || { name: "Error", icon: "Info" };
},
},
mounted() {

View file

@ -28,6 +28,7 @@
/>
<IconButton v-if="component.props.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widgetId, undefined)" :sharpRightCorners="nextIsSuffix" />
<IconLabel v-if="component.props.kind === 'IconLabel'" v-bind="component.props" />
<LayerReferenceInput v-if="component.props.kind === 'LayerReferenceInput'" v-bind="component.props" @update:value="(value: BigUint64Array) => updateLayout(component.widgetId, value)" />
<NumberInput
v-if="component.props.kind === 'NumberInput'"
v-bind="component.props"
@ -109,6 +110,7 @@ import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
import ColorInput from "@/components/widgets/inputs/ColorInput.vue";
import DropdownInput from "@/components/widgets/inputs/DropdownInput.vue";
import FontInput from "@/components/widgets/inputs/FontInput.vue";
import LayerReferenceInput from "@/components/widgets/inputs/LayerReferenceInput.vue";
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
@ -170,6 +172,7 @@ export default defineComponent({
FontInput,
IconButton,
IconLabel,
LayerReferenceInput,
NumberInput,
OptionalInput,
PivotAssist,

View file

@ -0,0 +1,160 @@
<template>
<LayoutRow
class="layer-reference-input"
:class="{ disabled, droppable, 'sharp-right-corners': sharpRightCorners }"
:title="tooltip"
@dragover="(e: DragEvent) => !disabled && dragOver(e)"
@dragleave="() => !disabled && dragLeave()"
@drop="(e: DragEvent) => !disabled && drop(e)"
>
<template v-if="value === undefined || droppable">
<LayoutRow class="drop-zone"></LayoutRow>
<TextLabel :italic="true">{{ droppable ? "Drop" : "Drag" }} Layer Here</TextLabel>
</template>
<template v-if="value !== undefined && !droppable">
<IconLabel v-if="layerName !== undefined && layerType" :icon="layerTypeData(layerType).icon" class="layer-icon" />
<TextLabel v-if="layerName !== undefined && layerType" :italic="layerName === ''" class="layer-name">{{ layerName || layerTypeData(layerType).name }}</TextLabel>
<TextLabel :bold="true" :italic="true" v-else class="missing">Layer Missing</TextLabel>
</template>
<IconButton v-if="value !== undefined && !droppable" :icon="'CloseX'" :size="16" :disabled="disabled" :action="() => clearLayer()" />
</LayoutRow>
</template>
<style lang="scss">
.layer-reference-input {
position: relative;
flex: 1 0 auto;
height: 24px;
border-radius: 2px;
background: var(--color-1-nearblack);
.drop-zone {
pointer-events: none;
border: 1px dashed var(--color-5-dullgray);
border-radius: 1px;
position: absolute;
top: 2px;
bottom: 2px;
left: 2px;
right: 2px;
}
&.droppable .drop-zone {
border: 1px dashed var(--color-e-nearwhite);
}
.layer-icon {
margin: 4px 8px;
+ .text-label {
padding-left: 0;
}
}
.text-label {
line-height: 18px;
padding: 3px calc(8px + 2px);
width: 100%;
text-align: center;
&.missing {
color: var(--color-data-unused1);
}
&.layer-name {
text-align: left;
}
}
.icon-button {
margin: 4px;
margin-left: 0;
}
&.disabled {
background: var(--color-2-mildblack);
.drop-zone {
border: 1px dashed var(--color-4-dimgray);
}
.text-label {
color: var(--color-8-uppergray);
}
.icon-label svg {
fill: var(--color-8-uppergray);
}
}
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { currentDraggingElement } from "@/io-managers/drag";
import type { LayerType, LayerTypeData } from "@/wasm-communication/messages";
import { layerTypeData } from "@/wasm-communication/messages";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
export default defineComponent({
emits: ["update:value"],
props: {
value: { type: String as PropType<string | undefined>, required: false },
layerName: { type: String as PropType<string | undefined>, required: false },
layerType: { type: String as PropType<LayerType | undefined>, required: false },
disabled: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
},
data() {
return {
hoveringDrop: false,
};
},
computed: {
droppable() {
return this.hoveringDrop && currentDraggingElement();
},
},
methods: {
dragOver(e: DragEvent): void {
this.hoveringDrop = true;
e.preventDefault();
},
dragLeave(): void {
this.hoveringDrop = false;
},
drop(e: DragEvent): void {
this.hoveringDrop = false;
const element = currentDraggingElement();
const layerPath = element?.getAttribute("data-layer") || undefined;
if (layerPath) {
e.preventDefault();
this.$emit("update:value", layerPath);
}
},
clearLayer(): void {
this.$emit("update:value", undefined);
},
layerTypeData(layerType: LayerType): LayerTypeData {
return layerTypeData(layerType) || { name: "Error", icon: "Info" };
},
},
components: {
IconButton,
IconLabel,
LayoutRow,
TextLabel,
},
});
</script>

View file

@ -1,5 +1,5 @@
<template>
<LayoutRow class="optional-input">
<LayoutRow class="optional-input" :class="disabled">
<CheckboxInput :checked="checked" :disabled="disabled" @input="(e: Event) => $emit('update:checked', (e.target as HTMLInputElement).checked)" :icon="icon" :tooltip="tooltip" />
</LayoutRow>
</template>
@ -18,6 +18,10 @@
border-radius: 2px 0 0 2px;
box-sizing: border-box;
}
&.disabled label {
border: 1px solid var(--color-4-dimgray);
}
}
</style>

View file

@ -0,0 +1,26 @@
let draggingElement: HTMLElement | undefined;
export function createDragManager(): () => void {
const clearDraggingElement = (): void => {
draggingElement = undefined;
};
// Add the event listener
document.addEventListener("drop", clearDraggingElement);
// Return the destructor
return () => {
// We use setTimeout to sequence this drop after any potential users in the current call stack progression, since this will begin in an entirely new call stack later
setTimeout(() => {
document.removeEventListener("drop", clearDraggingElement);
}, 0);
};
}
export function beginDraggingElement(element: HTMLElement): void {
draggingElement = element;
}
export function currentDraggingElement(): HTMLElement | undefined {
return draggingElement;
}

View file

@ -4,6 +4,7 @@ import { type IconName } from "@/utility-functions/icons";
import { browserVersion, operatingSystem } from "@/utility-functions/platform";
import { stripIndents } from "@/utility-functions/strip-indents";
import { type Editor } from "@/wasm-communication/editor";
import type { TextLabel } from "@/wasm-communication/messages";
import { type TextButtonWidget, type WidgetLayout, Widget, DisplayDialogPanic } from "@/wasm-communication/messages";
export function createPanicManager(editor: Editor, dialogState: DialogState): void {
@ -24,11 +25,11 @@ export function createPanicManager(editor: Editor, dialogState: DialogState): vo
}
function preparePanicDialog(header: string, details: string, panicDetails: string): [IconName, WidgetLayout, TextButtonWidget[]] {
const headerLabel: TextLabel = { kind: "TextLabel", value: header, disabled: false, bold: true, italic: false, tableAlign: false, minWidth: 0, multiline: false, tooltip: "" };
const detailsLabel: TextLabel = { kind: "TextLabel", value: details, disabled: false, bold: false, italic: false, tableAlign: false, minWidth: 0, multiline: true, tooltip: "" };
const widgets: WidgetLayout = {
layout: [
{ rowWidgets: [new Widget({ kind: "TextLabel", value: header, disabled: false, bold: true, italic: false, tableAlign: false, minWidth: 0, multiline: false, tooltip: "" }, 0n)] },
{ rowWidgets: [new Widget({ kind: "TextLabel", value: details, disabled: false, bold: false, italic: false, tableAlign: false, minWidth: 0, multiline: true, tooltip: "" }, 1n)] },
],
layout: [{ rowWidgets: [new Widget(headerLabel, 0n)] }, { rowWidgets: [new Widget(detailsLabel, 1n)] }],
layoutTarget: undefined,
};

View file

@ -15,7 +15,6 @@ export function createNodeGraphState(editor: Editor) {
editor.subscriptions.subscribeJsMessage(UpdateNodeGraph, (updateNodeGraph) => {
state.nodes = updateNodeGraph.nodes;
state.links = updateNodeGraph.links;
console.info("Recieved updated nodes", state.nodes);
});
editor.subscriptions.subscribeJsMessage(UpdateNodeTypes, (updateNodeTypes) => {
state.nodeTypes = updateNodeTypes.nodeTypes;

View file

@ -68,7 +68,7 @@ export function createPortfolioState(editor: Editor) {
imaginateCheckConnection(hostname, editor);
});
editor.subscriptions.subscribeJsMessage(TriggerImaginateGenerate, async (triggerImaginateGenerate) => {
const { documentId, layerPath, hostname, refreshFrequency, baseImage, parameters } = triggerImaginateGenerate;
const { documentId, layerPath, hostname, refreshFrequency, baseImage, maskImage, maskPaintMode, maskBlurPx, maskFillContent, parameters } = triggerImaginateGenerate;
// Handle img2img mode
let image: Blob | undefined;
@ -79,7 +79,14 @@ export function createPortfolioState(editor: Editor) {
preloadAndSetImaginateBlobURL(editor, image, documentId, layerPath, baseImage.size[0], baseImage.size[1]);
}
imaginateGenerate(parameters, image, hostname, refreshFrequency, documentId, layerPath, editor);
// Handle layer mask
let mask: Blob | undefined;
if (maskImage !== undefined) {
// Rasterize the SVG to an image file
mask = await rasterizeSVG(maskImage.svg, maskImage.size[0], maskImage.size[1], "image/png");
}
imaginateGenerate(parameters, image, mask, maskPaintMode, maskBlurPx, maskFillContent, hostname, refreshFrequency, documentId, layerPath, editor);
});
editor.subscriptions.subscribeJsMessage(TriggerImaginateTerminate, async (triggerImaginateTerminate) => {
const { documentId, layerPath, hostname } = triggerImaginateTerminate;

View file

@ -23,6 +23,10 @@ let statusAbortController = new AbortController();
export async function imaginateGenerate(
parameters: ImaginateGenerationParameters,
image: Blob | undefined,
mask: Blob | undefined,
maskPaintMode: string,
maskBlurPx: number,
maskFillContent: string,
hostname: string,
refreshFrequency: number,
documentId: bigint,
@ -41,7 +45,7 @@ export async function imaginateGenerate(
const discloseUploadingProgress = (progress: number): void => {
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, progress * 100, "Uploading");
};
const { uploaded, result, xhr } = await generate(discloseUploadingProgress, hostname, image, parameters);
const { uploaded, result, xhr } = await generate(discloseUploadingProgress, hostname, image, mask, maskPaintMode, maskBlurPx, maskFillContent, parameters);
generatingAbortRequest = xhr;
try {
@ -211,6 +215,10 @@ async function generate(
discloseUploadingProgress: (progress: number) => void,
hostname: string,
image: Blob | undefined,
mask: Blob | undefined,
maskPaintMode: string,
maskBlurPx: number,
maskFillContent: string,
parameters: ImaginateGenerationParameters
): Promise<{
uploaded: Promise<void>;
@ -255,6 +263,13 @@ async function generate(
};
} else {
const sourceImageBase64 = await blobToBase64(image);
const maskImageBase64 = mask ? await blobToBase64(mask) : "";
const maskFillContentIndexes = ["Fill", "Original", "LatentNoise", "LatentNothing"];
const maskFillContentIndexFound = maskFillContentIndexes.indexOf(maskFillContent);
const maskFillContentIndex = maskFillContentIndexFound === -1 ? undefined : maskFillContentIndexFound;
const maskInvert = maskPaintMode === "Inpaint" ? 1 : 0;
endpoint = `${hostname}sdapi/v1/img2img`;
@ -262,12 +277,12 @@ async function generate(
init_images: [sourceImageBase64],
// resize_mode: 0,
denoising_strength: parameters.denoisingStrength,
// mask: "",
// mask_blur: 4,
// inpainting_fill: 0,
// inpaint_full_res: true,
mask: mask && maskImageBase64,
mask_blur: mask && maskBlurPx,
inpainting_fill: mask && maskFillContentIndex,
inpaint_full_res: mask && false,
// inpaint_full_res_padding: 0,
// inpainting_mask_invert: 0,
inpainting_mask_invert: mask && maskInvert,
prompt: parameters.prompt,
// styles: [],
seed: Number(parameters.seed),
@ -291,6 +306,7 @@ async function generate(
// s_noise: 1,
override_settings: {
show_progress_every_n_steps: PROGRESS_EVERY_N_STEPS,
img2img_fix_steps: true,
},
sampler_index: parameters.samplingMethod,
// include_init_images: false,

View file

@ -509,6 +509,15 @@ export class TriggerImaginateGenerate extends JsMessage {
@Type(() => ImaginateBaseImage)
readonly baseImage!: ImaginateBaseImage | undefined;
@Type(() => ImaginateBaseImage)
readonly maskImage: ImaginateBaseImage | undefined;
readonly maskPaintMode!: string;
readonly maskBlurPx!: number;
readonly maskFillContent!: string;
readonly hostname!: string;
readonly refreshFrequency!: number;
@ -670,7 +679,7 @@ export class UpdateDocumentLayerDetails extends JsMessage {
export class LayerPanelEntry {
name!: string;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
visible!: boolean;
@ -764,7 +773,7 @@ export class CheckboxInput extends WidgetProps {
icon!: IconName;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -778,7 +787,7 @@ export class ColorInput extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -818,7 +827,7 @@ export class DropdownInput extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -831,7 +840,7 @@ export class FontInput extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -844,7 +853,7 @@ export class IconButton extends WidgetProps {
active!: boolean;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -853,10 +862,28 @@ export class IconLabel extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
export class LayerReferenceInput extends WidgetProps {
@Transform(({ value }: { value: BigUint64Array | undefined }) => (value ? String(value) : undefined))
value!: string | undefined;
layerName!: string | undefined;
layerType!: LayerType | undefined;
disabled!: boolean;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
// Styling
minWidth!: number;
}
export type NumberInputIncrementBehavior = "Add" | "Multiply" | "Callback" | "None";
export type NumberInputMode = "Increment" | "Range";
@ -865,7 +892,7 @@ export class NumberInput extends WidgetProps {
label!: string | undefined;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
// Disabled
@ -914,7 +941,7 @@ export class OptionalInput extends WidgetProps {
icon!: IconName;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -928,7 +955,7 @@ export class PopoverButton extends WidgetProps {
text!: string;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -975,7 +1002,7 @@ export class TextAreaInput extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -990,7 +1017,7 @@ export class TextButton extends WidgetProps {
disabled!: boolean;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -1021,7 +1048,7 @@ export class TextInput extends WidgetProps {
minWidth!: number;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -1042,7 +1069,7 @@ export class TextLabel extends WidgetProps {
multiline!: boolean;
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
@ -1063,6 +1090,7 @@ const widgetSubTypes = [
{ value: FontInput, name: "FontInput" },
{ value: IconButton, name: "IconButton" },
{ value: IconLabel, name: "IconLabel" },
{ value: LayerReferenceInput, name: "LayerReferenceInput" },
{ value: NumberInput, name: "NumberInput" },
{ value: OptionalInput, name: "OptionalInput" },
{ value: PopoverButton, name: "PopoverButton" },

View file

@ -179,7 +179,7 @@ impl JsEditorHandle {
self.dispatch(message);
Ok(())
}
_ => Err(Error::new("Could not update UI").into()),
(target, val) => Err(Error::new(&format!("Could not update UI\nDetails:\nTarget: {:?}\nValue: {:?}", target, val)).into()),
}
}

View file

@ -82,6 +82,22 @@ impl Document {
}
}
/// Renders a layer and its children
pub fn render_layer(&mut self, layer_path: &[LayerId], render_data: RenderData) -> Option<String> {
// Note: it is bad practice to directly clone and modify the Graphene document structure, this is a temporary hack until this whole system is replaced by the node graph
let mut temp_clone = self.layer_mut(layer_path).ok()?.clone();
// Render and append to the defs section
let mut svg_defs = String::from("<defs>");
temp_clone.render(&mut vec![], &mut svg_defs, render_data);
svg_defs.push_str("</defs>");
// Append the cached rendered SVG
svg_defs.push_str(&temp_clone.cache);
Some(svg_defs)
}
pub fn current_state_identifier(&self) -> u64 {
self.state_identifier.finish()
}
@ -895,6 +911,36 @@ impl Document {
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetMaskBlurPx { path, mask_blur_px } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate mask blur for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.mask_blur_px = mask_blur_px;
} else {
panic!("Incorrectly trying to set the mask blur for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetMaskFillContent { path, mode } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate mask fill content for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.mask_fill_content = mode;
} else {
panic!("Incorrectly trying to set the mask fill content for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetMaskPaintMode { path, paint } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate mask paint mode for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.mask_paint_mode = paint;
} else {
panic!("Incorrectly trying to set the mask paint mode for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetCfgScale { path, cfg_scale } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate CFG scale for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
@ -915,6 +961,16 @@ impl Document {
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetLayerPath { path, layer_path } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate layer path strength for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {
imaginate.mask_layer_ref = layer_path;
} else {
panic!("Incorrectly trying to set the layer path for a layer that is not an Imaginate layer type");
}
self.mark_as_dirty(&path)?;
Some(vec![LayerChanged { path }])
}
Operation::ImaginateSetSamples { path, samples } => {
let layer = self.layer_mut(&path).expect("Setting Imaginate samples for invalid layer");
if let LayerDataType::Imaginate(imaginate) = &mut layer.data {

View file

@ -18,6 +18,10 @@ pub struct ImaginateLayer {
pub sampling_method: ImaginateSamplingMethod,
pub use_img2img: bool,
pub denoising_strength: f64,
pub mask_layer_ref: Option<Vec<LayerId>>,
pub mask_paint_mode: ImaginateMaskPaintMode,
pub mask_blur_px: u32,
pub mask_fill_content: ImaginateMaskFillContent,
pub cfg_scale: f64,
pub prompt: String,
pub negative_prompt: String,
@ -62,6 +66,44 @@ pub struct ImaginateBaseImage {
pub size: DVec2,
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)]
pub enum ImaginateMaskPaintMode {
#[default]
Inpaint,
Outpaint,
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)]
pub enum ImaginateMaskFillContent {
#[default]
Fill,
Original,
LatentNoise,
LatentNothing,
}
impl ImaginateMaskFillContent {
pub fn list() -> [ImaginateMaskFillContent; 4] {
[
ImaginateMaskFillContent::Fill,
ImaginateMaskFillContent::Original,
ImaginateMaskFillContent::LatentNoise,
ImaginateMaskFillContent::LatentNothing,
]
}
}
impl std::fmt::Display for ImaginateMaskFillContent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImaginateMaskFillContent::Fill => write!(f, "Smeared Surroundings"),
ImaginateMaskFillContent::Original => write!(f, "Original Base Image"),
ImaginateMaskFillContent::LatentNoise => write!(f, "Randomness (Latent Noise)"),
ImaginateMaskFillContent::LatentNothing => write!(f, "Neutral (Latent Nothing)"),
}
}
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Deserialize, Serialize)]
pub enum ImaginateSamplingMethod {
#[default]
@ -182,6 +224,10 @@ impl Default for ImaginateLayer {
sampling_method: Default::default(),
use_img2img: false,
denoising_strength: 0.66,
mask_paint_mode: ImaginateMaskPaintMode::default(),
mask_layer_ref: None,
mask_blur_px: 4,
mask_fill_content: ImaginateMaskFillContent::default(),
cfg_scale: 10.,
prompt: "".into(),
negative_prompt: "".into(),

View file

@ -76,7 +76,7 @@ impl fmt::Display for LayerDataTypeDiscriminant {
LayerDataTypeDiscriminant::Text => write!(f, "Text"),
LayerDataTypeDiscriminant::Image => write!(f, "Image"),
LayerDataTypeDiscriminant::Imaginate => write!(f, "Imaginate"),
LayerDataTypeDiscriminant::NodeGraphFrame => write!(f, "NodeGraphFrame"),
LayerDataTypeDiscriminant::NodeGraphFrame => write!(f, "Node Graph Frame"),
}
}
}

View file

@ -1,6 +1,6 @@
use crate::boolean_ops::BooleanOperation as BooleanOperationType;
use crate::layers::blend_mode::BlendMode;
use crate::layers::imaginate_layer::{ImaginateSamplingMethod, ImaginateStatus};
use crate::layers::imaginate_layer::{ImaginateMaskFillContent, ImaginateMaskPaintMode, ImaginateSamplingMethod, ImaginateStatus};
use crate::layers::layer_info::Layer;
use crate::layers::style::{self, Stroke};
use crate::layers::vector::consts::ManipulatorType;
@ -95,6 +95,18 @@ pub enum Operation {
path: Vec<LayerId>,
prompt: String,
},
ImaginateSetMaskBlurPx {
path: Vec<LayerId>,
mask_blur_px: u32,
},
ImaginateSetMaskFillContent {
path: Vec<LayerId>,
mode: ImaginateMaskFillContent,
},
ImaginateSetMaskPaintMode {
path: Vec<LayerId>,
paint: ImaginateMaskPaintMode,
},
ImaginateSetCfgScale {
path: Vec<LayerId>,
cfg_scale: f64,
@ -118,6 +130,10 @@ pub enum Operation {
path: Vec<LayerId>,
denoising_strength: f64,
},
ImaginateSetLayerPath {
path: Vec<LayerId>,
layer_path: Option<Vec<LayerId>>,
},
ImaginateSetUseImg2Img {
path: Vec<LayerId>,
use_img2img: bool,