From 932ecd6092bd1b0ac3391309a550bac76cfb0e04 Mon Sep 17 00:00:00 2001 From: Sophie Zhang Date: Thu, 29 Jun 2023 13:56:41 -0400 Subject: undo and redo --- src/client/apis/gpt/GPT.ts | 2 +- src/client/views/MainView.tsx | 11 + src/client/views/nodes/ImageBox.tsx | 11 + .../views/nodes/formattedText/FormattedTextBox.tsx | 1 + .../views/nodes/generativeFill/GenerativeFill.scss | 4 +- .../views/nodes/generativeFill/GenerativeFill.tsx | 157 +++++++++++--- .../nodes/generativeFill/GenerativeFillButtons.tsx | 36 ++-- .../generativeFillUtils/ImageHandler.ts | 230 ++++++++++----------- .../generativeFillUtils/generativeFillConstants.ts | 5 +- 9 files changed, 273 insertions(+), 184 deletions(-) (limited to 'src') diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 4b3960902..18222b32a 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -39,7 +39,7 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => { temperature: opts.temp, prompt: `${opts.prompt}${inputText}`, }); - console.log(response.data.choices[0]); + // console.log(response.data.choices[0]); return response.data.choices[0].text; } catch (err) { console.log(err); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index c0c473cfc..1fa1eff84 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -65,6 +65,7 @@ import { PreviewCursor } from './PreviewCursor'; import { PropertiesView } from './PropertiesView'; import { DashboardStyleProvider, DefaultStyleProvider } from './StyleProvider'; import { TopBar } from './topbar/TopBar'; +import GenerativeFill from './nodes/generativeFill/GenerativeFill'; const _global = (window /* browser */ || global) /* node */ as any; @observer @@ -72,6 +73,15 @@ export class MainView extends React.Component { public static Instance: MainView; public static Live: boolean = false; private _docBtnRef = React.createRef(); + // for ai image editor + @observable public imageEditorOpen: boolean = false; + @action public setImageEditorOpen = (open: boolean) => (this.imageEditorOpen = open); + @observable public imageEditorSource: string = ''; + @action public setImageEditorSource = (source: string) => (this.imageEditorSource = source); + @observable public imageRootDoc: Doc | undefined; + @action public setImageRootDoc = (doc: Doc) => (this.imageRootDoc = doc); + @observable public addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined; + @observable public LastButton: Opt; @observable private _windowWidth: number = 0; @observable private _windowHeight: number = 0; @@ -1014,6 +1024,7 @@ export class MainView extends React.Component { {this.snapLines} + ); } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 5b302e7ce..2c6f92681 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -38,6 +38,7 @@ import './ImageBox.scss'; import { PinProps, PresBox } from './trails'; import React = require('react'); import Color = require('color'); +import { MainView } from '../MainView'; export const pageSchema = createSchema({ googlePhotosUrl: 'string', @@ -248,6 +249,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent Utils.CopyText(this.choosePath(field.url)), icon: 'copy' }); + funcs.push({ + description: 'Open Image Editor', + event: action(() => { + MainView.Instance.setImageEditorOpen(true); + MainView.Instance.setImageEditorSource(this.choosePath(field.url)); + MainView.Instance.addDoc = this.props.addDocument; + MainView.Instance.imageRootDoc = this.rootDoc; + }), + icon: 'pencil-alt', + }); if (!Doc.noviceMode) { funcs.push({ description: 'Export to Google Photos', event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: 'caret-square-right' }); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 0dc186eaf..3fa5c099b 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -882,6 +882,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { + console.log(this.gptRes); if (resIndex < this.gptRes.length) { this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + this.gptRes[resIndex]; setTimeout(() => { diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.scss b/src/client/views/nodes/generativeFill/GenerativeFill.scss index 92406ba9d..b1e570cf1 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFill.scss +++ b/src/client/views/nodes/generativeFill/GenerativeFill.scss @@ -6,14 +6,14 @@ $scale: 0.5; position: absolute; top: 0; left: 0; - z-index: 999; + z-index: 9999; height: 100vh; width: 100vw; display: flex; flex-direction: column; overflow: hidden; - .controls { + .generativeFillControls { flex-shrink: 0; height: $navHeight; background-color: #ffffff; diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx index 6dd80a5d1..0df05f4d7 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFill.tsx +++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx @@ -3,14 +3,21 @@ import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler'; import { BrushHandler } from './generativeFillUtils/BrushHandler'; import { IconButton, TextField } from '@mui/material'; import { CursorData, Point } from './generativeFillUtils/generativeFillInterfaces'; -import { activeColor, canvasSize, eraserColor } from './generativeFillUtils/generativeFillConstants'; +import { activeColor, canvasSize, eraserColor, freeformRenderSize } from './generativeFillUtils/generativeFillConstants'; import { PointerHandler } from './generativeFillUtils/PointerHandler'; -import { BsBrush, BsEraser } from 'react-icons/bs'; +import { BsBrush, BsEraser, BsX } from 'react-icons/bs'; import { AiOutlineUpload } from 'react-icons/ai'; import { CiUndo, CiRedo } from 'react-icons/ci'; import Buttons from './GenerativeFillButtons'; -import React from 'react'; +import React = require('react'); import './GenerativeFill.scss'; +import { EditableText } from 'browndash-components'; +import { MainView } from '../../MainView'; +import { Doc } from '../../../../fields/Doc'; +import { Networking } from '../../../Network'; +import { Utils } from '../../../../Utils'; +import { DocUtils, Docs } from '../../../documents/Documents'; +import { NumCast } from '../../../../fields/Types'; /** * For images not 1024x1024 fill in the rest in solid black, or a @@ -33,7 +40,14 @@ interface ImageEdit { children: ImageEdit[]; } -const GenerativeFill = () => { +interface GenerativeFillProps { + imageEditorOpen: boolean; + imageEditorSource: string; + imageRootDoc: Doc | undefined; + addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined; +} + +const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc }: GenerativeFillProps) => { const canvasRef = useRef(null); const canvasBackgroundRef = useRef(null); const drawingAreaRef = useRef(null); @@ -51,8 +65,49 @@ const GenerativeFill = () => { const [loading, setLoading] = useState(false); // used to store the current image loaded to the main canvas const currImg = useRef(null); - // ref to store history - const undoStack = useRef([]); + const originalImg = useRef(null); + // stores history of data urls + const undoStack = useRef([]); + // stores redo stack + const redoStack = useRef([]); + + // Undo and Redo + const handleUndo = () => { + const ctx = ImageUtility.getCanvasContext(canvasRef); + if (!ctx || !currImg.current || !canvasRef.current) return; + + const target = undoStack.current[undoStack.current.length - 1]; + if (!target) { + ImageUtility.drawImgToCanvas(currImg.current, canvasRef); + } else { + redoStack.current = [...redoStack.current, canvasRef.current.toDataURL()]; + const img = new Image(); + img.src = target; + ImageUtility.drawImgToCanvas(img, canvasRef); + undoStack.current = undoStack.current.slice(0, -1); + } + }; + + const handleRedo = () => { + // TODO: handle undo as well + const target = redoStack.current[redoStack.current.length - 1]; + if (!target) { + } else { + const img = new Image(); + img.src = target; + ImageUtility.drawImgToCanvas(img, canvasRef); + redoStack.current = redoStack.current.slice(0, -1); + } + }; + + const handleReset = () => { + if (!canvasRef.current || !currImg.current) return; + const ctx = ImageUtility.getCanvasContext(canvasRef); + if (!ctx) return; + ctx.clearRect(0, 0, canvasSize, canvasSize); + undoStack.current = []; + ImageUtility.drawImgToCanvas(currImg.current, canvasRef, true); + }; // initiate brushing const handlePointerDown = (e: React.PointerEvent) => { @@ -61,6 +116,9 @@ const GenerativeFill = () => { const ctx = ImageUtility.getCanvasContext(canvasRef); if (!ctx) return; + undoStack.current = [...undoStack.current, canvasRef.current.toDataURL()]; + redoStack.current = []; + setIsBrushing(true); const { x, y } = PointerHandler.getPointRelativeToElement(canvas, e, canvasScale); @@ -98,17 +156,13 @@ const GenerativeFill = () => { // first load useEffect(() => { + if (!imageEditorSource || imageEditorSource === '') return; const img = new Image(); - img.src = '/assets/art.jpeg'; + img.src = imageEditorSource; ImageUtility.drawImgToCanvas(img, canvasRef); currImg.current = img; - }, [canvasRef]); - - useEffect(() => { - if (!canvasBackgroundRef.current) return; - const ctx = ImageUtility.getCanvasContext(canvasBackgroundRef); - if (!ctx) return; - }, [canvasBackgroundRef]); + originalImg.current = img; + }, [canvasRef, imageEditorSource]); // handles brush sizing useEffect(() => { @@ -180,16 +234,9 @@ const GenerativeFill = () => { const maskBlob = await ImageUtility.canvasToBlob(canvas); const imgBlob = await ImageUtility.canvasToBlob(ImageUtility.getCanvasImg(img)); - // const res = await ImageUtility.getEdit( - // imgBlob, - // maskBlob, - // input !== "" - // ? input + " in the same style" - // : "Fill in the image in the same style", - // 1 - // ); + const res = await ImageUtility.getEdit(imgBlob, maskBlob, input !== '' ? input + ' in the same style' : 'Fill in the image in the same style', 2); - const res = await ImageUtility.mockGetEdit(); + // const res = await ImageUtility.mockGetEdit(); const { urls } = res as APISuccess; const image = new Image(); image.src = urls[0]; @@ -202,11 +249,47 @@ const GenerativeFill = () => { } }; + const onSave = async () => { + if (!currImg.current || !imageRootDoc) return; + try { + const src = currImg.current.src; + console.log(src); + const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [src] }); + const source = Utils.prepend(result.accessPaths.agnostic.client); + console.log(source); + const newImg = Docs.Create.ImageDocument(source, { + x: NumCast(imageRootDoc.x) + NumCast(imageRootDoc._width) + 20, + y: NumCast(imageRootDoc.y), + _height: freeformRenderSize, + _width: freeformRenderSize, + data_nativeWidth: result.nativeWidth, + data_nativeHeight: result.nativeHeight, + }); + + addDoc?.(newImg); + // Create link between prompt and image + DocUtils.MakeLink(imageRootDoc, newImg, { link_relationship: 'Image Edit' }); + console.log('done'); + } catch (err) { + console.log(err); + } + }; + return ( -
-
+
+

Generative Fill

- +
+ + { + MainView.Instance.setImageEditorOpen(false); + MainView.Instance.setImageEditorSource(''); + setEdits([]); + }}> + + +
{/* Main canvas for editing */}
{ }}> - { setBrushStyle(BrushStyle.SUBTRACT); }}> - + */} {/* Undo and Redo */} - {/* { e.stopPropagation(); - console.log(undoStack.current); + handleUndo(); }} onPointerUp={e => { e.stopPropagation(); }}> - {}}> + { + e.stopPropagation(); + handleRedo(); + }} + onPointerUp={e => { + e.stopPropagation(); + }}> - */} +
{/* Edits box */}
@@ -292,12 +382,15 @@ const GenerativeFill = () => { type="text" label="Prompt" placeholder="Prompt..." + InputLabelProps={{ style: { fontSize: '1.5rem' } }} + inputProps={{ style: { fontSize: '1.5rem' } }} sx={{ backgroundColor: '#ffffff', position: 'absolute', bottom: '1rem', transform: 'translateX(calc(50vw - 50%))', width: 'calc(100vw - 4rem)', + scale: 1.2, }} />
diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx index 348e27a16..e8cb61ab5 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx +++ b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx @@ -3,44 +3,34 @@ import { ImageUtility } from './generativeFillUtils/ImageHandler'; import { canvasSize } from './generativeFillUtils/generativeFillConstants'; import { Oval } from 'react-loader-spinner'; import './GenerativeFillButtons.scss'; -import React from 'react'; +import React = require('react'); +import { Doc } from '../../../../fields/Doc'; interface ButtonContainerProps { canvasRef: React.RefObject; - backgroundref: React.RefObject; currImg: React.MutableRefObject; - undoStack: React.MutableRefObject; getEdit: () => Promise; loading: boolean; + onSave: () => Promise; + onReset: () => void; } -const Buttons = ({ canvasRef, backgroundref, currImg, undoStack, loading, getEdit }: ButtonContainerProps) => { - const handleReset = () => { - if (!canvasRef.current || !currImg.current) return; - const ctx = ImageUtility.getCanvasContext(canvasRef); - if (!ctx) return; - ctx.clearRect(0, 0, canvasSize, canvasSize); - ImageUtility.drawImgToCanvas(currImg.current, canvasRef, true); +const Buttons = ({ canvasRef, currImg, loading, getEdit, onSave, onReset }: ButtonContainerProps) => { + const handleSave = () => { + onSave(); }; return (
- - + + {/* - {/* */} + Download Original + */}