aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/ImageBox.tsx
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2025-04-21 13:48:58 -0400
committerbobzel <zzzman@gmail.com>2025-04-21 13:48:58 -0400
commit17e24e780b54f2f7015c0ca955c3aa5091bba19c (patch)
treeb13002c92d58cb52a02b46e4e1d578f1d57125f2 /src/client/views/nodes/ImageBox.tsx
parent22a40443193320487c27ce02bd3f134d13cb7d65 (diff)
parent1f294ef4a171eec72a069a9503629eaf7975d983 (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.tsx676
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>
- )}
</>
);
}