diff options
Diffstat (limited to 'src')
28 files changed, 1190 insertions, 241 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index efd6ce3c9..69c66e0dc 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1086,7 +1086,7 @@ export namespace Docs { } export function AnnoPaletteDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.ANNOPALETTE), new List([Doc.MyAnnos]), { ...(options || {}) }); + return InstanceFromProto(Prototypes.get(DocumentType.ANNOPALETTE), new List([Doc.MyStickers]), { ...(options || {}) }); } export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) { diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 98a3e8914..009cd7de7 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -161,9 +161,9 @@ export class CurrentUserUtils { return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, templates, reqdScripts); } - static setupAnnoPalette(doc: Doc, field="myAnnos") { + static setupAnnoPalette(doc: Doc, field="myStickers") { const reqdOpts:DocumentOptions = { - title: "Saved Annotations", _xMargin: 0, _layout_showTitle: "title", hidden: false, _chromeHidden: true, + title: "Stickers", _xMargin: 0, _layout_showTitle: "title", hidden: false, _chromeHidden: true, _dragOnlyWithinContainer: true, layout_hideContextMenu: true, isSystem: true, _forceActive: true, _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, }; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 5a48b6c62..45dfcb5b8 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -232,9 +232,9 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora if (iconViewDoc.activeFrame) { iconViewDoc.opacity = 0; // if in an animation collection, set opacity to 0 to allow inkMasks and other documents to remain in the collection and to smoothly animate when they are activated in a different animation frame } else { - // if Doc is in the annotation palette, remove the flag indicating that it's saved + // if Doc is in the sticker palette, remove the flag indicating that it's saved const dragFactory = DocCast(iconView.Document.dragFactory); - if (dragFactory && DocCast(dragFactory.cloneOf).savedAsAnno) DocCast(dragFactory.cloneOf).savedAsAnno = undefined; + if (dragFactory && DocCast(dragFactory.cloneOf).savedAsSticker) DocCast(dragFactory.cloneOf).savedAsSticker = undefined; // if this is a face Annotation doc, then just hide it. if (iconView.Document.annotationOn && iconView.Document.face) iconView.Document.hidden = true; diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index 3a8a38073..65de85008 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -21,7 +21,7 @@ import { OverlayView } from './OverlayView'; import { DefaultStyleProvider, returnEmptyDocViewList, wavyBorderPath } from './StyleProvider'; import { DocumentView } from './nodes/DocumentView'; import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere'; -import { AnnotationPalette } from './smartdraw/AnnotationPalette'; +import { StickerPalette } from './smartdraw/StickerPalette'; interface LightboxViewProps { PanelWidth: number; @@ -56,7 +56,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> { }[] = []; private _savedState: LightboxSavedState = {}; private _history: { doc: Doc; target?: Doc }[] = []; - private _annoPaletteView: AnnotationPalette | null = null; + private _annoPaletteView: StickerPalette | null = null; @observable private _future: Doc[] = []; @observable private _layoutTemplate: Opt<Doc> = undefined; @observable private _layoutTemplateString: Opt<string> = undefined; @@ -317,7 +317,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> { </GestureOverlay> </div> - {this._showPalette && <AnnotationPalette ref={r => (this._annoPaletteView = r)} Document={DocCast(Doc.UserDoc().myLightboxDrawings)} />} + {this._showPalette && <StickerPalette ref={r => (this._annoPaletteView = r)} Document={DocCast(Doc.UserDoc().myLightboxDrawings)} />} {this.renderNavBtn(0, undefined, this._props.PanelHeight / 2 - 12.5, 'chevron-left', this._doc && this._history.length ? true : false, this.previous)} {this.renderNavBtn( this._props.PanelWidth - Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]), @@ -331,7 +331,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> { <LightboxTourBtn lightboxDoc={this.lightboxDoc} navBtn={this.renderNavBtn} future={this.future} stepInto={this.stepInto} /> {toggleBtn('lightboxView-navBtn', 'toggle reading view', BoolCast(this._doc?._layout_fitWidth), 'book-open', 'book', this.toggleFitWidth)} {toggleBtn('lightboxView-tabBtn', 'open document in a tab', false, 'file-export', '', this.downloadDoc)} - {toggleBtn('lightboxView-paletteBtn', 'toggle annotation palette', this._showPalette === true, 'palette', '', this.togglePalette)} + {toggleBtn('lightboxView-paletteBtn', 'toggle sticker palette', this._showPalette === true, 'palette', '', this.togglePalette)} {toggleBtn('lightboxView-penBtn', 'toggle pen annotation', Doc.ActiveTool === InkTool.Ink, 'pen', '', this.togglePen)} {toggleBtn('lightboxView-exploreBtn', 'toggle navigate only mode', SnappingManager.ExploreMode, 'globe-americas', '', this.toggleExplore)} </div> diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 2a59f6dc3..dda543470 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -64,7 +64,7 @@ import { ImportElementBox } from './nodes/importBox/ImportElementBox'; import { PresBox, PresElementBox } from './nodes/trails'; import { FaceRecognitionHandler } from './search/FaceRecognitionHandler'; import { SearchBox } from './search/SearchBox'; -import { AnnotationPalette } from './smartdraw/AnnotationPalette'; +import { StickerPalette } from './smartdraw/StickerPalette'; dotenv.config(); @@ -117,7 +117,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' }; KeyValueBox.Init(); PresBox.Init(TabDocView.AllTabDocs); DocumentContentsView.Init(KeyValueBox.LayoutString(), { - AnnotationPalette, + StickerPalette: StickerPalette, FormattedTextBox, ImageBox, FontIconBox, diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 7779d339f..7bc211036 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -73,7 +73,7 @@ import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere'; import { TaskCompletionBox } from './nodes/TaskCompletedBox'; import { DashFieldViewMenu } from './nodes/formattedText/DashFieldView'; import { RichTextMenu } from './nodes/formattedText/RichTextMenu'; -import GenerativeFill from './nodes/generativeFill/GenerativeFill'; +import ImageEditorBox from './nodes/imageEditor/ImageEditor'; import { PresBox } from './nodes/trails'; import { AnchorMenu } from './pdf/AnchorMenu'; import { GPTPopup } from './pdf/GPTPopup/GPTPopup'; @@ -446,6 +446,7 @@ export class MainView extends ObservableReactComponent<object> { fa.faAlignRight, fa.faHeading, fa.faRulerCombined, + fa.faFill, fa.faFillDrip, fa.faLink, fa.faUnlink, @@ -1146,7 +1147,7 @@ export class MainView extends ObservableReactComponent<object> { <LightboxView key="lightbox" PanelWidth={this._windowWidth} addSplit={CollectionDockingView.AddSplit} PanelHeight={this._windowHeight} maxBorder={this.lightboxMaxBorder} /> <GPTPopup key="gptpopup" /> <SchemaCSVPopUp key="schemacsvpopup" /> - <GenerativeFill imageEditorOpen={ImageEditor.Open} imageEditorSource={ImageEditor.Source} imageRootDoc={ImageEditor.RootDoc} addDoc={ImageEditor.AddDoc} /> + <ImageEditorBox imageEditorOpen={ImageEditor.Open} imageEditorSource={ImageEditor.Source} imageRootDoc={ImageEditor.RootDoc} addDoc={ImageEditor.AddDoc} /> </div> ); } diff --git a/src/client/views/StyleProviderQuiz.tsx b/src/client/views/StyleProviderQuiz.tsx index f07a2c803..b3fb8c930 100644 --- a/src/client/views/StyleProviderQuiz.tsx +++ b/src/client/views/StyleProviderQuiz.tsx @@ -17,7 +17,7 @@ import { AnchorMenu } from './pdf/AnchorMenu'; import { DocumentViewProps } from './nodes/DocumentView'; import { FieldViewProps } from './nodes/FieldView'; import { ImageBox } from './nodes/ImageBox'; -import { ImageUtility } from './nodes/generativeFill/generativeFillUtils/ImageHandler'; +import { ImageUtility } from './nodes/imageEditor/imageEditorUtils/ImageHandler'; import './StyleProviderQuiz.scss'; export namespace styleProviderQuiz { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index f2a5780c5..65b2702c6 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -55,7 +55,7 @@ import { FocusViewOptions } from '../../nodes/FocusViewOptions'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { OpenWhere } from '../../nodes/OpenWhere'; import { PinDocView, PinProps } from '../../PinFuncs'; -import { AnnotationPalette } from '../../smartdraw/AnnotationPalette'; +import { StickerPalette } from '../../smartdraw/StickerPalette'; import { DrawingOptions, SmartDrawHandler } from '../../smartdraw/SmartDrawHandler'; import { StyleProp } from '../../StyleProp'; import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; @@ -1998,9 +1998,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection icon: 'pen-to-square', }); optionItems.push({ - description: this.Document.savedAsAnno ? 'Saved as Annotation!' : 'Save to Annotation Palette', - event: action(undoable(async () => await AnnotationPalette.addToPalette(this.Document), 'save to palette')), - icon: this.Document.savedAsAnno ? 'clipboard-check' : 'file-arrow-down', + description: this.Document.savedAsSticker ? 'Sticker Saved!' : 'Save to Stickers', + event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')), + icon: this.Document.savedAsSticker ? 'clipboard-check' : 'file-arrow-down', }); this._props.renderDepth && optionItems.push({ diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index ff879a2ab..4d35068ee 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -32,6 +32,7 @@ import { MarqueeAnnotator } from '../MarqueeAnnotator'; import { OverlayView } from '../OverlayView'; import { AnchorMenu } from '../pdf/AnchorMenu'; import { PinDocView, PinProps } from '../PinFuncs'; +import { StickerPalette } from '../smartdraw/StickerPalette'; import { StyleProp } from '../StyleProp'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; @@ -320,6 +321,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }), icon: 'pencil-alt', }); + funcs.push({ + description: this.Document.savedAsSticker ? 'Sticker Saved!' : 'Save to Stickers', + event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')), + icon: this.Document.savedAsSticker ? 'clipboard-check' : 'file-arrow-down', + }); ContextMenu.Instance?.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); } }; diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts deleted file mode 100644 index 1e7801056..000000000 --- a/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface CursorData { - x: number; - y: number; - width: number; -} - -export interface Point { - x: number; - y: number; -} - -export enum BrushMode { - ADD, - SUBTRACT, -} - -export interface ImageDimensions { - width: number; - height: number; -} diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss b/src/client/views/nodes/imageEditor/GenerativeFillButtons.scss index 0180ef904..0180ef904 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss +++ b/src/client/views/nodes/imageEditor/GenerativeFillButtons.scss diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx b/src/client/views/nodes/imageEditor/GenerativeFillButtons.tsx index fe22b273d..32ed6b307 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx +++ b/src/client/views/nodes/imageEditor/GenerativeFillButtons.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import ReactLoading from 'react-loading'; import { Button, IconButton, Type } from 'browndash-components'; import { AiOutlineInfo } from 'react-icons/ai'; -import { activeColor } from './generativeFillUtils/generativeFillConstants'; +import { activeColor } from './imageEditorUtils/imageEditorConstants'; interface ButtonContainerProps { onClick: () => Promise<void>; diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.scss b/src/client/views/nodes/imageEditor/ImageEditor.scss index c2669a950..c691e6a18 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFill.scss +++ b/src/client/views/nodes/imageEditor/ImageEditor.scss @@ -2,7 +2,7 @@ $navHeight: 5rem; $canvasSize: 1024px; $scale: 0.5; -.generativeFillContainer { +.imageEditorContainer { position: absolute; top: 0; left: 0; @@ -13,7 +13,7 @@ $scale: 0.5; flex-direction: column; overflow: hidden; - .generativeFillControls { + .imageEditorTopBar { flex-shrink: 0; height: $navHeight; color: #000000; @@ -27,6 +27,12 @@ $scale: 0.5; border-bottom: 1px solid #c7cdd0; padding: 0 2rem; + .imageEditorControls { + display: flex; + align-items: center; + gap: 1.5rem; + } + h1 { font-size: 1.5rem; } @@ -69,13 +75,48 @@ $scale: 0.5; } } - .iconContainer { + .sideControlsContainer { + width: 160px; position: absolute; - top: 2rem; - left: 2rem; - display: flex; - flex-direction: column; - gap: 2rem; + left: 0; + height: 100%; + + .sideControls { + position: absolute; + width: 120px; + top: 3rem; + left: 2rem; + display: flex; + flex-direction: column; + gap: 1rem; + + .imageToolsContainer { + display: flex; + flex-direction: column; + gap: 10px; + } + + .cutToolsContainer { + display: grid; + gap: 5px; + grid-template-columns: 1fr 1fr; + } + + .undoRedoContainer { + justify-content: center; + display: flex; + flex-direction: row; + } + + .sliderContainer { + margin: 3rem 0; + height: 225px; + width: 100%; + display: flex; + justify-content: center; + cursor: pointer; + } + } } .editsBox { @@ -86,7 +127,18 @@ $scale: 0.5; flex-direction: column; gap: 1rem; + .originalImageLabel { + position: absolute; + bottom: 10; + left: 10; + color: #ffffff; + font-size: 0.8rem; + letter-spacing: 1px; + text-transform: uppercase; + } + img { + cursor: pointer; transition: all 0.2s ease-in-out; &:hover { opacity: 0.8; diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx index 261eb4bb4..9642dcc31 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFill.tsx +++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx @@ -4,7 +4,7 @@ /* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable react/function-component-definition */ import { Checkbox, FormControlLabel, Slider, TextField } from '@mui/material'; -import { IconButton } from 'browndash-components'; +import { Button, IconButton, Type } from 'browndash-components'; import * as React from 'react'; import { useEffect, useRef, useState } from 'react'; import { CgClose } from 'react-icons/cg'; @@ -20,17 +20,19 @@ import { CollectionDockingView } from '../../collections/CollectionDockingView'; import { CollectionFreeFormView } from '../../collections/collectionFreeForm'; import { ImageEditorData } from '../ImageBox'; import { OpenWhereMod } from '../OpenWhere'; -import './GenerativeFill.scss'; -import { EditButtons, CutButtons } from './GenerativeFillButtons'; -import { BrushHandler, BrushType } from './generativeFillUtils/BrushHandler'; -import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler'; -import { PointerHandler } from './generativeFillUtils/PointerHandler'; -import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './generativeFillUtils/generativeFillConstants'; -import { CursorData, ImageDimensions, Point } from './generativeFillUtils/generativeFillInterfaces'; +import './ImageEditor.scss'; +import { ApplyFuncButtons, ImageToolButton } from './ImageEditorButtons'; +import { BrushHandler, BrushType } from './imageEditorUtils/BrushHandler'; +import { APISuccess, ImageUtility } from './imageEditorUtils/ImageHandler'; +import { PointerHandler } from './imageEditorUtils/PointerHandler'; +import { activeColor, bgColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './imageEditorUtils/imageEditorConstants'; +import { BrushMode, CursorData, ImageDimensions, ImageEditTool, ImageToolType, Point } from './imageEditorUtils/imageEditorInterfaces'; import { DocumentView } from '../DocumentView'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { ImageField } from '../../../../fields/URLField'; import { resolve } from 'url'; +import { DocData } from '../../../../fields/DocSymbols'; +import { SettingsManager } from '../../../util/SettingsManager'; interface GenerativeFillProps { imageEditorOpen: boolean; @@ -41,7 +43,7 @@ interface GenerativeFillProps { // Added field on image doc: gen_fill_children: List of children Docs -const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc }: GenerativeFillProps) => { +const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc }: GenerativeFillProps) => { const canvasRef = useRef<HTMLCanvasElement>(null); const canvasBackgroundRef = useRef<HTMLCanvasElement>(null); const drawingAreaRef = useRef<HTMLDivElement>(null); @@ -55,13 +57,14 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD // format: array of [image source, corresponding image Doc] const [edits, setEdits] = useState<{ url: string; saveRes: Doc | undefined }[]>([]); const [edited, setEdited] = useState(false); - // const [brushStyle] = useState<BrushStyle>(BrushStyle.ADD); + const [isFirstDoc, setIsFirstDoc] = useState<boolean>(true); const [input, setInput] = useState(''); const [loading, setLoading] = useState(false); const [canvasDims, setCanvasDims] = useState<ImageDimensions>({ width: canvasSize, height: canvasSize, }); + const [cutType, setCutType] = useState<BrushMode>(BrushMode.IN); // whether to create a new collection or not const [isNewCollection, setIsNewCollection] = useState(true); // the current image in the main canvas @@ -82,6 +85,24 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD // constants for image cutting const cutPts = useRef<Point[]>([]); + /** + * + * @param type The new tool type we are changing to + */ + const changeTool = (type: ImageToolType) => { + switch (type) { + case ImageToolType.GenerativeFill: + setCurrTool(genFillTool); + setCursorData(prev => ({ ...prev, width: genFillTool.sliderDefault as number })); + break; + case ImageToolType.Cut: + setCurrTool(cutTool); + setCursorData(prev => ({ ...prev, width: cutTool.sliderDefault as number })); + break; + default: + break; + } + }; // Undo and Redo const handleUndo = () => { const ctx = ImageUtility.getCanvasContext(canvasRef); @@ -334,39 +355,51 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD setLoading(false); }; - const cutImage = async () => { + const cutImage = async (currCutType: BrushMode, brushWidth: number, prevEdits: { url: string; saveRes: Doc | undefined }[], firstDoc: boolean) => { const img = currImg.current; const canvas = canvasRef.current; if (!canvas || !img) return; - canvas.width = img.naturalWidth; - canvas.height = img.naturalHeight; const ctx = ImageUtility.getCanvasContext(canvasRef); if (!ctx) return; - ctx.globalCompositeOperation = 'source-over'; setLoading(true); - setEdited(true); // get the original image const canvasOriginalImg = ImageUtility.getCanvasImg(img); if (!canvasOriginalImg) return; - // draw the image onto the canvas - ctx.drawImage(img, 0, 0); - // get the mask which i assume is the thing the user draws on - // const canvasMask = ImageUtility.getCanvasMask(canvas, canvasOriginalImg); - // if (!canvasMask) return; - // canvasMask.width = canvas.width; - // canvasMask.height = canvas.height; - // now put the user's path around the mask + // NOTE: cutting two diff shapes can be made possible by having the user press a button to set a new shape! + if (currCutType !== BrushMode.LINE_OUT) handleReset(); // gets rid of the visible brush strokes (mostly needed for line_in) unless it's erasing (which depends on the brush strokes) + let minX = img.width; + let maxX = 0; + let minY = img.height; + let maxY = 0; if (cutPts.current.length) { ctx.beginPath(); - ctx.moveTo(cutPts.current[0].x, cutPts.current[0].y); // later check edge case where cutPts is empty + ctx.moveTo(cutPts.current[0].x, cutPts.current[0].y); for (let i = 0; i < cutPts.current.length; i++) { ctx.lineTo(cutPts.current[i].x, cutPts.current[i].y); + minX = Math.min(cutPts.current[i].x, minX); + minY = Math.min(cutPts.current[i].y, minY); + maxX = Math.max(cutPts.current[i].x, maxX); + maxY = Math.max(cutPts.current[i].y, maxY); + } + switch (currCutType) { + case BrushMode.IN: + ctx.closePath(); + ctx.globalCompositeOperation = 'destination-in'; + ctx.fill(); + break; + case BrushMode.OUT: + ctx.closePath(); + ctx.globalCompositeOperation = 'destination-out'; + ctx.fill(); + break; + case BrushMode.LINE_IN: + ctx.globalCompositeOperation = 'destination-in'; + ctx.lineWidth = brushWidth + 10; // added offset because width gets cut off a little bit + ctx.stroke(); + break; } - ctx.closePath(); - ctx.stroke(); - ctx.fill(); - // ctx.clip(); } + const url = canvas.toDataURL(); // this does the same thing as convert img to canvasurl if (!newCollectionRef.current) { if (!isNewCollection && imageRootDoc) { @@ -387,13 +420,43 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD CollectionDockingView.AddSplit(newCollectionRef.current, OpenWhereMod.right); } } + const image = new Image(); image.src = url; - await createNewImgDoc(image, true); - // add the doc to the main freeform - // eslint-disable-next-line no-use-before-define - setLoading(false); - cutPts.current.length = 0; + image.onload = async () => { + let finalImg: HTMLImageElement | undefined = image; + let finalImgURL: string = url; + if (currCutType == BrushMode.IN) { + const croppedData = cropImage(image, minX, maxX, minY, maxY); + finalImg = croppedData; + finalImgURL = croppedData.src; + } + currImg.current = finalImg; + const newImgDoc = await createNewImgDoc(finalImg, firstDoc); + if (newImgDoc) { + const docData = newImgDoc[DocData]; + // docData.backgroundColor = 'transparent'; + if (firstDoc) setIsFirstDoc(false); + setEdits([...prevEdits, { url: finalImgURL, saveRes: undefined }]); + } + setLoading(false); + cutPts.current.length = 0; + }; + }; + + const cropImage = (image: HTMLImageElement, minX: number, maxX: number, minY: number, maxY: number) => { + const croppedCanvas = document.createElement('canvas'); + const croppedCtx = croppedCanvas.getContext('2d'); + if (!croppedCtx) return image; + croppedCanvas.width = maxX - minX; + croppedCanvas.height = maxY - minY; + croppedCtx.globalCompositeOperation = 'source-over'; + croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height); + croppedCtx.drawImage(image, -minX, -minY); + const croppedURL = croppedCanvas.toDataURL(); + const croppedImage = new Image(); + croppedImage.src = croppedURL; + return croppedImage; }; // adjusts all the img positions to be aligned @@ -416,7 +479,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD }; // creates a new image document and returns its reference - const createNewImgDoc = async (img: HTMLImageElement, firstDoc: boolean): Promise<Doc | undefined> => { + const createNewImgDoc = async (img: HTMLImageElement, firstDoc: boolean, parent?: Doc): Promise<Doc | undefined> => { if (!imageRootDoc) return undefined; const { src } = img; const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [src] }); @@ -495,14 +558,22 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD DocumentView.addViewRenderedCb(newCollectionRef.current, dv => (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce()); } setEdits([]); + setIsFirstDoc(true); }; - return ( - <div className="generativeFillContainer" style={{ display: imageEditorOpen ? 'flex' : 'none' }}> - <div className="generativeFillControls"> + // defines the tools and sets current tool + const genFillTool: ImageEditTool = { type: ImageToolType.GenerativeFill, name: 'Generative Fill', btnText: 'GET EDITS', icon: 'fill', applyFunc: getEdit, sliderMin: 25, sliderMax: 500, sliderDefault: 150 }; + const cutTool: ImageEditTool = { type: ImageToolType.Cut, name: 'Cut', btnText: 'CUT IMAGE', icon: 'scissors', applyFunc: cutImage, sliderMin: 1, sliderMax: 50, sliderDefault: 5 }; + const imageEditTools: ImageEditTool[] = [genFillTool, cutTool]; + const [currTool, setCurrTool] = useState<ImageEditTool>(genFillTool); + + // the top controls for making a new collection, resetting, and applying edits, + function renderControls() { + return ( + <div className="imageEditorTopBar"> <h1>Image Editor</h1> {/* <IconButton text="Cut out" icon={<FontAwesomeIcon icon="scissors" />} /> */} - <div style={{ display: 'flex', alignItems: 'center', gap: '1.5rem' }}> + <div className="imageEditorControls"> <FormControlLabel control={ <Checkbox @@ -518,153 +589,201 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD labelPlacement="end" sx={{ whiteSpace: 'nowrap' }} /> - <EditButtons onClick={getEdit} loading={loading} onReset={handleReset} /> - <CutButtons onClick={cutImage} loading={loading} onReset={handleReset} /> + <ApplyFuncButtons onClick={() => currTool.applyFunc(cutType, cursorData.width, edits, isFirstDoc)} loading={loading} onReset={handleReset} btnText={currTool.btnText} /> <IconButton color={activeColor} tooltip="close" icon={<CgClose size="16px" />} onClick={handleViewClose} /> </div> </div> - {/* Main canvas for editing */} - <div - className="drawingArea" // this only works if pointerevents: none is set on the custom pointer - ref={drawingAreaRef} - onPointerOver={updateCursorData} - onPointerMove={updateCursorData} - onPointerDown={handlePointerDown} - onPointerUp={handlePointerUp}> - <canvas ref={canvasRef} width={canvasDims.width} height={canvasDims.height} style={{ transform: `scale(${canvasScale})` }} /> - <canvas ref={canvasBackgroundRef} width={canvasDims.width} height={canvasDims.height} style={{ transform: `scale(${canvasScale})` }} /> - <div - className="pointer" - style={{ - left: cursorData.x, - top: cursorData.y, - width: cursorData.width, - height: cursorData.width, - }}> - <div className="innerPointer" /> - </div> - {/* Icons */} - <div className="iconContainer"> + ); + } + + // the side icons including tool type, the slider, and undo/redo + function renderSideIcons() { + return ( + <div className="sideControlsContainer" style={{ backgroundColor: bgColor }}> + <div className="sideControls"> + <div className="imageToolsContainer"> + {imageEditTools.map(tool => { + return ImageToolButton(tool, tool.type === currTool.type, changeTool); + })} + </div> + {currTool.type == ImageToolType.Cut && ( + <div className="cutToolsContainer"> + <Button + style={{ width: '100%' }} + text="Keep in" + type={Type.TERT} + color={cutType == BrushMode.IN ? SettingsManager.userColor : bgColor} + onClick={() => { + setCutType(BrushMode.IN); + }} + /> + <Button + style={{ width: '100%' }} + text="Keep out" + type={Type.TERT} + color={cutType == BrushMode.OUT ? SettingsManager.userColor : bgColor} + onClick={() => { + setCutType(BrushMode.OUT); + }} + /> + <Button + style={{ width: '100%' }} + text="Draw in" + type={Type.TERT} + color={cutType == BrushMode.LINE_IN ? SettingsManager.userColor : bgColor} + onClick={() => { + setCutType(BrushMode.LINE_IN); + }} + /> + <Button + style={{ width: '100%' }} + text="Erase" + type={Type.TERT} + color={cutType == BrushMode.LINE_OUT ? SettingsManager.userColor : bgColor} + onClick={() => { + setCutType(BrushMode.LINE_OUT); + }} + /> + </div> + )} + <div className="sliderContainer" onPointerDown={e => e.stopPropagation()}> + {currTool.type === ImageToolType.GenerativeFill && ( + <Slider + sx={{ + '& input[type="range"]': { + WebkitAppearance: 'slider-vertical', + }, + }} + orientation="vertical" + min={genFillTool.sliderMin} + max={genFillTool.sliderMax} + defaultValue={genFillTool.sliderDefault} + size="small" + valueLabelDisplay="auto" + onChange={(e, val) => { + setCursorData(prev => ({ ...prev, width: val as number })); + }} + /> + )} + {currTool.type === ImageToolType.Cut && ( + <Slider + sx={{ + '& input[type="range"]': { + WebkitAppearance: 'slider-vertical', + }, + }} + orientation="vertical" + min={cutTool.sliderMin} + max={cutTool.sliderMax} + defaultValue={cutTool.sliderDefault} + size="small" + valueLabelDisplay="auto" + onChange={(e, val) => { + setCursorData(prev => ({ ...prev, width: val as number })); + }} + /> + )} + </div> {/* Undo and Redo */} - <IconButton - style={{ cursor: 'pointer' }} - onPointerDown={e => { - e.stopPropagation(); - handleUndo(); - }} - onPointerUp={e => { - e.stopPropagation(); - }} - color={activeColor} - tooltip="Undo" - icon={<IoMdUndo />} - /> - <IconButton - style={{ cursor: 'pointer' }} - onPointerDown={e => { - e.stopPropagation(); - handleRedo(); - }} - onPointerUp={e => { - e.stopPropagation(); - }} - color={activeColor} - tooltip="Redo" - icon={<IoMdRedo />} - /> - <div onPointerDown={e => e.stopPropagation()} style={{ height: 225, width: '100%', display: 'flex', justifyContent: 'center', cursor: 'pointer' }}> - <Slider - sx={{ - '& input[type="range"]': { - WebkitAppearance: 'slider-vertical', - }, + <div className="undoRedoContainer"> + <IconButton + style={{ cursor: 'pointer' }} + onPointerDown={e => { + e.stopPropagation(); + handleUndo(); }} - orientation="vertical" - min={25} - max={500} - defaultValue={150} - size="small" - valueLabelDisplay="auto" - onChange={(e: any, val: any) => { - setCursorData(prev => ({ ...prev, width: val as number })); + onPointerUp={e => { + e.stopPropagation(); }} + color={activeColor} + tooltip="Undo" + icon={<IoMdUndo />} /> - </div> - <div onPointerDown={e => e.stopPropagation()} style={{ height: 225, width: '100%', display: 'flex', justifyContent: 'center', cursor: 'pointer' }}> - <Slider - sx={{ - '& input[type="range"]': { - WebkitAppearance: 'slider-vertical', - }, + <IconButton + style={{ cursor: 'pointer' }} + onPointerDown={e => { + e.stopPropagation(); + handleRedo(); }} - orientation="vertical" - min={1} - max={500} - defaultValue={150} - size="small" - valueLabelDisplay="auto" - onChange={(e: any, val: any) => { - setCursorData(prev => ({ ...prev, width: val as number })); + onPointerUp={e => { + e.stopPropagation(); }} + color={activeColor} + tooltip="Redo" + icon={<IoMdRedo />} /> </div> </div> - {/* Edits thumbnails */} - <div className="editsBox"> - {edits.map((edit, i) => ( + </div> + ); + } + + // circular pointer for drawing/erasing + function renderPointer() { + return ( + <div + className="pointer" + style={{ + left: cursorData.x, + top: cursorData.y, + width: cursorData.width, + height: cursorData.width, + }}> + <div className="innerPointer" /> + </div> + ); + } + + // the previews for each edit + function renderEditThumbnails() { + return ( + <div className="editsBox"> + {edits.map((edit, i) => ( + <img + // eslint-disable-next-line react/no-array-index-key + key={i} + alt="image edits" + width={75} + src={edit.url} + onClick={async () => { + const img = new Image(); + img.src = edit.url; + ImageUtility.drawImgToCanvas(img, canvasRef, img.width, img.height); + currImg.current = img; + parentDoc.current = edit.saveRes ?? null; + }} + /> + ))} + {/* Original img thumbnail */} + {edits.length > 0 && ( + <div style={{ position: 'relative' }}> + <label className="originalImageLabel">Original</label> <img - // eslint-disable-next-line react/no-array-index-key - key={i} - alt="image edits" + alt="image stuff" width={75} - src={edit.url} - style={{ cursor: 'pointer' }} - onClick={async () => { + src={originalImg.current?.src} + onClick={() => { + if (!originalImg.current) return; const img = new Image(); - img.src = edit.url; + img.src = originalImg.current.src; ImageUtility.drawImgToCanvas(img, canvasRef, canvasDims.width, canvasDims.height); currImg.current = img; - parentDoc.current = edit.saveRes ?? null; + if (!parentDoc.current) parentDoc.current = originalDoc.current; }} /> - ))} - {/* Original img thumbnail */} - {edits.length > 0 && ( - <div style={{ position: 'relative' }}> - <label - style={{ - position: 'absolute', - bottom: 10, - left: 10, - color: '#ffffff', - fontSize: '0.8rem', - letterSpacing: '1px', - textTransform: 'uppercase', - }}> - Original - </label> - <img - alt="image stuff" - width={75} - src={originalImg.current?.src} - style={{ cursor: 'pointer' }} - onClick={() => { - if (!originalImg.current) return; - const img = new Image(); - img.src = originalImg.current.src; - ImageUtility.drawImgToCanvas(img, canvasRef, canvasDims.width, canvasDims.height); - currImg.current = img; - parentDoc.current = originalDoc.current; - }} - /> - </div> - )} - </div> + </div> + )} </div> + ); + } + + // the prompt box for generative fill + function renderPromptBox() { + return ( <div> <TextField value={input} - onChange={(e: any) => setInput(e.target.value)} + onChange={e => setInput(e.target.value)} disabled={isBrushing} type="text" label="Prompt" @@ -680,8 +799,29 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD }} /> </div> + ); + } + + return ( + <div className="imageEditorContainer" style={{ display: imageEditorOpen ? 'flex' : 'none' }}> + {renderControls()} + {/* Main canvas for editing */} + <div + className="drawingArea" // this only works if pointerevents: none is set on the custom pointer + ref={drawingAreaRef} + onPointerOver={updateCursorData} + onPointerMove={updateCursorData} + onPointerDown={handlePointerDown} + onPointerUp={handlePointerUp}> + <canvas ref={canvasRef} width={canvasDims.width} height={canvasDims.height} style={{ transform: `scale(${canvasScale})` }} /> + <canvas ref={canvasBackgroundRef} width={canvasDims.width} height={canvasDims.height} style={{ transform: `scale(${canvasScale})` }} /> + {renderPointer()} + {renderSideIcons()} + {renderEditThumbnails()} + </div> + {currTool.type === ImageToolType.GenerativeFill && renderPromptBox()} </div> ); }; -export default GenerativeFill; +export default ImageEditor; diff --git a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx new file mode 100644 index 000000000..cb963616b --- /dev/null +++ b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx @@ -0,0 +1,69 @@ +import './GenerativeFillButtons.scss'; +import * as React from 'react'; +import ReactLoading from 'react-loading'; +import { Button, IconButton, Type } from 'browndash-components'; +import { AiOutlineInfo } from 'react-icons/ai'; +import { bgColor } from './imageEditorUtils/imageEditorConstants'; +import { ImageEditTool, ImageToolType } from './imageEditorUtils/imageEditorInterfaces'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { SettingsManager } from '../../../util/SettingsManager'; + +interface ButtonContainerProps { + onClick: () => Promise<void>; + loading: boolean; + onReset: () => void; + btnText: string; +} + +export function ApplyFuncButtons({ loading, onClick: getEdit, onReset, btnText }: ButtonContainerProps) { + return ( + <div className="generativeFillBtnContainer"> + <Button text="RESET" type={Type.PRIM} color={SettingsManager.userVariantColor} onClick={onReset} /> + {loading ? ( + <Button + text={btnText} + type={Type.TERT} + color={SettingsManager.userVariantColor} + icon={<ReactLoading type="spin" color="#ffffff" width={20} height={20} />} + iconPlacement="right" + onClick={() => { + if (!loading) getEdit(); + }} + /> + ) : ( + <Button + text={btnText} + type={Type.TERT} + color={SettingsManager.userVariantColor} + onClick={() => { + if (!loading) getEdit(); + }} + /> + )} + <IconButton + type={Type.SEC} + color={SettingsManager.userVariantColor} + tooltip="Open Documentation" + icon={<AiOutlineInfo size="16px" />} + onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/generativeai/#editing', '_blank')} + /> + </div> + ); +} + +export function ImageToolButton(tool: ImageEditTool, isActive: boolean, selectTool: (type: ImageToolType) => void) { + return ( + <div className="imageEditorButtonContainer"> + <Button + style={{ width: '100%' }} + text={tool.name} + type={Type.TERT} + color={isActive ? SettingsManager.userVariantColor : bgColor} + icon={<FontAwesomeIcon icon={tool.icon} />} + onClick={() => { + selectTool(tool.type); + }} + /> + </div> + ); +} diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts b/src/client/views/nodes/imageEditor/imageEditingUtils/ImageHandler.ts index 24dba1778..514e8a94f 100644 --- a/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts +++ b/src/client/views/nodes/imageEditor/imageEditingUtils/ImageHandler.ts @@ -1,5 +1,5 @@ import { RefObject } from 'react'; -import { bgColor, canvasSize } from './generativeFillConstants'; +import { bgColor, canvasSize } from '../ImageEditorUtils/imageEditorConstants'; export interface APISuccess { status: 'success'; diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/BrushHandler.ts index 8a66d7347..a9fe02d4f 100644 --- a/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts +++ b/src/client/views/nodes/imageEditor/imageEditorUtils/BrushHandler.ts @@ -1,6 +1,6 @@ import { GenerativeFillMathHelpers } from './GenerativeFillMathHelpers'; -import { eraserColor } from './generativeFillConstants'; -import { Point } from './generativeFillInterfaces'; +import { eraserColor } from './imageEditorConstants'; +import { Point } from './imageEditorInterfaces'; import { points } from '@turf/turf'; export enum BrushType { diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/GenerativeFillMathHelpers.ts index 6da8c3da0..f820300f3 100644 --- a/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts +++ b/src/client/views/nodes/imageEditor/imageEditorUtils/GenerativeFillMathHelpers.ts @@ -1,4 +1,4 @@ -import { Point } from './generativeFillInterfaces'; +import { Point } from './imageEditorInterfaces'; export class GenerativeFillMathHelpers { static distanceBetween = (p1: Point, p2: Point) => Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2); diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts new file mode 100644 index 000000000..ece0f4d7f --- /dev/null +++ b/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts @@ -0,0 +1,312 @@ +import { RefObject } from 'react'; +import { bgColor, canvasSize } from './imageEditorConstants'; + +export interface APISuccess { + status: 'success'; + urls: string[]; +} + +export interface APIError { + status: 'error'; + message: string; +} + +export class ImageUtility { + /** + * + * @param canvas Canvas to convert + * @returns Blob of canvas + */ + static canvasToBlob = (canvas: HTMLCanvasElement): Promise<Blob> => + new Promise(resolve => { + canvas.toBlob(blob => { + if (blob) { + resolve(blob); + } + }, 'image/png'); + }); + + // given a square api image, get the cropped img + static getCroppedImg = (img: HTMLImageElement, width: number, height: number): HTMLCanvasElement | undefined => { + // Create a new canvas element + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + if (ctx) { + // Clear the canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + if (width < height) { + // horizontal padding, x offset + const xOffset = (canvasSize - width) / 2; + ctx.drawImage(img, xOffset, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); + } else { + // vertical padding, y offset + const yOffset = (canvasSize - height) / 2; + ctx.drawImage(img, 0, yOffset, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); + } + return canvas; + } + return undefined; + }; + + // converts an image to a canvas data url + static convertImgToCanvasUrl = async (imageSrc: string, width: number, height: number): Promise<string> => + new Promise<string>((resolve, reject) => { + const img = new Image(); + img.onload = () => { + const canvas = this.getCroppedImg(img, width, height); + if (canvas) { + const dataUrl = canvas.toDataURL(); + resolve(dataUrl); + } + }; + img.onerror = error => { + reject(error); + }; + img.src = imageSrc; + }); + + // calls the openai api to get image edits + static getEdit = async (imgBlob: Blob, maskBlob: Blob, prompt: string, n?: number): Promise<APISuccess | APIError> => { + const apiUrl = 'https://api.openai.com/v1/images/edits'; + const fd = new FormData(); + fd.append('image', imgBlob, 'image.png'); + fd.append('mask', maskBlob, 'mask.png'); + fd.append('prompt', prompt); + fd.append('size', '1024x1024'); + fd.append('n', n ? JSON.stringify(n) : '1'); + fd.append('response_format', 'b64_json'); + + try { + const res = await fetch(apiUrl, { + method: 'POST', + headers: { + Authorization: `Bearer ${process.env.OPENAI_KEY}`, + }, + body: fd, + }); + const data = await res.json(); + console.log(data.data); + return { + status: 'success', + urls: (data.data as { b64_json: string }[]).map(urlData => `data:image/png;base64,${urlData.b64_json}`), + }; + } catch (err) { + console.log(err); + return { status: 'error', message: 'API error.' }; + } + }; + + // mock api call + static mockGetEdit = async (mockSrc: string): Promise<APISuccess | APIError> => ({ + status: 'success', + urls: [mockSrc, mockSrc, mockSrc], + }); + + // Gets the canvas rendering context of a canvas + static getCanvasContext = (canvasRef: RefObject<HTMLCanvasElement>): CanvasRenderingContext2D | null => { + if (!canvasRef.current) return null; + const ctx = canvasRef.current.getContext('2d'); + if (!ctx) return null; + return ctx; + }; + + // Helper for downloading the canvas (for debugging) + static downloadCanvas = (canvas: HTMLCanvasElement) => { + const url = canvas.toDataURL(); + const downloadLink = document.createElement('a'); + downloadLink.href = url; + downloadLink.download = 'canvas'; + + downloadLink.click(); + downloadLink.remove(); + }; + + // Download the canvas (for debugging) + static downloadImageCanvas = (imgUrl: string) => { + const img = new Image(); + img.src = imgUrl; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = canvasSize; + canvas.height = canvasSize; + const ctx = canvas.getContext('2d'); + ctx?.drawImage(img, 0, 0, canvasSize, canvasSize); + + this.downloadCanvas(canvas); + }; + }; + + // Clears the canvas + static clearCanvas = (canvasRef: React.RefObject<HTMLCanvasElement>) => { + const ctx = this.getCanvasContext(canvasRef); + if (!ctx || !canvasRef.current) return; + ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); + }; + + // Draws the image to the current canvas + static drawImgToCanvas = (img: HTMLImageElement, canvasRef: React.RefObject<HTMLCanvasElement>, width: number, height: number) => { + const drawImg = (htmlImg: HTMLImageElement) => { + const ctx = this.getCanvasContext(canvasRef); + if (!ctx) return; + ctx.globalCompositeOperation = 'source-over'; + ctx.clearRect(0, 0, canvasRef.current?.width || width, canvasRef.current?.height || height); + ctx.drawImage(htmlImg, 0, 0, width, height); + }; + + if (img.complete) { + drawImg(img); + } else { + img.onload = () => { + drawImg(img); + }; + } + }; + + // Gets the image mask for the openai endpoint + static getCanvasMask = (srcCanvas: HTMLCanvasElement, paddedCanvas: HTMLCanvasElement): HTMLCanvasElement | undefined => { + const canvas = document.createElement('canvas'); + canvas.width = canvasSize; + canvas.height = canvasSize; + const ctx = canvas.getContext('2d'); + if (!ctx) return undefined; + ctx?.clearRect(0, 0, canvasSize, canvasSize); + ctx.drawImage(paddedCanvas, 0, 0); + + // extract and set padding data + if (srcCanvas.height > srcCanvas.width) { + // horizontal padding, x offset + const xOffset = (canvasSize - srcCanvas.width) / 2; + ctx?.clearRect(xOffset, 0, srcCanvas.width, srcCanvas.height); + ctx.drawImage(srcCanvas, xOffset, 0, srcCanvas.width, srcCanvas.height); + } else { + // vertical padding, y offset + const yOffset = (canvasSize - srcCanvas.height) / 2; + ctx?.clearRect(0, yOffset, srcCanvas.width, srcCanvas.height); + ctx.drawImage(srcCanvas, 0, yOffset, srcCanvas.width, srcCanvas.height); + } + return canvas; + }; + + // Fills in the blank areas of the image with an image reflection (to fill in a square-shaped canvas) + static drawHorizontalReflection = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, xOffset: number) => { + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const { data } = imageData; + for (let i = 0; i < canvas.height; i++) { + for (let j = 0; j < xOffset; j++) { + const targetIdx = 4 * (i * canvas.width + j); + const sourceI = i; + const sourceJ = xOffset + (xOffset - j); + const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); + data[targetIdx] = data[sourceIdx]; + data[targetIdx + 1] = data[sourceIdx + 1]; + data[targetIdx + 2] = data[sourceIdx + 2]; + } + } + for (let i = 0; i < canvas.height; i++) { + for (let j = canvas.width - 1; j >= canvas.width - 1 - xOffset; j--) { + const targetIdx = 4 * (i * canvas.width + j); + const sourceI = i; + const sourceJ = canvas.width - 1 - xOffset - (xOffset - (canvas.width - j)); + const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); + data[targetIdx] = data[sourceIdx]; + data[targetIdx + 1] = data[sourceIdx + 1]; + data[targetIdx + 2] = data[sourceIdx + 2]; + } + } + ctx.putImageData(imageData, 0, 0); + }; + + // Fills in the blank areas of the image with an image reflection (to fill in a square-shaped canvas) + static drawVerticalReflection = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, yOffset: number) => { + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const { data } = imageData; + for (let j = 0; j < canvas.width; j++) { + for (let i = 0; i < yOffset; i++) { + const targetIdx = 4 * (i * canvas.width + j); + const sourceJ = j; + const sourceI = yOffset + (yOffset - i); + const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); + data[targetIdx] = data[sourceIdx]; + data[targetIdx + 1] = data[sourceIdx + 1]; + data[targetIdx + 2] = data[sourceIdx + 2]; + } + } + for (let j = 0; j < canvas.width; j++) { + for (let i = canvas.height - 1; i >= canvas.height - 1 - yOffset; i--) { + const targetIdx = 4 * (i * canvas.width + j); + const sourceJ = j; + const sourceI = canvas.height - 1 - yOffset - (yOffset - (canvas.height - i)); + const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); + data[targetIdx] = data[sourceIdx]; + data[targetIdx + 1] = data[sourceIdx + 1]; + data[targetIdx + 2] = data[sourceIdx + 2]; + } + } + ctx.putImageData(imageData, 0, 0); + }; + + // Gets the unaltered (besides filling in padding) version of the image for the api call + static getCanvasImg = (img: HTMLImageElement): HTMLCanvasElement | undefined => { + const canvas = document.createElement('canvas'); + canvas.width = canvasSize; + canvas.height = canvasSize; + const ctx = canvas.getContext('2d'); + if (!ctx) return undefined; + // fix scaling + const scale = Math.min(canvasSize / img.width, canvasSize / img.height); + const width = Math.floor(img.width * scale); + const height = Math.floor(img.height * scale); + ctx?.clearRect(0, 0, canvasSize, canvasSize); + ctx.fillStyle = bgColor; + ctx.fillRect(0, 0, canvasSize, canvasSize); + + // extract and set padding data + if (img.naturalHeight > img.naturalWidth) { + // horizontal padding, x offset + const xOffset = Math.floor((canvasSize - width) / 2); + ctx.drawImage(img, xOffset, 0, width, height); + + // draw reflected image padding + this.drawHorizontalReflection(ctx, canvas, xOffset); + } else { + // vertical padding, y offset + const yOffset = Math.floor((canvasSize - height) / 2); + ctx.drawImage(img, 0, yOffset, width, height); + + // draw reflected image padding + this.drawVerticalReflection(ctx, canvas, yOffset); + } + return canvas; + }; + + /** + * Converts a url to base64 (tainted canvas workaround) + */ + static urlToBase64 = async (imageUrl: string): Promise<string | undefined> => { + try { + const res = await fetch(imageUrl); + const blob = await res.blob(); + + return new Promise<string>((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const base64Data = reader.result?.toString().split(',')[1]; + if (base64Data) { + resolve(base64Data); + } else { + reject(new Error('Failed to convert.')); + } + }; + reader.onerror = () => { + reject(new Error('Error reading image data')); + }; + reader.readAsDataURL(blob); + }); + } catch (err) { + console.error(err); + } + return undefined; + }; +} diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/PointerHandler.ts index 260923a64..e86f46636 100644 --- a/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts +++ b/src/client/views/nodes/imageEditor/imageEditorUtils/PointerHandler.ts @@ -1,4 +1,4 @@ -import { Point } from './generativeFillInterfaces'; +import { Point } from './imageEditorInterfaces'; export class PointerHandler { static getPointRelativeToElement = (element: HTMLElement, e: React.PointerEvent | PointerEvent, scale: number): Point => { diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorConstants.ts index 4772304bc..4772304bc 100644 --- a/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts +++ b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorConstants.ts diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts new file mode 100644 index 000000000..75659fc53 --- /dev/null +++ b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts @@ -0,0 +1,41 @@ +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { Doc } from '../../../../../fields/Doc'; + +export interface CursorData { + x: number; + y: number; + width: number; +} + +export interface Point { + x: number; + y: number; +} + +export enum ImageToolType { + GenerativeFill = 'genFill', + Cut = 'cut', +} + +export interface ImageEditTool { + type: ImageToolType; + name: string; + btnText: string; + icon: IconProp; + applyFunc: (currCutType: BrushMode, brushWidth: number, prevEdits: { url: string; saveRes: Doc | undefined }[], isFirstDoc: boolean) => Promise<void>; + sliderMin?: number; + sliderMax?: number; + sliderDefault?: number; +} + +export enum BrushMode { + IN, + OUT, + LINE_IN, + LINE_OUT, +} + +export interface ImageDimensions { + width: number; + height: number; +} diff --git a/src/client/views/nodes/imageEditor/imageToolUtils/BrushHandler.ts b/src/client/views/nodes/imageEditor/imageToolUtils/BrushHandler.ts new file mode 100644 index 000000000..7139bebc3 --- /dev/null +++ b/src/client/views/nodes/imageEditor/imageToolUtils/BrushHandler.ts @@ -0,0 +1,35 @@ +import { GenerativeFillMathHelpers } from '../imageEditorUtils/GenerativeFillMathHelpers'; +import { eraserColor } from '../imageEditorUtils/imageEditorConstants'; +import { Point } from '../imageEditorUtils/imageEditorInterfaces'; +import { points } from '@turf/turf'; + +export enum BrushType { + GEN_FILL, + CUT, +} + +export class BrushHandler { + static brushCircleOverlay = (x: number, y: number, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string /* , erase: boolean */) => { + ctx.globalCompositeOperation = 'destination-out'; + ctx.fillStyle = fillColor; + ctx.shadowColor = eraserColor; + ctx.shadowBlur = 5; + ctx.beginPath(); + ctx.arc(x, y, brushRadius, 0, 2 * Math.PI); + ctx.fill(); + ctx.closePath(); + }; + + static createBrushPathOverlay = (startPoint: Point, endPoint: Point, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string, brushType: BrushType) => { + const dist = GenerativeFillMathHelpers.distanceBetween(startPoint, endPoint); + const pts: Point[] = []; + for (let i = 0; i < dist; i += 5) { + const s = i / dist; + const x = startPoint.x * (1 - s) + endPoint.x * s; + const y = startPoint.y * (1 - s) + endPoint.y * s; + pts.push({ x: startPoint.x, y: startPoint.y }); + BrushHandler.brushCircleOverlay(x, y, brushRadius, ctx, fillColor); + } + return pts; + }; +} diff --git a/src/client/views/nodes/imageEditor/imageToolUtils/ImageHandler.ts b/src/client/views/nodes/imageEditor/imageToolUtils/ImageHandler.ts new file mode 100644 index 000000000..b9723b5be --- /dev/null +++ b/src/client/views/nodes/imageEditor/imageToolUtils/ImageHandler.ts @@ -0,0 +1,312 @@ +import { RefObject } from 'react'; +import { bgColor, canvasSize } from '../imageEditorUtils/imageEditorConstants'; + +export interface APISuccess { + status: 'success'; + urls: string[]; +} + +export interface APIError { + status: 'error'; + message: string; +} + +export class ImageUtility { + /** + * + * @param canvas Canvas to convert + * @returns Blob of canvas + */ + static canvasToBlob = (canvas: HTMLCanvasElement): Promise<Blob> => + new Promise(resolve => { + canvas.toBlob(blob => { + if (blob) { + resolve(blob); + } + }, 'image/png'); + }); + + // given a square api image, get the cropped img + static getCroppedImg = (img: HTMLImageElement, width: number, height: number): HTMLCanvasElement | undefined => { + // Create a new canvas element + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + if (ctx) { + // Clear the canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + if (width < height) { + // horizontal padding, x offset + const xOffset = (canvasSize - width) / 2; + ctx.drawImage(img, xOffset, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); + } else { + // vertical padding, y offset + const yOffset = (canvasSize - height) / 2; + ctx.drawImage(img, 0, yOffset, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); + } + return canvas; + } + return undefined; + }; + + // converts an image to a canvas data url + static convertImgToCanvasUrl = async (imageSrc: string, width: number, height: number): Promise<string> => + new Promise<string>((resolve, reject) => { + const img = new Image(); + img.onload = () => { + const canvas = this.getCroppedImg(img, width, height); + if (canvas) { + const dataUrl = canvas.toDataURL(); + resolve(dataUrl); + } + }; + img.onerror = error => { + reject(error); + }; + img.src = imageSrc; + }); + + // calls the openai api to get image edits + static getEdit = async (imgBlob: Blob, maskBlob: Blob, prompt: string, n?: number): Promise<APISuccess | APIError> => { + const apiUrl = 'https://api.openai.com/v1/images/edits'; + const fd = new FormData(); + fd.append('image', imgBlob, 'image.png'); + fd.append('mask', maskBlob, 'mask.png'); + fd.append('prompt', prompt); + fd.append('size', '1024x1024'); + fd.append('n', n ? JSON.stringify(n) : '1'); + fd.append('response_format', 'b64_json'); + + try { + const res = await fetch(apiUrl, { + method: 'POST', + headers: { + Authorization: `Bearer ${process.env.OPENAI_KEY}`, + }, + body: fd, + }); + const data = await res.json(); + console.log(data.data); + return { + status: 'success', + urls: (data.data as { b64_json: string }[]).map(urlData => `data:image/png;base64,${urlData.b64_json}`), + }; + } catch (err) { + console.log(err); + return { status: 'error', message: 'API error.' }; + } + }; + + // mock api call + static mockGetEdit = async (mockSrc: string): Promise<APISuccess | APIError> => ({ + status: 'success', + urls: [mockSrc, mockSrc, mockSrc], + }); + + // Gets the canvas rendering context of a canvas + static getCanvasContext = (canvasRef: RefObject<HTMLCanvasElement>): CanvasRenderingContext2D | null => { + if (!canvasRef.current) return null; + const ctx = canvasRef.current.getContext('2d'); + if (!ctx) return null; + return ctx; + }; + + // Helper for downloading the canvas (for debugging) + static downloadCanvas = (canvas: HTMLCanvasElement) => { + const url = canvas.toDataURL(); + const downloadLink = document.createElement('a'); + downloadLink.href = url; + downloadLink.download = 'canvas'; + + downloadLink.click(); + downloadLink.remove(); + }; + + // Download the canvas (for debugging) + static downloadImageCanvas = (imgUrl: string) => { + const img = new Image(); + img.src = imgUrl; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = canvasSize; + canvas.height = canvasSize; + const ctx = canvas.getContext('2d'); + ctx?.drawImage(img, 0, 0, canvasSize, canvasSize); + + this.downloadCanvas(canvas); + }; + }; + + // Clears the canvas + static clearCanvas = (canvasRef: React.RefObject<HTMLCanvasElement>) => { + const ctx = this.getCanvasContext(canvasRef); + if (!ctx || !canvasRef.current) return; + ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); + }; + + // Draws the image to the current canvas + static drawImgToCanvas = (img: HTMLImageElement, canvasRef: React.RefObject<HTMLCanvasElement>, width: number, height: number) => { + const drawImg = (htmlImg: HTMLImageElement) => { + const ctx = this.getCanvasContext(canvasRef); + if (!ctx) return; + ctx.globalCompositeOperation = 'source-over'; + ctx.clearRect(0, 0, width, height); + ctx.drawImage(htmlImg, 0, 0, width, height); + }; + + if (img.complete) { + drawImg(img); + } else { + img.onload = () => { + drawImg(img); + }; + } + }; + + // Gets the image mask for the openai endpoint + static getCanvasMask = (srcCanvas: HTMLCanvasElement, paddedCanvas: HTMLCanvasElement): HTMLCanvasElement | undefined => { + const canvas = document.createElement('canvas'); + canvas.width = canvasSize; + canvas.height = canvasSize; + const ctx = canvas.getContext('2d'); + if (!ctx) return undefined; + ctx?.clearRect(0, 0, canvasSize, canvasSize); + ctx.drawImage(paddedCanvas, 0, 0); + + // extract and set padding data + if (srcCanvas.height > srcCanvas.width) { + // horizontal padding, x offset + const xOffset = (canvasSize - srcCanvas.width) / 2; + ctx?.clearRect(xOffset, 0, srcCanvas.width, srcCanvas.height); + ctx.drawImage(srcCanvas, xOffset, 0, srcCanvas.width, srcCanvas.height); + } else { + // vertical padding, y offset + const yOffset = (canvasSize - srcCanvas.height) / 2; + ctx?.clearRect(0, yOffset, srcCanvas.width, srcCanvas.height); + ctx.drawImage(srcCanvas, 0, yOffset, srcCanvas.width, srcCanvas.height); + } + return canvas; + }; + + // Fills in the blank areas of the image with an image reflection (to fill in a square-shaped canvas) + static drawHorizontalReflection = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, xOffset: number) => { + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const { data } = imageData; + for (let i = 0; i < canvas.height; i++) { + for (let j = 0; j < xOffset; j++) { + const targetIdx = 4 * (i * canvas.width + j); + const sourceI = i; + const sourceJ = xOffset + (xOffset - j); + const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); + data[targetIdx] = data[sourceIdx]; + data[targetIdx + 1] = data[sourceIdx + 1]; + data[targetIdx + 2] = data[sourceIdx + 2]; + } + } + for (let i = 0; i < canvas.height; i++) { + for (let j = canvas.width - 1; j >= canvas.width - 1 - xOffset; j--) { + const targetIdx = 4 * (i * canvas.width + j); + const sourceI = i; + const sourceJ = canvas.width - 1 - xOffset - (xOffset - (canvas.width - j)); + const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); + data[targetIdx] = data[sourceIdx]; + data[targetIdx + 1] = data[sourceIdx + 1]; + data[targetIdx + 2] = data[sourceIdx + 2]; + } + } + ctx.putImageData(imageData, 0, 0); + }; + + // Fills in the blank areas of the image with an image reflection (to fill in a square-shaped canvas) + static drawVerticalReflection = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, yOffset: number) => { + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const { data } = imageData; + for (let j = 0; j < canvas.width; j++) { + for (let i = 0; i < yOffset; i++) { + const targetIdx = 4 * (i * canvas.width + j); + const sourceJ = j; + const sourceI = yOffset + (yOffset - i); + const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); + data[targetIdx] = data[sourceIdx]; + data[targetIdx + 1] = data[sourceIdx + 1]; + data[targetIdx + 2] = data[sourceIdx + 2]; + } + } + for (let j = 0; j < canvas.width; j++) { + for (let i = canvas.height - 1; i >= canvas.height - 1 - yOffset; i--) { + const targetIdx = 4 * (i * canvas.width + j); + const sourceJ = j; + const sourceI = canvas.height - 1 - yOffset - (yOffset - (canvas.height - i)); + const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); + data[targetIdx] = data[sourceIdx]; + data[targetIdx + 1] = data[sourceIdx + 1]; + data[targetIdx + 2] = data[sourceIdx + 2]; + } + } + ctx.putImageData(imageData, 0, 0); + }; + + // Gets the unaltered (besides filling in padding) version of the image for the api call + static getCanvasImg = (img: HTMLImageElement): HTMLCanvasElement | undefined => { + const canvas = document.createElement('canvas'); + canvas.width = canvasSize; + canvas.height = canvasSize; + const ctx = canvas.getContext('2d'); + if (!ctx) return undefined; + // fix scaling + const scale = Math.min(canvasSize / img.width, canvasSize / img.height); + const width = Math.floor(img.width * scale); + const height = Math.floor(img.height * scale); + ctx?.clearRect(0, 0, canvasSize, canvasSize); + ctx.fillStyle = bgColor; + ctx.fillRect(0, 0, canvasSize, canvasSize); + + // extract and set padding data + if (img.naturalHeight > img.naturalWidth) { + // horizontal padding, x offset + const xOffset = Math.floor((canvasSize - width) / 2); + ctx.drawImage(img, xOffset, 0, width, height); + + // draw reflected image padding + this.drawHorizontalReflection(ctx, canvas, xOffset); + } else { + // vertical padding, y offset + const yOffset = Math.floor((canvasSize - height) / 2); + ctx.drawImage(img, 0, yOffset, width, height); + + // draw reflected image padding + this.drawVerticalReflection(ctx, canvas, yOffset); + } + return canvas; + }; + + /** + * Converts a url to base64 (tainted canvas workaround) + */ + static urlToBase64 = async (imageUrl: string): Promise<string | undefined> => { + try { + const res = await fetch(imageUrl); + const blob = await res.blob(); + + return new Promise<string>((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const base64Data = reader.result?.toString().split(',')[1]; + if (base64Data) { + resolve(base64Data); + } else { + reject(new Error('Failed to convert.')); + } + }; + reader.onerror = () => { + reject(new Error('Error reading image data')); + }; + reader.readAsDataURL(blob); + }); + } catch (err) { + console.error(err); + } + return undefined; + }; +} diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx index d0f6566a5..4c5be047b 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.tsx +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -83,7 +83,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { /** * AddDrawing and RemoveDrawing are defined by the other classes that call the smart draw functions (i.e. CollectionFreeForm, FormattedTextBox, AnnotationPalette) to define how a drawing document should be added - or removed in their respective locations (to the freeform canvs, to the annotation palette's preview, etc.) + or removed in their respective locations (to the freeform canvas, to the sticker palette's preview, etc.) */ public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string) => void = unimplementedFunction; public RemoveDrawing: (useLastContainer: boolean, doc?: Doc) => void = unimplementedFunction; diff --git a/src/client/views/smartdraw/AnnotationPalette.scss b/src/client/views/smartdraw/StickerPalette.scss index 4f11e8afc..ca99410cf 100644 --- a/src/client/views/smartdraw/AnnotationPalette.scss +++ b/src/client/views/smartdraw/StickerPalette.scss @@ -1,4 +1,4 @@ -.annotation-palette { +.sticker-palette { display: flex; flex-direction: column; align-items: center; diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx index f1e2e4f41..468d0fd13 100644 --- a/src/client/views/smartdraw/AnnotationPalette.tsx +++ b/src/client/views/smartdraw/StickerPalette.tsx @@ -22,24 +22,25 @@ import { ObservableReactComponent } from '../ObservableReactComponent'; import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider'; import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; import { FieldView } from '../nodes/FieldView'; -import './AnnotationPalette.scss'; +import './StickerPalette.scss'; import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler'; -interface AnnotationPaletteProps { +interface StickerPaletteProps { Document: Doc; } /** - * The AnnotationPalette can be toggled in the lightbox view of a document. The goal of the palette - * is to offer an easy way for users to save then drag and drop repeated annotations onto a document. - * These annotations can be of any annotation type and operate similarly to user templates. + * The StickerPalette can be toggled in the lightbox view of a document. The goal of the palette + * is to offer an easy way for users to create stickers and drag and drop them onto a document. + * These stickers can technically be of any document type and operate similarly to user templates. + * However, the palette is designed to be geared toward ink stickers and image stickers. * - * On the "add" side of the palette, there is a way to create a drawing annotation with GPT. Users can + * On the "add" side of the palette, there is a way to create a drawing sticker with GPT. Users can * enter the item to draw, toggle different settings, then GPT will generate three versions of the drawing - * to choose from. These drawings can then be saved to the palette as annotations. + * to choose from. These drawings can then be saved to the palette as stickers. */ @observer -export class AnnotationPalette extends ObservableReactComponent<AnnotationPaletteProps> { +export class StickerPalette extends ObservableReactComponent<StickerPaletteProps> { @observable private _paletteMode: 'create' | 'view' = 'view'; @observable private _userInput: string = ''; @observable private _isLoading: boolean = false; @@ -50,13 +51,13 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett @observable private _opts: DrawingOptions = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 }; private _gptRes: string[] = []; - constructor(props: AnnotationPaletteProps) { + constructor(props: StickerPaletteProps) { super(props); makeObservable(this); } public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(AnnotationPalette, fieldKey); + return FieldView.LayoutString(StickerPalette, fieldKey); } Contains = (view: DocumentView) => { @@ -112,18 +113,18 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett }; /** - * Adds a doc to the annotation palette. Gets a snapshot of the document to use as a preview in the palette. When this - * preview is dragged onto a parent document, a copy of that document is added as an annotation. + * Adds a doc to the sticker palette. Gets a snapshot of the document to use as a preview in the palette. When this + * preview is dragged onto a parent document, a copy of that document is added as a sticker. */ public static addToPalette = async (doc: Doc) => { - if (!doc.savedAsAnno) { + if (!doc.savedAsSticker) { const docView = DocumentView.getDocumentView(doc); await docView?.ComponentView?.updateIcon?.(true); const { clone } = await Doc.MakeClone(doc); clone.title = doc.title; const image = ImageCast(doc.icon, ImageCast(clone[Doc.LayoutFieldKey(clone)]))?.url?.href; - Doc.AddDocToList(Doc.MyAnnos, 'data', makeUserTemplateButtonOrImage(clone, image)); - doc.savedAsAnno = true; + Doc.AddDocToList(Doc.MyStickers, 'data', makeUserTemplateButtonOrImage(clone, image)); + doc.savedAsSticker = true; } }; @@ -138,7 +139,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett /** * Calls the draw with GPT functions in SmartDrawHandler to allow users to generate drawings straight from - * the annotation palette. + * the sticker palette. */ @undoBatch generateDrawings = action(async () => { @@ -171,7 +172,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett }; /** - * Saves the currently showing, newly generated drawing to the annotation palette and sets the metadata. + * Saves the currently showing, newly generated drawing to the sticker palette and sets the metadata. * AddToPalette() is generically used to add any document to the palette, while this defines the behavior for when a user * presses the "save drawing" button. */ @@ -185,21 +186,21 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett docData.drawingColored = this._opts.autoColor; docData.drawingSize = this._opts.size; docData.drawingData = this._gptRes[cIndex]; - docData.width = this._opts.size; + focusedDrawing.width = this._opts.size; docData.x = this._opts.x; docData.y = this._opts.y; - await AnnotationPalette.addToPalette(focusedDrawing); + await StickerPalette.addToPalette(focusedDrawing); this.resetPalette(true); }; render() { return ( - <div className="annotation-palette" style={{ zIndex: 1000 }} onClick={e => e.stopPropagation()}> + <div className="sticker-palette" style={{ zIndex: 1000 }} onClick={e => e.stopPropagation()}> {this._paletteMode === 'view' && ( <> <DocumentView ref={r => (this._docView = r)} - Document={Doc.MyAnnos} + Document={Doc.MyStickers} addDocument={undefined} addDocTab={DocumentViewInternal.addDocTabFunc} pinToPres={DocumentView.PinDoc} @@ -342,7 +343,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett searchFilterDocs={returnEmptyDoclist} /> <div className="palette-buttons"> - <Button text="Back" tooltip="Back to All Annotations" icon={<FontAwesomeIcon icon="reply" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(true)} /> + <Button text="Back" tooltip="Back to All Stickers" icon={<FontAwesomeIcon icon="reply" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(true)} /> <div className="palette-save-reset"> <Button tooltip="Save" icon={<FontAwesomeIcon icon="file-arrow-down" />} color={SettingsManager.userColor} onClick={this.saveDrawing} /> <Button tooltip="Reset" icon={<FontAwesomeIcon icon="rotate-left" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(false)} /> @@ -356,6 +357,6 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett } Docs.Prototypes.TemplateMap.set(DocumentType.ANNOPALETTE, { - layout: { view: AnnotationPalette, dataField: 'data' }, + layout: { view: StickerPalette, dataField: 'data' }, options: { acl: '' }, }); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index aef7bf330..e62ca4bb8 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -236,7 +236,7 @@ export class Doc extends RefField { public static get MyPublishedDocs() { return DocListCast(Doc.ActiveDashboard?.myPublishedDocs).concat(DocListCast(DocCast(Doc.UserDoc().myPublishedDocs)?.data)); } // prettier-ignore public static get MyDashboards() { return DocCast(Doc.UserDoc().myDashboards); } // prettier-ignore public static get MyTemplates() { return DocCast(Doc.UserDoc().myTemplates); } // prettier-ignore - public static get MyAnnos() { return DocCast(Doc.UserDoc().myAnnos); } // prettier-ignore + public static get MyStickers() { return DocCast(Doc.UserDoc().myStickers); } // prettier-ignore public static get MyLightboxDrawings() { return DocCast(Doc.UserDoc().myLightboxDrawings); } // prettier-ignore public static get MyImports() { return DocCast(Doc.UserDoc().myImports); } // prettier-ignore public static get MyFilesystem() { return DocCast(Doc.UserDoc().myFilesystem); } // prettier-ignore |