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

@@ -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" },