diff options
author | bobzel <zzzman@gmail.com> | 2025-04-21 13:48:58 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2025-04-21 13:48:58 -0400 |
commit | 17e24e780b54f2f7015c0ca955c3aa5091bba19c (patch) | |
tree | b13002c92d58cb52a02b46e4e1d578f1d57125f2 /src/client/views/nodes/ImageBox.tsx | |
parent | 22a40443193320487c27ce02bd3f134d13cb7d65 (diff) | |
parent | 1f294ef4a171eec72a069a9503629eaf7975d983 (diff) |
merged with master and cleaned up outpainting a bit.
Diffstat (limited to 'src/client/views/nodes/ImageBox.tsx')
-rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 676 |
1 files changed, 224 insertions, 452 deletions
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 033b0b5a2..c3df82611 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,4 +1,4 @@ -import { Button, Colors, Size, Type } from '@dash/components'; +import { Button, Colors, EditableText, Size, Type } from '@dash/components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Slider, Tooltip } from '@mui/material'; import axios from 'axios'; @@ -8,16 +8,17 @@ import { extname } from 'path'; import * as React from 'react'; import { AiOutlineSend } from 'react-icons/ai'; import ReactLoading from 'react-loading'; -import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils'; +import { ClientUtils, DashColor, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; +import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; +import { ComputedField } from '../../../fields/ScriptField'; import { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { Upload } from '../../../server/SharedMediaTypes'; import { emptyFunction } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -45,9 +46,8 @@ import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; -import { RichTextField } from '../../../fields/RichTextField'; -import { List } from '../../../fields/List'; +const DefaultPath = '/assets/unknown-file-icon-hi.png'; export class ImageEditorData { // eslint-disable-next-line no-use-before-define private static _instance: ImageEditorData; @@ -152,11 +152,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }, { fireImmediately: true, delay: 1000 } ); - const { layoutDoc } = this; this._disposers.path = reaction( - () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width) }), - ({ nativeSize, width }) => { - if ((layoutDoc === this.layoutDoc && !this.layoutDoc._layout_nativeDimEditable) || !this.layoutDoc._height) { + () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width), height: this.layoutDoc._height }), + ({ nativeSize, width, height }) => { + if (!this.layoutDoc._layout_nativeDimEditable || !height) { this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth; } }, @@ -224,19 +223,21 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { if (de.metaKey || hitDropTarget(e.target as HTMLElement, this._overlayIconRef.current)) { added = de.complete.docDragData.droppedDocuments.reduce((last: boolean, drop: Doc) => { this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate:hover'; + this.Document.$backgroundColor_alternate = ComputedField.MakeFunction('this.data_alternates[0]?.$backgroundColor'); return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', drop); }, true); } else if (hitDropTarget(e.target as HTMLElement, this._regenerateIconRef.current)) { this._regenerateLoading = true; - const drag = de.complete.docDragData?.draggedDocuments.lastElement(); - const dragField = drag[Doc.LayoutFieldKey(drag)]; + const drag = de.complete.docDragData.draggedDocuments.lastElement(); + const dragField = drag[Doc.LayoutDataKey(drag)]; + const descText = RTFCast(dragField)?.Text || StrCast(dragField) || RTFCast(drag.text)?.Text || StrCast(drag.text) || StrCast(this.Document.title); const oldPrompt = StrCast(this.Document.ai_firefly_prompt, StrCast(this.Document.title)); const newPrompt = (text: string) => (oldPrompt ? `${oldPrompt} ~~~ ${text}` : text); - DrawingFillHandler.drawingToImage(this.Document, 100, newPrompt(dragField instanceof RichTextField ? dragField.Text : ''), drag)?.then(action(() => (this._regenerateLoading = false))); + DrawingFillHandler.drawingToImage(this.Document, 90, newPrompt(descText), drag)?.then(action(() => (this._regenerateLoading = false))); added = false; } else if (de.altKey || !this.dataDoc[this.fieldKey]) { const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; - const targetField = Doc.LayoutFieldKey(layoutDoc); + const targetField = Doc.LayoutDataKey(layoutDoc); const targetDoc = layoutDoc[DocData]; if (targetDoc[targetField] instanceof ImageField) { added = true; @@ -266,17 +267,17 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const nw = nscale / oldnativeWidth; this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nw; this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nw; - this.dataDoc._freeform_panX = nw * NumCast(this.dataDoc._freeform_panX); - this.dataDoc._freeform_panY = nw * NumCast(this.dataDoc._freeform_panY); - this.dataDoc._freeform_panX_max = this.dataDoc._freeform_panX_max ? nw * NumCast(this.dataDoc._freeform_panX_max) : undefined; - this.dataDoc._freeform_panX_min = this.dataDoc._freeform_panX_min ? nw * NumCast(this.dataDoc._freeform_panX_min) : undefined; - this.dataDoc._freeform_panY_max = this.dataDoc._freeform_panY_max ? nw * NumCast(this.dataDoc._freeform_panY_max) : undefined; - this.dataDoc._freeform_panY_min = this.dataDoc._freeform_panY_min ? nw * NumCast(this.dataDoc._freeform_panY_min) : undefined; + this.dataDoc.freeform_panX = nw * NumCast(this.dataDoc.freeform_panX); + this.dataDoc.freeform_panY = nw * NumCast(this.dataDoc.freeform_panY); + this.dataDoc.freeform_panX_max = this.dataDoc.freeform_panX_max ? nw * NumCast(this.dataDoc.freeform_panX_max) : undefined; + this.dataDoc.freeform_panX_min = this.dataDoc.freeform_panX_min ? nw * NumCast(this.dataDoc.freeform_panX_min) : undefined; + this.dataDoc.freeform_panY_max = this.dataDoc.freeform_panY_max ? nw * NumCast(this.dataDoc.freeform_panY_max) : undefined; + this.dataDoc.freeform_panY_min = this.dataDoc.freeform_panY_min ? nw * NumCast(this.dataDoc.freeform_panY_min) : undefined; const newnativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']); DocListCast(this.dataDoc[this.annotationKey]).forEach(doc => { doc.x = (NumCast(doc.x) / oldnativeWidth) * newnativeWidth; doc.y = (NumCast(doc.y) / oldnativeWidth) * newnativeWidth; - if (!RTFCast(doc[Doc.LayoutFieldKey(doc)])) { + if (!RTFCast(doc[Doc.LayoutDataKey(doc)])) { doc.width = (NumCast(doc.width) / oldnativeWidth) * newnativeWidth; doc.height = (NumCast(doc.height) / oldnativeWidth) * newnativeWidth; } @@ -298,40 +299,38 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { crop = (region: Doc | undefined, addCrop?: boolean) => { if (!region) return undefined; const cropping = Doc.MakeCopy(region, true); - const regionData = region[DocData]; - regionData.lockedPosition = true; - regionData.title = 'region:' + this.Document.title; - regionData.followLinkToggle = true; + region.$lockedPosition = true; + region.$title = 'region:' + this.Document.title; + region.$followLinkToggle = true; this.addDocument(region); const anchx = NumCast(cropping.x); const anchy = NumCast(cropping.y); const anchw = NumCast(cropping._width); const anchh = NumCast(cropping._height); const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) / anchh; - cropping.title = 'crop: ' + this.Document.title; cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); cropping.y = NumCast(this.Document.y); + cropping.onClick = undefined; cropping._width = anchw * (this._props.NativeDimScaling?.() || 1); cropping._height = anchh * (this._props.NativeDimScaling?.() || 1); - cropping.onClick = undefined; - const croppingProto = cropping[DocData]; - croppingProto.annotationOn = undefined; - croppingProto.isDataDoc = true; - croppingProto.backgroundColor = undefined; - croppingProto.proto = Cast(this.Document.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO - croppingProto.type = DocumentType.IMG; - croppingProto.layout = ImageBox.LayoutString('data'); - croppingProto.data = ObjectField.MakeCopy(this.dataDoc[this.fieldKey] as ObjectField); - croppingProto.data_nativeWidth = anchw; - croppingProto.data_nativeHeight = anchh; - croppingProto.freeform_scale = viewScale; - croppingProto.freeform_panX = anchx / viewScale; - croppingProto.freeform_panY = anchy / viewScale; - croppingProto.freeform_scale_min = viewScale; - croppingProto.freeform_panX_min = anchx / viewScale; - croppingProto.freeform_panX_max = anchw / viewScale; - croppingProto.freeform_panY_min = anchy / viewScale; - croppingProto.freeform_panY_max = anchh / viewScale; + cropping.$title = 'crop: ' + this.Document.title; + cropping.$annotationOn = undefined; + cropping.$isDataDoc = true; + cropping.$backgroundColor = undefined; + cropping.$proto = Cast(this.Document.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO + cropping.$type = DocumentType.IMG; + cropping.$layout = ImageBox.LayoutString('data'); + cropping.$data = ObjectField.MakeCopy(this.dataDoc[this.fieldKey] as ObjectField); + cropping.$data_nativeWidth = anchw; + cropping.$data_nativeHeight = anchh; + cropping.$freeform_scale = viewScale; + cropping.$freeform_panX = anchx / viewScale; + cropping.$freeform_panY = anchy / viewScale; + cropping.$freeform_scale_min = viewScale; + cropping.$freeform_panX_min = anchx / viewScale; + cropping.$freeform_panX_max = anchw / viewScale; + cropping.$freeform_panY_min = anchy / viewScale; + cropping.$freeform_panY_max = anchh / viewScale; if (addCrop) { DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' }); cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); @@ -354,339 +353,141 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }); @observable _showOutpaintPrompt: boolean = false; -@observable _outpaintPromptInput: string = 'Extend this image naturally with matching content'; - -@action -openOutpaintPrompt = () => { - this._showOutpaintPrompt = true; -}; - -@action -closeOutpaintPrompt = () => { - this._showOutpaintPrompt = false; -}; + @observable _outpaintPromptInput: string = 'Extend this image naturally with matching content'; -@action -handlePromptChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this._outpaintPromptInput = e.target.value; -}; - -@action -submitOutpaintPrompt = () => { - this.closeOutpaintPrompt(); - this.processOutpaintingWithPrompt(this._outpaintPromptInput); -}; + @action + openOutpaintPrompt = () => { + this._showOutpaintPrompt = true; + }; -@action -processOutpaintingWithPrompt = async (customPrompt: string) => { - const field = Cast(this.dataDoc[this.fieldKey], ImageField); - if (!field) return; + @action + closeOutpaintPrompt = () => { + this._showOutpaintPrompt = false; + }; - const origWidth = NumCast(this.Document._outpaintingOriginalWidth); - const origHeight = NumCast(this.Document._outpaintingOriginalHeight); + @action + handlePromptChange = (val: string | number) => { + this._outpaintPromptInput = val; + }; - if (!origWidth || !origHeight) { - console.error('Original dimensions (_outpaintingOriginalWidth/_outpaintingOriginalHeight) not set. Ensure resizeViewForOutpainting was called first.'); - return; - } + @action + submitOutpaintPrompt = () => { + this.closeOutpaintPrompt(); + this.processOutpaintingWithPrompt(this._outpaintPromptInput); + }; - // Set flag that outpainting is in progress - this._outpaintingInProgress = true; + @action + processOutpaintingWithPrompt = async (customPrompt: string) => { + const field = Cast(this.dataDoc[this.fieldKey], ImageField); + if (!field) return; - try { - const currentPath = this.choosePath(field.url); - const newWidth = NumCast(this.Document._width); - const newHeight = NumCast(this.Document._height); + const origWidth = NumCast(this.Document._outpaintingOriginalWidth); + const origHeight = NumCast(this.Document._outpaintingOriginalHeight); - // Revert dimensions if prompt is blank (acts like Cancel) - if (!customPrompt) { - this.Document._width = origWidth; - this.Document._height = origHeight; - this._outpaintingInProgress = false; + if (!origWidth || !origHeight) { + console.error('Original dimensions (_outpaintingOriginalWidth/_outpaintingOriginalHeight) not set. Ensure resizeViewForOutpainting was called first.'); return; } - // Optional: add loading indicator - const loadingOverlay = document.createElement('div'); - loadingOverlay.style.position = 'absolute'; - loadingOverlay.style.top = '0'; - loadingOverlay.style.left = '0'; - loadingOverlay.style.width = '100%'; - loadingOverlay.style.height = '100%'; - loadingOverlay.style.background = 'rgba(0,0,0,0.5)'; - loadingOverlay.style.display = 'flex'; - loadingOverlay.style.justifyContent = 'center'; - loadingOverlay.style.alignItems = 'center'; - loadingOverlay.innerHTML = '<div style="color: white; font-size: 16px;">Generating outpainted image...</div>'; - this._mainCont?.appendChild(loadingOverlay); - - const response = await Networking.PostToServer('/outpaintImage', { - imageUrl: currentPath, - prompt: customPrompt, - originalDimensions: { width: origWidth, height: origHeight }, - newDimensions: { width: newWidth, height: newHeight }, - }); - - if (response && typeof response === 'object' && 'url' in response && typeof response.url === 'string') { - console.log('Received outpainted image:', response.url); + // Set flag that outpainting is in progress + this._outpaintingInProgress = true; - if (!this.dataDoc[this.fieldKey + '_alternates']) { - this.dataDoc[this.fieldKey + '_alternates'] = new List<Doc>(); + try { + const currentPath = this.choosePath(field.url); + const newWidth = NumCast(this.Document._width); + const newHeight = NumCast(this.Document._height); + + // Revert dimensions if prompt is blank (acts like Cancel) + if (!customPrompt) { + this.Document._width = origWidth; + this.Document._height = origHeight; + this._outpaintingInProgress = false; + return; } - const originalDoc = Docs.Create.ImageDocument(field.url.href, { - title: `Original: ${this.Document.title}`, - _nativeWidth: Doc.NativeWidth(this.dataDoc), - _nativeHeight: Doc.NativeHeight(this.dataDoc), + // Optional: add loading indicator + const loadingOverlay = document.createElement('div'); + loadingOverlay.style.position = 'absolute'; + loadingOverlay.style.top = '0'; + loadingOverlay.style.left = '0'; + loadingOverlay.style.width = '100%'; + loadingOverlay.style.height = '100%'; + loadingOverlay.style.background = 'rgba(0,0,0,0.5)'; + loadingOverlay.style.display = 'flex'; + loadingOverlay.style.justifyContent = 'center'; + loadingOverlay.style.alignItems = 'center'; + loadingOverlay.innerHTML = '<div style="color: white; font-size: 16px;">Generating outpainted image...</div>'; + this._mainCont?.appendChild(loadingOverlay); + + const response = await Networking.PostToServer('/outpaintImage', { + imageUrl: currentPath, + prompt: customPrompt, + originalDimensions: { width: origWidth, height: origHeight }, + newDimensions: { width: newWidth, height: newHeight }, }); - Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', originalDoc); - - // Replace with new outpainted image - this.dataDoc[this.fieldKey] = new ImageField(response.url); - - Doc.SetNativeWidth(this.dataDoc, newWidth); - Doc.SetNativeHeight(this.dataDoc, newHeight); - - this.Document.ai = true; - this.Document.ai_outpainted = true; - this.Document.ai_outpaint_prompt = customPrompt; - } else { - console.error('Unexpected API response:', response); - this.Document._width = origWidth; - this.Document._height = origHeight; - alert('Failed to receive a valid image URL from server.'); - } - - this._mainCont?.removeChild(loadingOverlay); - } catch (error) { - console.error('Error during outpainting:', error); - this.Document._width = origWidth; - this.Document._height = origHeight; - alert('An error occurred while outpainting. Please try again.'); - } finally { - this._outpaintingInProgress = false; - delete this.Document._originalDims; - } -}; - -processOutpainting = () => { - this.openOutpaintPrompt(); -}; - -componentUI = () => ( - <> - {this._showOutpaintPrompt && ( - <div className="imageBox-regenerate-dialog"> - <h3>Outpaint Image</h3> - <p>Enter a prompt for extending the image:</p> - <input - type="text" - value={this._outpaintPromptInput} - onChange={this.handlePromptChange} - /> - <div className="buttons"> - <button onClick={this.closeOutpaintPrompt}>Cancel</button> - <button className="generate-btn" onClick={this.submitOutpaintPrompt}> - Generate - </button> - </div> - </div> - )} - </> -); - - // // Add this method to process outpainting when resize is complete - // @action - // processOutpainting = async () => { - // const field = Cast(this.dataDoc[this.fieldKey], ImageField); - // if (!field) return; - - // const origWidth = NumCast(this.Document._outpaintingOriginalWidth); - // const origHeight = NumCast(this.Document._outpaintingOriginalHeight); - - // if (!origWidth || !origHeight) { - // console.error('Original dimensions (_outpaintingOriginalWidth/_outpaintingOriginalHeight) not set. Ensure resizeViewForOutpainting was called first.'); - // return; - // } - - // //alert(`Original dimensions: ${origWidth} x ${origHeight}`); - - // // Set flag that outpainting is in progress - // this._outpaintingInProgress = true; - - // try { - // // Get the current path to the image - // const currentPath = this.choosePath(field.url); - - // // Get original and new dimensions for calculating mask - // const newWidth = NumCast(this.Document._width); - // const newHeight = NumCast(this.Document._height); - - // // Optional: Ask user for a prompt to guide the outpainting - // let prompt = 'Extend this image naturally with matching content'; - // const customPrompt = await new Promise<string>(resolve => { - // const dialog = document.createElement('div'); - // Object.assign(dialog.style, { - // position: 'fixed', - // top: '50%', - // left: '50%', - // transform: 'translate(-50%, -50%)', - // background: 'white', - // padding: '20px', - // borderRadius: '8px', - // boxShadow: '0 4px 12px rgba(0,0,0,0.2)', - // zIndex: '10000', - // }); - - // const title = document.createElement('h3'); - // title.style.marginTop = '0'; - // title.textContent = 'Outpaint Image'; - - // const description = document.createElement('p'); - // description.textContent = 'Enter a prompt for extending the image:'; + if (response && typeof response === 'object' && 'url' in response && typeof response.url === 'string') { + console.log('Received outpainted image:', response.url); - // const input = document.createElement('input'); - // input.id = 'outpaint-prompt'; - // input.type = 'text'; - // input.value = 'Extend this image naturally with matching content'; - // Object.assign(input.style, { - // width: '300px', - // padding: '8px', - // marginBottom: '10px', - // }); - - // const buttonContainer = document.createElement('div'); - // Object.assign(buttonContainer.style, { - // display: 'flex', - // justifyContent: 'flex-end', - // gap: '10px', - // }); - - // const cancelButton = document.createElement('button'); - // cancelButton.textContent = 'Cancel'; - - // const confirmButton = document.createElement('button'); - // confirmButton.textContent = 'Generate'; - // Object.assign(confirmButton.style, { - // background: '#0078d4', - // color: 'white', - // border: 'none', - // padding: '8px 16px', - // }); - - // buttonContainer.appendChild(cancelButton); - // buttonContainer.appendChild(confirmButton); - - // dialog.appendChild(title); - // dialog.appendChild(description); - // dialog.appendChild(input); - // dialog.appendChild(buttonContainer); - - // document.body.appendChild(dialog); - - // cancelButton.onclick = () => { - // document.body.removeChild(dialog); - // resolve(''); - // }; - - // confirmButton.onclick = () => { - // const promptValue = input.value; - // document.body.removeChild(dialog); - // resolve(promptValue); - // }; - // }); - - // // If user cancelled, reset dimensions to original - // if (!customPrompt) { - // this.Document._width = origWidth; - // this.Document._height = origHeight; - // this._outpaintingInProgress = false; - // return; - // } - - // // Show loading indicator - // const loadingOverlay = document.createElement('div'); - // loadingOverlay.style.position = 'absolute'; - // loadingOverlay.style.top = '0'; - // loadingOverlay.style.left = '0'; - // loadingOverlay.style.width = '100%'; - // loadingOverlay.style.height = '100%'; - // loadingOverlay.style.background = 'rgba(0,0,0,0.5)'; - // loadingOverlay.style.display = 'flex'; - // loadingOverlay.style.justifyContent = 'center'; - // loadingOverlay.style.alignItems = 'center'; - // loadingOverlay.innerHTML = '<div style="color: white; font-size: 16px;">Generating outpainted image...</div>'; - // this._mainCont?.appendChild(loadingOverlay); - - // // Call the outpaint API - // const response = await Networking.PostToServer('/outpaintImage', { - // imageUrl: currentPath, - // prompt: customPrompt, - // originalDimensions: { - // width: origWidth, - // height: origHeight, - // }, - // newDimensions: { - // width: newWidth, - // height: newHeight, - // }, - // }); - // if (response && typeof response === 'object' && 'url' in response && typeof response.url === 'string') { - // console.log('Response is valid and contains URL:', response.url); - // } else { - // console.error('Unexpected API response:', response); - // alert('Failed to receive a valid image URL from server.'); - // } + if (!this.dataDoc[this.fieldKey + '_alternates']) { + this.dataDoc[this.fieldKey + '_alternates'] = new List<Doc>(); + } - // if (response && 'url' in response && typeof response.url === 'string') { - // // Save the original image as an alternate - // if (!this.dataDoc[this.fieldKey + '_alternates']) { - // this.dataDoc[this.fieldKey + '_alternates'] = new List<Doc>(); - // } + const originalDoc = Docs.Create.ImageDocument(field.url.href, { + title: `Original: ${this.Document.title}`, + _nativeWidth: Doc.NativeWidth(this.dataDoc), + _nativeHeight: Doc.NativeHeight(this.dataDoc), + }); - // // Create a copy of the current image as an alternate - // const originalDoc = Docs.Create.ImageDocument(field.url.href, { - // title: `Original: ${this.Document.title}`, - // _nativeWidth: Doc.NativeWidth(this.dataDoc), - // _nativeHeight: Doc.NativeHeight(this.dataDoc), - // }); + Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', originalDoc); - // // Add to alternates - // Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', originalDoc); + // Replace with new outpainted image + this.dataDoc[this.fieldKey] = new ImageField(response.url); - // // Update the image with the outpainted version - // this.dataDoc[this.fieldKey] = new ImageField(response.url); + Doc.SetNativeWidth(this.dataDoc, newWidth); + Doc.SetNativeHeight(this.dataDoc, newHeight); - // // Update native dimensions - // Doc.SetNativeWidth(this.dataDoc, newWidth); - // Doc.SetNativeHeight(this.dataDoc, newHeight); + this.Document.$ai = true; + this.Document.$ai_outpainted = true; + this.Document.$ai_outpaint_prompt = customPrompt; + } else { + console.error('Unexpected API response:', response); + this.Document._width = origWidth; + this.Document._height = origHeight; + alert('Failed to receive a valid image URL from server.'); + } - // // Add AI metadata - // this.Document.ai = true; - // this.Document.ai_outpainted = true; - // this.Document.ai_outpaint_prompt = customPrompt; - // } else { - // // If failed, revert to original dimensions - // this.Document._width = origWidth; - // this.Document._height = origHeight; - // alert('Failed to outpaint image. Please try again.'); - // } + this._mainCont?.removeChild(loadingOverlay); + } catch (error) { + console.error('Error during outpainting:', error); + this.Document._width = origWidth; + this.Document._height = origHeight; + alert('An error occurred while outpainting. Please try again.'); + } finally { + this._outpaintingInProgress = false; + } + }; - // // Remove loading overlay - // this._mainCont?.removeChild(loadingOverlay); - // } catch (error) { - // console.error('Error during outpainting:', error); - // // Revert to original dimensions on error - // this.Document._width = origWidth; - // this.Document._height = origHeight; - // alert('An error occurred while outpainting. Please try again.'); - // } finally { - // // Clear the outpainting flags - // this._outpaintingInProgress = false; - // delete this.Document._originalDims; - // } - // }; + processOutpainting = () => this.openOutpaintPrompt(); + + componentUI = () => + !this._showOutpaintPrompt ? null : ( + <div className="imageBox-regenerate-dialog" style={{ backgroundColor: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> + <h3>Outpaint Image</h3> + <EditableText + placeholder="Enter a prompt for extending the image:" + setVal={val => this.handlePromptChange(val)} + val={this._outpaintPromptInput} + type={Type.TERT} + color={SettingsManager.userColor} + background={SettingsManager.userBackgroundColor} + /> + <div className="buttons"> + <Button text="Cancel" type={Type.TERT} onClick={this.closeOutpaintPrompt} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} /> + <Button text="Generate" type={Type.TERT} onClick={this.submitOutpaintPrompt} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} /> + </div> + </div> + ); specificContextMenu = (): void => { const field = Cast(this.dataDoc[this.fieldKey], ImageField); @@ -698,9 +499,9 @@ componentUI = () => ( funcs.push({ description: 'GetImageText', event: () => { Networking.PostToServer('/queryFireflyImageText', { file: (file => { - const ext = extname(file); - return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); - })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href), + const ext = file ? extname(file) : ''; + return file?.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); + })(ImageCast(this.Document[Doc.LayoutDataKey(this.Document)])?.url.href), }).then(text => alert(text)); }, icon: 'expand-arrows-alt', @@ -726,14 +527,8 @@ componentUI = () => ( icon: this.Document.savedAsSticker ? 'clipboard-check' : 'file-arrow-down', }); // Add new outpainting option - funcs.push({ - description: 'Outpaint Image', - event: () => { - this.openOutpaintPrompt(); - }, - icon: 'brush', - }); - + funcs.push({ description: 'Outpaint Image', event: () => this.openOutpaintPrompt(), icon: 'brush' }); + // Add outpainting history option if the image was outpainted this.Document.ai_outpainted && funcs.push({ @@ -752,15 +547,14 @@ componentUI = () => ( }; // updateIcon = () => new Promise<void>(res => res()); - updateIcon = (usePanelDimensions?: boolean) => { - const contentDiv = this._mainCont; - return !contentDiv + updateIcon = (/* usePanelDimensions?: boolean */) => + !this._mainCont || !DocListCast(this.dataDoc[this.annotationKey]).length ? new Promise<void>(res => res()) : UpdateIcon( this.layoutDoc[Id] + '_icon_' + new Date().getTime(), - contentDiv, - usePanelDimensions || true ? this._props.PanelWidth() : NumCast(this.layoutDoc._width), - usePanelDimensions || true ? this._props.PanelHeight() : NumCast(this.layoutDoc._height), + this._mainCont, + this._props.PanelWidth(), // usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width), + this._props.PanelHeight(), // usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height), this._props.PanelWidth(), this._props.PanelHeight(), 0, @@ -773,14 +567,13 @@ componentUI = () => ( this.dataDoc.icon_nativeHeight = nativeHeight; } ); - }; choosePath = (url: URL) => { if (!url?.href) return ''; const lower = url.href.toLowerCase(); if (url.protocol === 'data') return url.href; if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf('dashblobstore') === -1) return ClientUtils.CorsProxy(url.href); - if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower) || lower.endsWith('/assets/unknown-file-icon-hi.png')) return `/assets/unknown-file-icon-hi.png`; + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower) || lower.endsWith(DefaultPath)) return DefaultPath; const ext = extname(url.href); return url.href.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); @@ -794,7 +587,7 @@ componentUI = () => ( @computed get nativeSize() { TraceMobx(); - if (this.paths.length && this.paths[0].includes('icon-hi')) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0 }; + if (this.paths.length && this.paths[0].includes(DefaultPath)) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0 }; const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500)); const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500)); const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '_nativeOrientation'], 1); @@ -804,15 +597,15 @@ componentUI = () => ( /** * How much the content of the view is being scaled based on its nesting and its fit-to-width settings */ - @computed get viewScaling() { return this.ScreenToLocalBoxXf().Scale * ( this._props.NativeDimScaling?.() || 1); } // prettier-ignore + @computed get viewScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.()??1); } // prettier-ignore /** * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size. */ - @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth, 0.5 * Math.min(NumCast(this.Document.width)))* this.viewScaling; } // prettier-ignore + @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth, 0.2 * this._props.PanelWidth())*this.viewScaling; } // prettier-ignore /** * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content */ - @computed get uiBtnScaling() { return Math.min(this.maxWidgetSize / this._sideBtnWidth, 1); } // prettier-ignore + @computed get uiBtnScaling() { return Math.min(1/(this._props.NativeDimScaling?.()??1), this.maxWidgetSize / this._sideBtnWidth); } // prettier-ignore @computed get overlayImageIcon() { const usePath = this.layoutDoc[`_${this.fieldKey}_usePath`]; @@ -859,30 +652,33 @@ componentUI = () => ( } @computed get regenerateImageIcon() { return ( - <div - className="imageBox-regenerateDropTarget" - ref={this._regenerateIconRef} - onClick={() => DocumentView.showDocument(DocCast(this.Document.ai_firefly_generatedDocs), { openLocation: OpenWhere.addRight })} - style={{ - display: (this._props.isContentActive() && (SnappingManager.CanEmbed || this.Document.ai_firefly_generatedDocs)) || this._regenerateLoading ? 'block' : 'none', - transform: `scale(${this.uiBtnScaling})`, - width: this._sideBtnWidth, - height: this._sideBtnWidth, - background: 'transparent', - // color: SettingsManager.userBackgroundColor, - }}> - {this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width="100%" height="100%" /> : <FontAwesomeIcon icon="portrait" color={SettingsManager.userColor} size="lg" />} - </div> + <Tooltip title={'click to show AI generations. Drop an image on to create a new generation'}> + <div + className="imageBox-regenerateDropTarget" + ref={this._regenerateIconRef} + onClick={() => DocCast(this.Document.ai_firefly_generatedDocs) && DocumentView.showDocument(DocCast(this.Document.ai_firefly_generatedDocs)!, { openLocation: OpenWhere.addRight })} + style={{ + display: (this._props.isContentActive() && (SnappingManager.CanEmbed || this.Document.ai_firefly_generatedDocs)) || this._regenerateLoading ? 'block' : 'none', + transform: `scale(${this.uiBtnScaling})`, + width: this._sideBtnWidth, + height: this._sideBtnWidth, + background: 'black', + color: 'white', + // color: SettingsManager.userBackgroundColor, + }}> + {this._regenerateLoading ? <ReactLoading type="spin" width="100%" height="100%" /> : <FontAwesomeIcon icon="portrait" size="lg" />} + </div> + </Tooltip> ); } @computed get paths() { const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey])); // retrieve the primary image URL that is being rendered from the data doc const alts = DocListCast(this.dataDoc[this.fieldKey + '_alternates']); // retrieve alternate documents that may be rendered as alternate images - const defaultUrl = new URL(ClientUtils.prepend('/assets/unknown-file-icon-hi.png')); + const defaultUrl = new URL(ClientUtils.prepend(DefaultPath)); const altpaths = alts - ?.map(doc => (doc instanceof Doc ? (ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url ?? defaultUrl) : defaultUrl)) + ?.map(doc => (doc instanceof Doc ? (ImageCast(doc[Doc.LayoutDataKey(doc)])?.url ?? defaultUrl) : defaultUrl)) .filter(url => url) .map(url => this.choosePath(url)) ?? []; // acc ess the primary layout data of the alternate documents const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; @@ -892,11 +688,15 @@ componentUI = () => ( @computed get content() { TraceMobx(); + const usePath = StrCast(this.Document[this.fieldKey + '_usePath']); + const alternate = '_' + usePath.replace(':hover', ''); + const altColor = DashColor(StrCast(this.Document[this.fieldKey + alternate], StrCast(this.Document['$backgroundColor' + alternate], 'black'))); + const backColor = DashColor((this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string) ?? Colors.WHITE); // allow use case where the image is transparent when the alpha value is to smallest possible value from UI (alpha = 1 out of 255) const backAlpha = backColor.alpha() < 0.015 && backColor.alpha() > 0 ? backColor.alpha() : 1; - const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0]; - const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement(); + const fadepath = this.layoutDoc.hideImage ? '' : this.paths[0]; + const srcpath = this.layoutDoc.hideImage ? '' : this.paths.lastElement(); const { nativeWidth, nativeHeight /* , nativeOrientation */ } = this.nativeSize; const rotation = NumCast(this.dataDoc[this.fieldKey + '_rotation']); const aspect = rotation % 180 ? nativeHeight / nativeWidth : 1; @@ -930,15 +730,15 @@ componentUI = () => ( key="paths" src={srcpath} style={{ transform, transformOrigin }} - onError={action(e => { - this._error = e.toString(); - })} + onError={action(e => (this._error = e.toString()))} draggable={false} width={nativeWidth} /> {fadepath === srcpath ? null : ( - <div className={`imageBox-fadeBlocker${this.usingAlternate ? '-hover' : ''}`} style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}> - <img alt="" className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={20} /> + <div + className={`imageBox-fadeBlocker${!this.usingAlternate ? '-hover' : ''}`} + style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms'), background: altColor.alpha() === 0 ? 'transparent' : altColor.toString() }}> + <img alt="" className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} /> </div> )} </div> @@ -1001,7 +801,7 @@ componentUI = () => ( onClick={action(async () => { this._regenerateLoading = true; if (this._fireflyRefStrength) { - DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title), this.Document)?.then(action(() => (this._regenerateLoading = false))); + DrawingFillHandler.drawingToImage(this.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title))?.then(action(() => (this._regenerateLoading = false))); } else { SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput || StrCast(this.Document.title), true).then( action(newImgs => { @@ -1010,7 +810,7 @@ componentUI = () => ( const url = firstImg.pathname; const imgField = new ImageField(url); this._prevImgs.length === 0 && - this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname }); + this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field?.url.pathname ?? '' }); this._prevImgs.unshift({ prompt: firstImg.prompt, seed: firstImg.seed, pathname: url }); this.dataDoc.ai_firefly_history = JSON.stringify(this._prevImgs); this.dataDoc.ai_firefly_prompt = firstImg.prompt; @@ -1055,21 +855,24 @@ componentUI = () => ( } screenToLocalTransform = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this.ScreenToLocalBoxXf().Scale); marqueeDown = (e: React.PointerEvent) => { - if (!this.dataDoc[this.fieldKey]) { - this.chooseImage(); - } else if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) { - setupMoveUpEvents( - this, - e, - action(moveEv => { - MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this.marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); - return true; - }), - returnFalse, - () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), - false - ); + if (!e.altKey && e.button === 0) { + if (NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) { + setupMoveUpEvents( + this, + e, + action(moveEv => { + MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + this.marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); + return true; + }), + returnFalse, + () => { + if (!this.dataDoc[this.fieldKey]) this.chooseImage(); + else MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + }, + false + ); + } } }; @action @@ -1094,8 +897,6 @@ componentUI = () => ( TraceMobx(); const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string; const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this._props.NativeDimScaling?.() || 1)}px` : borderRad; - const alts = DocListCast(this.dataDoc[this.fieldKey + '_alternates']); - const doc = this.usingAlternate ? (alts.lastElement() ?? this.Document) : this.Document; return ( <> <div @@ -1121,7 +922,7 @@ componentUI = () => ( <CollectionFreeFormView ref={this._ffref} {...this._props} - Document={doc} + Document={this.Document} setContentViewBox={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} @@ -1144,41 +945,12 @@ componentUI = () => ( addDocument={this.addDocument}> {this.content} </CollectionFreeFormView> - {this.Loading ? ( - <div className="loading-spinner" style={{ position: 'absolute' }}> - <ReactLoading type="spin" height={50} width={50} color={'blue'} /> + {this._outpaintingInProgress && ( + <div className="imageBox-outpaintingSpinner"> + <ReactLoading type="spin" color="#666" height={60} width={60} /> </div> - ) : null} - {this.regenerateImageIcon} - {this.overlayImageIcon} - {this.annotationLayer} - {!this._mainCont || !this.DocumentView || !this._annotationLayer.current ? null : ( - <MarqueeAnnotator - Document={this.Document} - ref={this.marqueeref} - scrollTop={0} - annotationLayerScrollTop={0} - scaling={returnOne} - annotationLayerScaling={this._props.NativeDimScaling} - screenTransform={this.DocumentView().screenToViewTransform} - docView={this.DocumentView} - addDocument={this.addDocument} - finishMarquee={this.finishMarquee} - savedAnnotations={this.savedAnnotations} - selectionText={returnEmptyString} - annotationLayer={this._annotationLayer.current} - marqueeContainer={this._mainCont} - highlightDragSrcColor="" - anchorMenuCrop={this.crop} - // anchorMenuFlashcard={() => this.getImageDesc()} - /> )} </div> - {this._outpaintingInProgress && ( - <div className="imageBox-outpaintingSpinner"> - <ReactLoading type="spin" color="#666" height={60} width={60} /> - </div> - )} </> ); } |