From 3b17868560090756caf8b9b0f043ea163f2320e8 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Sun, 20 Oct 2024 12:44:15 -0400
Subject: changes

---
 src/client/views/nodes/imageEditor/ImageEditor.tsx | 758 +++++++++++++++++++++
 1 file changed, 758 insertions(+)
 create mode 100644 src/client/views/nodes/imageEditor/ImageEditor.tsx

(limited to 'src/client/views/nodes/imageEditor/ImageEditor.tsx')

diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
new file mode 100644
index 000000000..86f7d8d29
--- /dev/null
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -0,0 +1,758 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
+/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
+/* eslint-disable jsx-a11y/img-redundant-alt */
+/* 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 * as React from 'react';
+import { useEffect, useRef, useState } from 'react';
+import { CgClose } from 'react-icons/cg';
+import { IoMdRedo, IoMdUndo } from 'react-icons/io';
+import { ClientUtils } from '../../../../ClientUtils';
+import { Doc, DocListCast } from '../../../../fields/Doc';
+import { List } from '../../../../fields/List';
+import { NumCast } from '../../../../fields/Types';
+import { Networking } from '../../../Network';
+import { DocUtils } from '../../../documents/DocUtils';
+import { Docs } from '../../../documents/Documents';
+import { CollectionDockingView } from '../../collections/CollectionDockingView';
+import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
+import { ImageEditorData } from '../ImageBox';
+import { OpenWhereMod } from '../OpenWhere';
+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, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './imageEditorUtils/imageEditorConstants';
+import { 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';
+
+interface GenerativeFillProps {
+    imageEditorOpen: boolean;
+    imageEditorSource: string;
+    imageRootDoc: Doc | undefined;
+    addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined;
+}
+
+// Added field on image doc: gen_fill_children: List of children Docs
+
+const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc }: GenerativeFillProps) => {
+    const canvasRef = useRef<HTMLCanvasElement>(null);
+    const canvasBackgroundRef = useRef<HTMLCanvasElement>(null);
+    const drawingAreaRef = useRef<HTMLDivElement>(null);
+    const [cursorData, setCursorData] = useState<CursorData>({
+        x: 0,
+        y: 0,
+        width: 150,
+    });
+    const [isBrushing, setIsBrushing] = useState(false);
+    const [canvasScale, setCanvasScale] = useState(0.5);
+    // format: array of [image source, corresponding image Doc]
+    const [edits, setEdits] = useState<{ url: string; saveRes: Doc | undefined }[]>([]);
+    const [edited, setEdited] = useState(false);
+    const isFirstDoc = useRef(true);
+    // const [brushStyle] = useState<BrushStyle>(BrushStyle.ADD);
+    const [input, setInput] = useState('');
+    const [loading, setLoading] = useState(false);
+    const [canvasDims, setCanvasDims] = useState<ImageDimensions>({
+        width: canvasSize,
+        height: canvasSize,
+    });
+    // whether to create a new collection or not
+    const [isNewCollection, setIsNewCollection] = useState(true);
+    // the current image in the main canvas
+    const currImg = useRef<HTMLImageElement | null>(null);
+    // the unedited version of each generation (parent)
+    const originalImg = useRef<HTMLImageElement | null>(null);
+    const originalDoc = useRef<Doc | null>(null);
+    // stores history of data urls
+    const undoStack = useRef<string[]>([]);
+    // stores redo stack
+    const redoStack = useRef<string[]>([]);
+
+    // references to keep track of tree structure
+    const newCollectionRef = useRef<Doc | null>(null);
+    const parentDoc = useRef<Doc | null>(null);
+    const childrenDocs = useRef<Doc[]>([]);
+
+    // 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);
+        if (!ctx || !currImg.current || !canvasRef.current) return;
+
+        const target = undoStack.current[undoStack.current.length - 1];
+        if (!target) {
+            ImageUtility.drawImgToCanvas(currImg.current, canvasRef, canvasDims.width, canvasDims.height);
+        } else {
+            redoStack.current = [...redoStack.current, canvasRef.current.toDataURL()];
+            const img = new Image();
+            img.src = target;
+            ImageUtility.drawImgToCanvas(img, canvasRef, canvasDims.width, canvasDims.height);
+            undoStack.current = undoStack.current.slice(0, -1);
+        }
+    };
+
+    const handleRedo = () => {
+        const ctx = ImageUtility.getCanvasContext(canvasRef);
+        if (!ctx || !currImg.current || !canvasRef.current) return;
+
+        const target = redoStack.current[redoStack.current.length - 1];
+        if (target) {
+            undoStack.current = [...undoStack.current, canvasRef.current?.toDataURL()];
+            const img = new Image();
+            img.src = target;
+            ImageUtility.drawImgToCanvas(img, canvasRef, canvasDims.width, canvasDims.height);
+            redoStack.current = redoStack.current.slice(0, -1);
+        }
+    };
+
+    // resets any erase strokes
+    const handleReset = () => {
+        if (!canvasRef.current || !currImg.current) return;
+        const ctx = ImageUtility.getCanvasContext(canvasRef);
+        if (!ctx) return;
+        ctx.clearRect(0, 0, canvasSize, canvasSize);
+        undoStack.current = [];
+        redoStack.current = [];
+        ImageUtility.drawImgToCanvas(currImg.current, canvasRef, canvasDims.width, canvasDims.height);
+    };
+
+    // initiate brushing
+    const handlePointerDown = (e: React.PointerEvent) => {
+        const canvas = canvasRef.current;
+        if (!canvas) return;
+        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);
+        BrushHandler.brushCircleOverlay(x, y, cursorData.width / 2 / canvasScale, ctx, eraserColor /* , brushStyle === BrushStyle.SUBTRACT */);
+    };
+
+    // stop brushing, push to undo stack
+    const handlePointerUp = () => {
+        const ctx = ImageUtility.getCanvasContext(canvasBackgroundRef);
+        if (!ctx) return;
+        if (!isBrushing) return;
+        setIsBrushing(false);
+    };
+
+    // handles brushing on pointer movement
+    useEffect(() => {
+        if (!isBrushing) return undefined;
+        const canvas = canvasRef.current;
+        if (!canvas) return undefined;
+        const ctx = ImageUtility.getCanvasContext(canvasRef);
+        if (!ctx) return undefined;
+
+        const handlePointerMove = (e: PointerEvent) => {
+            const currPoint = PointerHandler.getPointRelativeToElement(canvas, e, canvasScale);
+            const lastPoint: Point = {
+                x: currPoint.x - e.movementX / canvasScale,
+                y: currPoint.y - e.movementY / canvasScale,
+            };
+            const pts = BrushHandler.createBrushPathOverlay(lastPoint, currPoint, cursorData.width / 2 / canvasScale, ctx, eraserColor, BrushType.CUT);
+            cutPts.current.push(...pts);
+        };
+
+        drawingAreaRef.current?.addEventListener('pointermove', handlePointerMove);
+        return () => {
+            drawingAreaRef.current?.removeEventListener('pointermove', handlePointerMove);
+        };
+    }, [isBrushing]);
+
+    // first load
+    useEffect(() => {
+        const loadInitial = async () => {
+            if (!imageEditorSource || imageEditorSource === '') return;
+            const img = new Image();
+            const res = await ImageUtility.urlToBase64(imageEditorSource);
+            if (!res) return;
+            img.src = `data:image/png;base64,${res}`;
+
+            img.onload = () => {
+                currImg.current = img;
+                originalImg.current = img;
+                const imgWidth = img.naturalWidth;
+                const imgHeight = img.naturalHeight;
+                const scale = Math.min(canvasSize / imgWidth, canvasSize / imgHeight);
+                const width = imgWidth * scale;
+                const height = imgHeight * scale;
+                setCanvasDims({ width, height });
+            };
+        };
+
+        loadInitial();
+
+        // cleanup
+        return () => {
+            setInput('');
+            setEdited(false);
+            newCollectionRef.current = null;
+            parentDoc.current = null;
+            childrenDocs.current = [];
+            currImg.current = null;
+            originalImg.current = null;
+            originalDoc.current = null;
+            undoStack.current = [];
+            redoStack.current = [];
+            ImageUtility.clearCanvas(canvasRef);
+        };
+    }, [canvasRef, imageEditorSource]);
+
+    // once the appropriate dimensions are set, draw the image to the canvas
+    useEffect(() => {
+        if (!currImg.current) return;
+        ImageUtility.drawImgToCanvas(currImg.current, canvasRef, canvasDims.width, canvasDims.height);
+    }, [canvasDims]);
+
+    // handles brush sizing
+    useEffect(() => {
+        const handleKeyPress = (e: KeyboardEvent) => {
+            if (e.key === 'ArrowUp') {
+                e.preventDefault();
+                e.stopPropagation();
+                setCursorData(data => ({ ...data, width: data.width + 5 }));
+            } else if (e.key === 'ArrowDown') {
+                e.preventDefault();
+                e.stopPropagation();
+                setCursorData(data => (data.width >= 20 ? { ...data, width: data.width - 5 } : data));
+            }
+        };
+        window.addEventListener('keydown', handleKeyPress);
+        return () => window.removeEventListener('keydown', handleKeyPress);
+    }, []);
+
+    // handle pinch zoom
+    useEffect(() => {
+        const handlePinch = (e: WheelEvent) => {
+            e.preventDefault();
+            e.stopPropagation();
+            const delta = e.deltaY;
+            const scaleFactor = delta > 0 ? 0.98 : 1.02;
+            setCanvasScale(prevScale => prevScale * scaleFactor);
+        };
+
+        drawingAreaRef.current?.addEventListener('wheel', handlePinch, {
+            passive: false,
+        });
+        return () => drawingAreaRef.current?.removeEventListener('wheel', handlePinch);
+    }, [drawingAreaRef]);
+
+    // updates the current position of the cursor
+    const updateCursorData = (e: React.PointerEvent) => {
+        const drawingArea = drawingAreaRef.current;
+        if (!drawingArea) return;
+        const { x, y } = PointerHandler.getPointRelativeToElement(drawingArea, e, 1);
+        setCursorData(data => ({
+            ...data,
+            x,
+            y,
+        }));
+    };
+
+    // Get AI Edit
+    const getEdit = async () => {
+        const img = currImg.current;
+        if (!img) return;
+        const canvas = canvasRef.current;
+        if (!canvas) return;
+        const ctx = ImageUtility.getCanvasContext(canvasRef);
+        if (!ctx) return;
+        setLoading(true);
+        setEdited(true);
+        try {
+            const canvasOriginalImg = ImageUtility.getCanvasImg(img);
+            if (!canvasOriginalImg) return;
+            const canvasMask = ImageUtility.getCanvasMask(canvas, canvasOriginalImg);
+            if (!canvasMask) return;
+            const maskBlob = await ImageUtility.canvasToBlob(canvasMask);
+            const imgBlob = await ImageUtility.canvasToBlob(canvasOriginalImg);
+            const res = await ImageUtility.getEdit(imgBlob, maskBlob, input !== '' ? input + ' in the same style' : 'Fill in the image in the same style', 2);
+
+            // create first image
+            if (!newCollectionRef.current) {
+                if (!isNewCollection && imageRootDoc) {
+                    // if the parent hasn't been set yet
+                    if (!parentDoc.current) parentDoc.current = imageRootDoc;
+                } else {
+                    if (!(originalImg.current && imageRootDoc)) return;
+                    // create new collection and add it to the view
+                    newCollectionRef.current = Docs.Create.FreeformDocument([], {
+                        x: NumCast(imageRootDoc.x) + NumCast(imageRootDoc._width) + offsetX,
+                        y: NumCast(imageRootDoc.y),
+                        _width: newCollectionSize,
+                        _height: newCollectionSize,
+                        title: 'Image edit collection',
+                    });
+                    DocUtils.MakeLink(imageRootDoc, newCollectionRef.current, { link_relationship: 'Image Edit Version History' });
+
+                    // opening new tab
+                    CollectionDockingView.AddSplit(newCollectionRef.current, OpenWhereMod.right);
+
+                    // add the doc to the main freeform
+                    // eslint-disable-next-line no-use-before-define
+                    await createNewImgDoc(originalImg.current, true);
+                }
+            } else {
+                childrenDocs.current = [];
+            }
+
+            originalImg.current = currImg.current;
+            originalDoc.current = parentDoc.current;
+            const { urls } = res as APISuccess;
+            if (res.status !== 'error') {
+                const imgUrls = await Promise.all(urls.map(url => ImageUtility.convertImgToCanvasUrl(url, canvasDims.width, canvasDims.height)));
+                const imgRes = await Promise.all(
+                    imgUrls.map(async url => {
+                        // eslint-disable-next-line no-use-before-define
+                        const saveRes = await onSave(url);
+                        return { url, saveRes };
+                    })
+                );
+                setEdits(imgRes);
+                const image = new Image();
+                // eslint-disable-next-line prefer-destructuring
+                image.src = imgUrls[0];
+                ImageUtility.drawImgToCanvas(image, canvasRef, canvasDims.width, canvasDims.height);
+                currImg.current = image;
+                parentDoc.current = imgRes[0].saveRes ?? null;
+            }
+        } catch (err) {
+            console.log(err);
+        }
+        setLoading(false);
+    };
+
+    const cutImage = async () => {
+        const img = currImg.current;
+        const canvas = canvasRef.current;
+        if (!canvas || !img) return;
+        const ctx = ImageUtility.getCanvasContext(canvasRef);
+        if (!ctx) return;
+        setLoading(true);
+        // get the original image
+        const canvasOriginalImg = ImageUtility.getCanvasImg(img);
+        if (!canvasOriginalImg) return;
+        // NOTE: cutting two diff shapes can be made possible by having the user press a button to set a new shape!
+        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
+            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);
+            }
+            ctx.closePath();
+            ctx.globalCompositeOperation = 'destination-in';
+            ctx.fill();
+        }
+
+        const url = canvas.toDataURL(); // this does the same thing as convert img to canvasurl
+        if (!newCollectionRef.current) {
+            if (!isNewCollection && imageRootDoc) {
+                // if the parent hasn't been set yet
+                if (!parentDoc.current) parentDoc.current = imageRootDoc;
+            } else {
+                if (!(originalImg.current && imageRootDoc)) return;
+                // create new collection and add it to the view
+                newCollectionRef.current = Docs.Create.FreeformDocument([], {
+                    x: NumCast(imageRootDoc.x) + NumCast(imageRootDoc._width) + offsetX,
+                    y: NumCast(imageRootDoc.y),
+                    _width: newCollectionSize,
+                    _height: newCollectionSize,
+                    title: 'Image edit collection',
+                });
+                DocUtils.MakeLink(imageRootDoc, newCollectionRef.current, { link_relationship: 'Image Edit Version History' });
+                // opening new tab
+                CollectionDockingView.AddSplit(newCollectionRef.current, OpenWhereMod.right);
+            }
+        }
+
+        const image = new Image();
+        image.src = url;
+        image.onload = async () => {
+            const croppedCanvas = document.createElement('canvas');
+            const croppedCtx = croppedCanvas.getContext('2d');
+            if (!croppedCtx) return;
+            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;
+
+            currImg.current = croppedImage;
+            const newImgDoc = await createNewImgDoc(croppedImage, isFirstDoc.current);
+            if (isFirstDoc.current) isFirstDoc.current = false;
+            if (newImgDoc) {
+                const docData = newImgDoc[DocData];
+                docData.backgroundColor = 'transparent';
+            }
+            setEdits(prevEdits => [...prevEdits, { url: croppedURL, saveRes: undefined }]);
+            setLoading(false);
+            cutPts.current.length = 0;
+        };
+    };
+
+    // adjusts all the img positions to be aligned
+    const adjustImgPositions = () => {
+        if (!parentDoc.current) return;
+        const startY = NumCast(parentDoc.current.y);
+        const children = DocListCast(parentDoc.current.gen_fill_children);
+        const len = children.length;
+        const initialYPositions: number[] = [];
+        for (let i = 0; i < len; i++) {
+            initialYPositions.push(startY + i * offsetDistanceY);
+        }
+        children.forEach((doc, i) => {
+            if (len % 2 === 1) {
+                doc.y = initialYPositions[i] - Math.floor(len / 2) * offsetDistanceY;
+            } else {
+                doc.y = initialYPositions[i] - (len / 2 - 1 / 2) * offsetDistanceY;
+            }
+        });
+    };
+
+    // creates a new image document and returns its reference
+    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] });
+        const source = ClientUtils.prepend(result.accessPaths.agnostic.client);
+
+        if (firstDoc) {
+            const x = 0;
+            const initialY = 0;
+            const newImg = Docs.Create.ImageDocument(source, {
+                x: x,
+                y: initialY,
+                _height: freeformRenderSize,
+                _width: freeformRenderSize,
+                data_nativeWidth: result.nativeWidth,
+                data_nativeHeight: result.nativeHeight,
+            });
+            if (isNewCollection && newCollectionRef.current) {
+                Doc.AddDocToList(newCollectionRef.current, undefined, newImg);
+            } else {
+                addDoc?.(newImg);
+            }
+            parentDoc.current = newImg;
+            return newImg;
+        }
+        if (!parentDoc.current) return undefined;
+        const x = NumCast(parentDoc.current.x) + freeformRenderSize + offsetX;
+        const initialY = 0;
+
+        const newImg = Docs.Create.ImageDocument(source, {
+            x: x,
+            y: initialY,
+            _height: freeformRenderSize,
+            _width: freeformRenderSize,
+            data_nativeWidth: result.nativeWidth,
+            data_nativeHeight: result.nativeHeight,
+        });
+
+        const parentList = DocListCast(parentDoc.current.gen_fill_children);
+        if (parentList.length > 0) {
+            parentList.push(newImg);
+            parentDoc.current.gen_fill_children = new List<Doc>(parentList);
+        } else {
+            parentDoc.current.gen_fill_children = new List<Doc>([newImg]);
+        }
+
+        DocUtils.MakeLink(parentDoc.current, newImg, { link_relationship: `Image edit; Prompt: ${input}` });
+        adjustImgPositions();
+
+        if (isNewCollection && newCollectionRef.current) {
+            Doc.AddDocToList(newCollectionRef.current, undefined, newImg);
+        } else {
+            addDoc?.(newImg);
+        }
+        return newImg;
+    };
+
+    // Saves an image to the collection
+    const onSave = async (src: string) => {
+        const img = new Image();
+        img.src = src;
+        if (!currImg.current || !originalImg.current || !imageRootDoc) return undefined;
+        try {
+            const res = await createNewImgDoc(img, false);
+            return res;
+        } catch (err) {
+            console.log(err);
+        }
+        return undefined;
+    };
+
+    // Closes the editor view
+    const handleViewClose = () => {
+        ImageEditorData.Open = false;
+        ImageEditorData.Source = '';
+        if (newCollectionRef.current) {
+            DocumentView.addViewRenderedCb(newCollectionRef.current, dv => (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce());
+        }
+        setEdits([]);
+    };
+
+    // 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 className="imageEditorControls">
+                    <FormControlLabel
+                        control={
+                            <Checkbox
+                                // disable once edited has been clicked (doesn't make sense to change after first edit)
+                                disabled={edited}
+                                checked={isNewCollection}
+                                onChange={() => {
+                                    setIsNewCollection(prev => !prev);
+                                }}
+                            />
+                        }
+                        label="Create New Collection"
+                        labelPlacement="end"
+                        sx={{ whiteSpace: 'nowrap' }}
+                    />
+                    <ApplyFuncButtons onClick={currTool.applyFunc} loading={loading} onReset={handleReset} btnText={currTool.btnText} />
+                    <IconButton color={activeColor} tooltip="close" icon={<CgClose size="16px" />} onClick={handleViewClose} />
+                </div>
+            </div>
+        );
+    }
+
+    // the side icons including tool type, the slider, and undo/redo
+    function renderSideIcons() {
+        return (
+            <div className="iconContainer">
+                <div className="imageToolsContainer">
+                    {imageEditTools.map(tool => {
+                        return ImageToolButton(tool, tool.type === currTool.type, changeTool);
+                    })}
+                </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 */}
+                <div className="undoRedoContainer">
+                    <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>
+            </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
+                            alt="image stuff"
+                            width={75}
+                            src={originalImg.current?.src}
+                            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;
+                                if (!parentDoc.current) parentDoc.current = originalDoc.current;
+                            }}
+                        />
+                    </div>
+                )}
+            </div>
+        );
+    }
+
+    // the prompt box for generative fill
+    function renderPromptBox() {
+        return (
+            <div>
+                <TextField
+                    value={input}
+                    onChange={e => setInput(e.target.value)}
+                    disabled={isBrushing}
+                    type="text"
+                    label="Prompt"
+                    placeholder="Prompt..."
+                    InputLabelProps={{ style: { fontSize: '16px' } }}
+                    inputProps={{ style: { fontSize: '16px' } }}
+                    sx={{
+                        backgroundColor: '#ffffff',
+                        position: 'absolute',
+                        bottom: '16px',
+                        transform: 'translateX(calc(50vw - 50%))',
+                        width: 'calc(100vw - 64px)',
+                    }}
+                />
+            </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 ImageEditor;
-- 
cgit v1.2.3-70-g09d2


From d0569fcb79389b3c4f3d60e2a84f6f9220ca9cf3 Mon Sep 17 00:00:00 2001
From: eleanor-park <113556828+eleanor-park@users.noreply.github.com>
Date: Sun, 20 Oct 2024 23:52:20 -0400
Subject: added different cut types, haven't been able to run & test

---
 .../views/nodes/imageEditor/ImageEditor.scss       |  6 +++
 src/client/views/nodes/imageEditor/ImageEditor.tsx | 56 ++++++++++++++++++++--
 .../imageEditorUtils/imageEditorInterfaces.ts      |  6 ++-
 3 files changed, 61 insertions(+), 7 deletions(-)

(limited to 'src/client/views/nodes/imageEditor/ImageEditor.tsx')

diff --git a/src/client/views/nodes/imageEditor/ImageEditor.scss b/src/client/views/nodes/imageEditor/ImageEditor.scss
index 21c28f6da..49146aa23 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.scss
+++ b/src/client/views/nodes/imageEditor/ImageEditor.scss
@@ -89,6 +89,12 @@ $scale: 0.5;
                 gap: 10px;
             }
 
+            .cutToolsContainer {
+                display: grid;
+                gap: 5px;
+                grid-template-columns: 1fr 1fr;
+            }
+
             .undoRedoContainer {
                 justify-content: center;
                 display: flex;
diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index 86f7d8d29..d9f46876e 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -25,13 +25,14 @@ import { ApplyFuncButtons, ImageToolButton } from './ImageEditorButtons';
 import { BrushHandler, BrushType } from './imageEditorUtils/BrushHandler';
 import { APISuccess, ImageUtility } from './imageEditorUtils/ImageHandler';
 import { PointerHandler } from './imageEditorUtils/PointerHandler';
-import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './imageEditorUtils/imageEditorConstants';
-import { CursorData, ImageDimensions, ImageEditTool, ImageToolType, Point } from './imageEditorUtils/imageEditorInterfaces';
+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;
@@ -57,13 +58,13 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
     const [edits, setEdits] = useState<{ url: string; saveRes: Doc | undefined }[]>([]);
     const [edited, setEdited] = useState(false);
     const isFirstDoc = useRef(true);
-    // const [brushStyle] = useState<BrushStyle>(BrushStyle.ADD);
     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
@@ -84,6 +85,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
     // constants for image cutting
     const cutPts = useRef<Point[]>([]);
 
+
     /**
      *
      * @param type The new tool type we are changing to
@@ -380,8 +382,21 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                 maxY = Math.max(cutPts.current[i].y, maxY);
             }
             ctx.closePath();
-            ctx.globalCompositeOperation = 'destination-in';
-            ctx.fill();
+            switch (cutType) { // may need to move this before the drawing at all for the line cases
+                case BrushMode.IN:
+                    ctx.globalCompositeOperation = 'destination-in';
+                    ctx.fill();
+                    break;
+                case BrushMode.OUT:
+                    ctx.globalCompositeOperation = 'destination-out';
+                    ctx.fill();
+                    break;
+                case BrushMode.LINE_OUT:
+                    ctx.globalCompositeOperation = 'destination-out';
+                    break;
+                case BrushMode.LINE_IN:
+                    ctx.globalCompositeOperation = 'destination-in';
+            }
         }
 
         const url = canvas.toDataURL(); // this does the same thing as convert img to canvasurl
@@ -578,6 +593,37 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                         return ImageToolButton(tool, tool.type === currTool.type, changeTool);
                     })}
                 </div>
+                {currTool.type == ImageToolType.Cut && 
+                    <div className="cutToolContainer"> 
+                        <Button 
+                            text="Keep in"
+                            type={Type.TERT}
+                            color={cutType == BrushMode.IN ? SettingsManager.userVariantColor : bgColor}
+                            onClick={() => {
+                                setCutType(BrushMode.IN);
+                            }}/>
+                        <Button 
+                            text="Keep out"
+                            type={Type.TERT}
+                            color={cutType == BrushMode.OUT ? SettingsManager.userVariantColor : bgColor}
+                            onClick={() => {
+                                setCutType(BrushMode.OUT);
+                            }}/>
+                        <Button 
+                            text="Draw in"
+                            type={Type.TERT}
+                            color={cutType == BrushMode.LINE_IN ? SettingsManager.userVariantColor : bgColor}
+                            onClick={() => {
+                                setCutType(BrushMode.LINE_IN);
+                            }}/>
+                        <Button 
+                            text="Erase"
+                            type={Type.TERT}
+                            color={cutType == BrushMode.LINE_OUT ? SettingsManager.userVariantColor : bgColor}
+                            onClick={() => {
+                                setCutType(BrushMode.LINE_OUT);
+                            }}/>
+                    </div>}
                 <div className="sliderContainer" onPointerDown={e => e.stopPropagation()}>
                     {currTool.type === ImageToolType.GenerativeFill && (
                         <Slider
diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
index f4ae7d9c4..bd2fac775 100644
--- a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
+++ b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
@@ -28,8 +28,10 @@ export interface ImageEditTool {
 }
 
 export enum BrushMode {
-    ADD,
-    SUBTRACT,
+    IN,
+    OUT,
+    LINE_IN,
+    LINE_OUT,
 }
 
 export interface ImageDimensions {
-- 
cgit v1.2.3-70-g09d2


From e8661840c39219fd848e664c990943450c41bbee Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Mon, 28 Oct 2024 23:10:24 -0400
Subject: basic structure of 4 cut modes implemented, many bugs to fix

---
 .../views/nodes/imageEditor/ImageEditor.scss       |  59 +++--
 src/client/views/nodes/imageEditor/ImageEditor.tsx | 265 +++++++++++----------
 .../views/nodes/imageEditor/ImageEditorButtons.tsx |   2 +-
 .../imageEditorUtils/imageEditorInterfaces.ts      |   3 +-
 4 files changed, 180 insertions(+), 149 deletions(-)

(limited to 'src/client/views/nodes/imageEditor/ImageEditor.tsx')

diff --git a/src/client/views/nodes/imageEditor/ImageEditor.scss b/src/client/views/nodes/imageEditor/ImageEditor.scss
index 49146aa23..c691e6a18 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.scss
+++ b/src/client/views/nodes/imageEditor/ImageEditor.scss
@@ -75,38 +75,47 @@ $scale: 0.5;
             }
         }
 
-        .iconContainer {
+        .sideControlsContainer {
+            width: 160px;
             position: absolute;
-            top: 3rem;
-            left: 2rem;
-            display: flex;
-            flex-direction: column;
-            gap: 4rem;
+            left: 0;
+            height: 100%;
 
-            .imageToolsContainer {
+            .sideControls {
+                position: absolute;
+                width: 120px;
+                top: 3rem;
+                left: 2rem;
                 display: flex;
                 flex-direction: column;
-                gap: 10px;
-            }
+                gap: 1rem;
 
-            .cutToolsContainer {
-                display: grid;
-                gap: 5px;
-                grid-template-columns: 1fr 1fr;
-            }
+                .imageToolsContainer {
+                    display: flex;
+                    flex-direction: column;
+                    gap: 10px;
+                }
 
-            .undoRedoContainer {
-                justify-content: center;
-                display: flex;
-                flex-direction: row;
-            }
+                .cutToolsContainer {
+                    display: grid;
+                    gap: 5px;
+                    grid-template-columns: 1fr 1fr;
+                }
 
-            .sliderContainer {
-                height: 225px;
-                width: 100%;
-                display: flex;
-                justify-content: center;
-                cursor: pointer;
+                .undoRedoContainer {
+                    justify-content: center;
+                    display: flex;
+                    flex-direction: row;
+                }
+
+                .sliderContainer {
+                    margin: 3rem 0;
+                    height: 225px;
+                    width: 100%;
+                    display: flex;
+                    justify-content: center;
+                    cursor: pointer;
+                }
             }
         }
 
diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index d9f46876e..980f3e566 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.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';
@@ -85,7 +85,6 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
     // constants for image cutting
     const cutPts = useRef<Point[]>([]);
 
-
     /**
      *
      * @param type The new tool type we are changing to
@@ -356,7 +355,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         setLoading(false);
     };
 
-    const cutImage = async () => {
+    const cutImage = async (currCutType: BrushMode, brushWidth: number, prevEdits: { url: string; saveRes: Doc | undefined }[]) => {
         const img = currImg.current;
         const canvas = canvasRef.current;
         if (!canvas || !img) return;
@@ -381,21 +380,22 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                 maxX = Math.max(cutPts.current[i].x, maxX);
                 maxY = Math.max(cutPts.current[i].y, maxY);
             }
-            ctx.closePath();
-            switch (cutType) { // may need to move this before the drawing at all for the line cases
+            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_OUT:
-                    ctx.globalCompositeOperation = 'destination-out';
-                    break;
                 case BrushMode.LINE_IN:
                     ctx.globalCompositeOperation = 'destination-in';
+                    ctx.lineWidth = brushWidth + 20;
+                    ctx.stroke();
+                    break;
             }
         }
 
@@ -423,31 +423,41 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         const image = new Image();
         image.src = url;
         image.onload = async () => {
-            const croppedCanvas = document.createElement('canvas');
-            const croppedCtx = croppedCanvas.getContext('2d');
-            if (!croppedCtx) return;
-            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;
-
-            currImg.current = croppedImage;
-            const newImgDoc = await createNewImgDoc(croppedImage, isFirstDoc.current);
-            if (isFirstDoc.current) isFirstDoc.current = false;
+            let finalImg: HTMLImageElement | undefined = image;
+            let finalImgURL: string = url;
+            if (currCutType == BrushMode.IN) {
+                const croppedData = cropImage(image, minX, maxY, minY, maxY);
+                finalImg = croppedData;
+                finalImgURL = croppedData.src;
+            }
+            currImg.current = finalImg;
+            const newImgDoc = await createNewImgDoc(finalImg, isFirstDoc.current);
             if (newImgDoc) {
                 const docData = newImgDoc[DocData];
                 docData.backgroundColor = 'transparent';
+                if (isFirstDoc.current) isFirstDoc.current = false;
+                setEdits([...prevEdits, { url: finalImgURL, saveRes: undefined }]);
             }
-            setEdits(prevEdits => [...prevEdits, { url: croppedURL, 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
     const adjustImgPositions = () => {
         if (!parentDoc.current) return;
@@ -577,7 +587,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                         labelPlacement="end"
                         sx={{ whiteSpace: 'nowrap' }}
                     />
-                    <ApplyFuncButtons onClick={currTool.applyFunc} loading={loading} onReset={handleReset} btnText={currTool.btnText} />
+                    <ApplyFuncButtons onClick={() => currTool.applyFunc(cutType, cursorData.width, edits)} loading={loading} onReset={handleReset} btnText={currTool.btnText} />
                     <IconButton color={activeColor} tooltip="close" icon={<CgClose size="16px" />} onClick={handleViewClose} />
                 </div>
             </div>
@@ -587,109 +597,120 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
     // the side icons including tool type, the slider, and undo/redo
     function renderSideIcons() {
         return (
-            <div className="iconContainer">
-                <div className="imageToolsContainer">
-                    {imageEditTools.map(tool => {
-                        return ImageToolButton(tool, tool.type === currTool.type, changeTool);
-                    })}
-                </div>
-                {currTool.type == ImageToolType.Cut && 
-                    <div className="cutToolContainer"> 
-                        <Button 
-                            text="Keep in"
-                            type={Type.TERT}
-                            color={cutType == BrushMode.IN ? SettingsManager.userVariantColor : bgColor}
-                            onClick={() => {
-                                setCutType(BrushMode.IN);
-                            }}/>
-                        <Button 
-                            text="Keep out"
-                            type={Type.TERT}
-                            color={cutType == BrushMode.OUT ? SettingsManager.userVariantColor : bgColor}
-                            onClick={() => {
-                                setCutType(BrushMode.OUT);
-                            }}/>
-                        <Button 
-                            text="Draw in"
-                            type={Type.TERT}
-                            color={cutType == BrushMode.LINE_IN ? SettingsManager.userVariantColor : bgColor}
-                            onClick={() => {
-                                setCutType(BrushMode.LINE_IN);
-                            }}/>
-                        <Button 
-                            text="Erase"
-                            type={Type.TERT}
-                            color={cutType == BrushMode.LINE_OUT ? SettingsManager.userVariantColor : 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',
-                                },
+            <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 */}
+                    <div className="undoRedoContainer">
+                        <IconButton
+                            style={{ cursor: 'pointer' }}
+                            onPointerDown={e => {
+                                e.stopPropagation();
+                                handleUndo();
                             }}
-                            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 }));
+                            onPointerUp={e => {
+                                e.stopPropagation();
                             }}
+                            color={activeColor}
+                            tooltip="Undo"
+                            icon={<IoMdUndo />}
                         />
-                    )}
-                    {currTool.type === ImageToolType.Cut && (
-                        <Slider
-                            sx={{
-                                '& input[type="range"]': {
-                                    WebkitAppearance: 'slider-vertical',
-                                },
+                        <IconButton
+                            style={{ cursor: 'pointer' }}
+                            onPointerDown={e => {
+                                e.stopPropagation();
+                                handleRedo();
                             }}
-                            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 }));
+                            onPointerUp={e => {
+                                e.stopPropagation();
                             }}
+                            color={activeColor}
+                            tooltip="Redo"
+                            icon={<IoMdRedo />}
                         />
-                    )}
-                </div>
-                {/* Undo and Redo */}
-                <div className="undoRedoContainer">
-                    <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>
                 </div>
             </div>
         );
diff --git a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
index e90babf9b..cb963616b 100644
--- a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
@@ -55,7 +55,7 @@ export function ImageToolButton(tool: ImageEditTool, isActive: boolean, selectTo
     return (
         <div className="imageEditorButtonContainer">
             <Button
-                style={{ width: '100%', border: '1px' }}
+                style={{ width: '100%' }}
                 text={tool.name}
                 type={Type.TERT}
                 color={isActive ? SettingsManager.userVariantColor : bgColor}
diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
index bd2fac775..660af7c42 100644
--- a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
+++ b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
@@ -1,4 +1,5 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { Doc } from '../../../../../fields/Doc';
 
 export interface CursorData {
     x: number;
@@ -21,7 +22,7 @@ export interface ImageEditTool {
     name: string;
     btnText: string;
     icon: IconProp;
-    applyFunc: () => Promise<void>;
+    applyFunc: (currCutType: BrushMode, brushWidth: number, prevEdits: { url: string; saveRes: Doc | undefined }[]) => Promise<void>;
     sliderMin?: number;
     sliderMax?: number;
     sliderDefault?: number;
-- 
cgit v1.2.3-70-g09d2


From 4dbfe98a13b441a18b37c6bdcc6b62dab3b05fee Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Wed, 30 Oct 2024 00:27:20 -0400
Subject: bug fixes to cut modes and rendering

---
 src/client/views/nodes/imageEditor/ImageEditor.tsx     | 18 ++++++++++--------
 .../imageEditorUtils/imageEditorInterfaces.ts          |  2 +-
 2 files changed, 11 insertions(+), 9 deletions(-)

(limited to 'src/client/views/nodes/imageEditor/ImageEditor.tsx')

diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index 980f3e566..e6d651ee9 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -57,7 +57,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
     // format: array of [image source, corresponding image Doc]
     const [edits, setEdits] = useState<{ url: string; saveRes: Doc | undefined }[]>([]);
     const [edited, setEdited] = useState(false);
-    const isFirstDoc = useRef(true);
+    const [isFirstDoc, setIsFirstDoc] = useState<boolean>(true);
     const [input, setInput] = useState('');
     const [loading, setLoading] = useState(false);
     const [canvasDims, setCanvasDims] = useState<ImageDimensions>({
@@ -355,7 +355,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         setLoading(false);
     };
 
-    const cutImage = async (currCutType: BrushMode, brushWidth: number, prevEdits: { url: string; saveRes: Doc | undefined }[]) => {
+    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;
@@ -366,13 +366,14 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         const canvasOriginalImg = ImageUtility.getCanvasImg(img);
         if (!canvasOriginalImg) return;
         // 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);
@@ -393,7 +394,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                     break;
                 case BrushMode.LINE_IN:
                     ctx.globalCompositeOperation = 'destination-in';
-                    ctx.lineWidth = brushWidth + 20;
+                    ctx.lineWidth = brushWidth + 10; // added offset because width gets cut off a little bit
                     ctx.stroke();
                     break;
             }
@@ -426,16 +427,16 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
             let finalImg: HTMLImageElement | undefined = image;
             let finalImgURL: string = url;
             if (currCutType == BrushMode.IN) {
-                const croppedData = cropImage(image, minX, maxY, minY, maxY);
+                const croppedData = cropImage(image, minX, maxX, minY, maxY);
                 finalImg = croppedData;
                 finalImgURL = croppedData.src;
             }
             currImg.current = finalImg;
-            const newImgDoc = await createNewImgDoc(finalImg, isFirstDoc.current);
+            const newImgDoc = await createNewImgDoc(finalImg, firstDoc);
             if (newImgDoc) {
                 const docData = newImgDoc[DocData];
                 docData.backgroundColor = 'transparent';
-                if (isFirstDoc.current) isFirstDoc.current = false;
+                if (firstDoc) setIsFirstDoc(false);
                 setEdits([...prevEdits, { url: finalImgURL, saveRes: undefined }]);
             }
             setLoading(false);
@@ -557,6 +558,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
             DocumentView.addViewRenderedCb(newCollectionRef.current, dv => (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce());
         }
         setEdits([]);
+        setIsFirstDoc(true);
     };
 
     // defines the tools and sets current tool
@@ -587,7 +589,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                         labelPlacement="end"
                         sx={{ whiteSpace: 'nowrap' }}
                     />
-                    <ApplyFuncButtons onClick={() => currTool.applyFunc(cutType, cursorData.width, edits)} loading={loading} onReset={handleReset} btnText={currTool.btnText} />
+                    <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>
diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
index 660af7c42..75659fc53 100644
--- a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
+++ b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
@@ -22,7 +22,7 @@ export interface ImageEditTool {
     name: string;
     btnText: string;
     icon: IconProp;
-    applyFunc: (currCutType: BrushMode, brushWidth: number, prevEdits: { url: string; saveRes: Doc | undefined }[]) => Promise<void>;
+    applyFunc: (currCutType: BrushMode, brushWidth: number, prevEdits: { url: string; saveRes: Doc | undefined }[], isFirstDoc: boolean) => Promise<void>;
     sliderMin?: number;
     sliderMax?: number;
     sliderDefault?: number;
-- 
cgit v1.2.3-70-g09d2


From b5944e87f9d4f3149161de4de0d76db486461c76 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Wed, 30 Oct 2024 19:38:18 -0400
Subject: integrated image stickers with palette

---
 src/client/documents/Documents.ts                  |   2 +-
 src/client/util/CurrentUserUtils.ts                |   4 +-
 src/client/views/DocumentDecorations.tsx           |   4 +-
 src/client/views/LightboxView.tsx                  |   8 +-
 src/client/views/Main.tsx                          |   4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   8 +-
 src/client/views/nodes/ImageBox.tsx                |   8 +-
 src/client/views/nodes/imageEditor/ImageEditor.tsx |   2 +-
 src/client/views/smartdraw/AnnotationPalette.scss  |  56 ----
 src/client/views/smartdraw/AnnotationPalette.tsx   | 361 --------------------
 src/client/views/smartdraw/SmartDrawHandler.tsx    |   2 +-
 src/client/views/smartdraw/StickerPalette.scss     |  56 ++++
 src/client/views/smartdraw/StickerPalette.tsx      | 362 +++++++++++++++++++++
 src/fields/Doc.ts                                  |   2 +-
 14 files changed, 443 insertions(+), 436 deletions(-)
 delete mode 100644 src/client/views/smartdraw/AnnotationPalette.scss
 delete mode 100644 src/client/views/smartdraw/AnnotationPalette.tsx
 create mode 100644 src/client/views/smartdraw/StickerPalette.scss
 create mode 100644 src/client/views/smartdraw/StickerPalette.tsx

(limited to 'src/client/views/nodes/imageEditor/ImageEditor.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 5f2a592ae..b79e8930b 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1044,7 +1044,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 274bc79be..bb3d232fb 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -160,9 +160,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 62f2de776..24ebca751 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 a543b4875..f55ed2aa0 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.Pen, '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/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d2bc8f2c2..8d73601d1 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -42,7 +42,7 @@ import { FocusViewOptions } from '../../nodes/FocusViewOptions';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { OpenWhere, OpenWhereMod } 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';
@@ -1992,9 +1992,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 0dfc0ec28..f98ff6d6e 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' });
         }
     };
@@ -338,7 +344,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     @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('icon-hi')) 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);
diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index e6d651ee9..9642dcc31 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -435,7 +435,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
             const newImgDoc = await createNewImgDoc(finalImg, firstDoc);
             if (newImgDoc) {
                 const docData = newImgDoc[DocData];
-                docData.backgroundColor = 'transparent';
+                // docData.backgroundColor = 'transparent';
                 if (firstDoc) setIsFirstDoc(false);
                 setEdits([...prevEdits, { url: finalImgURL, saveRes: undefined }]);
             }
diff --git a/src/client/views/smartdraw/AnnotationPalette.scss b/src/client/views/smartdraw/AnnotationPalette.scss
deleted file mode 100644
index 4f11e8afc..000000000
--- a/src/client/views/smartdraw/AnnotationPalette.scss
+++ /dev/null
@@ -1,56 +0,0 @@
-.annotation-palette {
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    position: absolute;
-    right: 14px;
-    top: 50px;
-    border-radius: 5px;
-    margin: auto;
-}
-
-.palette-create {
-    display: flex;
-    flex-direction: row;
-    width: 170px;
-
-    .palette-create-input {
-        color: black;
-        width: 170px;
-    }
-}
-
-.palette-create-options {
-    display: flex;
-    flex-direction: row;
-    justify-content: space-between;
-    width: 170px;
-    margin-top: 5px;
-
-    .palette-color {
-        display: flex;
-        flex-direction: column;
-        align-items: center;
-        width: 40px;
-    }
-
-    .palette-detail,
-    .palette-size {
-        display: flex;
-        flex-direction: column;
-        align-items: center;
-        width: 60px;
-    }
-}
-
-.palette-buttons {
-    width: 100%;
-    display: flex;
-    flex-direction: row;
-    justify-content: space-between;
-}
-
-.palette-save-reset {
-    display: flex;
-    flex-direction: row;
-}
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
deleted file mode 100644
index dc0c01d36..000000000
--- a/src/client/views/smartdraw/AnnotationPalette.tsx
+++ /dev/null
@@ -1,361 +0,0 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Slider, Switch } from '@mui/material';
-import { Button } from 'browndash-components';
-import { action, makeObservable, observable } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import { AiOutlineSend } from 'react-icons/ai';
-import ReactLoading from 'react-loading';
-import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils';
-import { emptyFunction } from '../../../Utils';
-import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc';
-import { DocData } from '../../../fields/DocSymbols';
-import { ImageCast, NumCast } from '../../../fields/Types';
-import { ImageField } from '../../../fields/URLField';
-import { DocumentType } from '../../documents/DocumentTypes';
-import { Docs } from '../../documents/Documents';
-import { makeUserTemplateButtonOrImage } from '../../util/DropConverter';
-import { SettingsManager } from '../../util/SettingsManager';
-import { Transform } from '../../util/Transform';
-import { undoBatch } from '../../util/UndoManager';
-import { ObservableReactComponent } from '../ObservableReactComponent';
-import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider';
-import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
-import { FieldView } from '../nodes/FieldView';
-import './AnnotationPalette.scss';
-import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler';
-
-interface AnnotationPaletteProps {
-    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.
- *
- * On the "add" side of the palette, there is a way to create a drawing annotation 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.
- */
-@observer
-export class AnnotationPalette extends ObservableReactComponent<AnnotationPaletteProps> {
-    @observable private _paletteMode: 'create' | 'view' = 'view';
-    @observable private _userInput: string = '';
-    @observable private _isLoading: boolean = false;
-    @observable private _canInteract: boolean = true;
-    @observable private _showRegenerate: boolean = false;
-    @observable private _docView: DocumentView | null = null;
-    @observable private _docCarouselView: DocumentView | null = null;
-    @observable private _opts: DrawingOptions = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 };
-    private _gptRes: string[] = [];
-
-    constructor(props: AnnotationPaletteProps) {
-        super(props);
-        makeObservable(this);
-    }
-
-    public static LayoutString(fieldKey: string) {
-        return FieldView.LayoutString(AnnotationPalette, fieldKey);
-    }
-
-    Contains = (view: DocumentView) => {
-        return (this._docView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docView)) || (this._docCarouselView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docCarouselView));
-    };
-
-    return170 = () => 170;
-
-    @action
-    handleKeyPress = async (event: React.KeyboardEvent) => {
-        if (event.key === 'Enter') {
-            await this.generateDrawings();
-        }
-    };
-
-    @action
-    setPaletteMode = (mode: 'create' | 'view') => {
-        this._paletteMode = mode;
-    };
-
-    @action
-    setUserInput = (input: string) => {
-        if (!this._isLoading) this._userInput = input;
-    };
-
-    @action
-    setDetail = (detail: number) => {
-        if (this._canInteract) this._opts.complexity = detail;
-    };
-
-    @action
-    setColor = (autoColor: boolean) => {
-        if (this._canInteract) this._opts.autoColor = autoColor;
-    };
-
-    @action
-    setSize = (size: number) => {
-        if (this._canInteract) this._opts.size = size;
-    };
-
-    @action
-    resetPalette = (changePaletteMode: boolean) => {
-        if (changePaletteMode) this.setPaletteMode('view');
-        this.setUserInput('');
-        this.setDetail(5);
-        this.setColor(true);
-        this.setSize(200);
-        this._showRegenerate = false;
-        this._canInteract = true;
-        this._opts = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 };
-        this._gptRes = [];
-        this._props.Document[DocData].data = undefined;
-    };
-
-    /**
-     * 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.
-     */
-    public static addToPalette = async (doc: Doc) => {
-        if (!doc.savedAsAnno) {
-            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;
-        }
-    };
-
-    public static getIcon(group: Doc) {
-        const docView = DocumentView.getDocumentView(group);
-        if (docView) {
-            docView.ComponentView?.updateIcon?.(true);
-            return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
-        }
-        return undefined;
-    }
-
-    /**
-     * Calls the draw with GPT functions in SmartDrawHandler to allow users to generate drawings straight from
-     * the annotation palette.
-     */
-    @undoBatch
-    generateDrawings = action(async () => {
-        this._isLoading = true;
-        this._props.Document[DocData].data = undefined;
-        for (let i = 0; i < 3; i++) {
-            try {
-                SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
-                this._canInteract = false;
-                if (this._showRegenerate) {
-                    await SmartDrawHandler.Instance.regenerate(this._opts, this._gptRes[i], this._userInput);
-                } else {
-                    await SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity, this._opts.size, this._opts.autoColor);
-                }
-            } catch (e) {
-                console.log('Error generating drawing', e);
-            }
-        }
-        this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput);
-        this._userInput = '';
-        this._isLoading = false;
-        this._showRegenerate = true;
-    });
-
-    @action
-    addDrawing = (drawing: Doc, opts: DrawingOptions, gptRes: string) => {
-        this._gptRes.push(gptRes);
-        drawing[DocData].freeform_fitContentsToBox = true;
-        Doc.AddDocToList(this._props.Document, 'data', drawing);
-    };
-
-    /**
-     * Saves the currently showing, newly generated drawing to the annotation 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.
-     */
-    saveDrawing = async () => {
-        const cIndex = NumCast(this._props.Document.carousel_index);
-        const focusedDrawing = DocListCast(this._props.Document.data)[cIndex];
-        const docData = focusedDrawing[DocData];
-        docData.title = this._opts.text.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text;
-        docData.drawingInput = this._opts.text;
-        docData.drawingComplexity = this._opts.complexity;
-        docData.drawingColored = this._opts.autoColor;
-        docData.drawingSize = this._opts.size;
-        docData.drawingData = this._gptRes[cIndex];
-        focusedDrawing.width = this._opts.size;
-        docData.x = this._opts.x;
-        docData.y = this._opts.y;
-        await AnnotationPalette.addToPalette(focusedDrawing);
-        this.resetPalette(true);
-    };
-
-    render() {
-        return (
-            <div className="annotation-palette" style={{ zIndex: 1000 }} onClick={e => e.stopPropagation()}>
-                {this._paletteMode === 'view' && (
-                    <>
-                        <DocumentView
-                            ref={r => (this._docView = r)}
-                            Document={Doc.MyAnnos}
-                            addDocument={undefined}
-                            addDocTab={DocumentViewInternal.addDocTabFunc}
-                            pinToPres={DocumentView.PinDoc}
-                            containerViewPath={returnEmptyDocViewList}
-                            styleProvider={DefaultStyleProvider}
-                            removeDocument={returnFalse}
-                            ScreenToLocalTransform={Transform.Identity}
-                            PanelWidth={this.return170}
-                            PanelHeight={this.return170}
-                            renderDepth={0}
-                            isContentActive={returnTrue}
-                            focus={emptyFunction}
-                            whenChildContentsActiveChanged={emptyFunction}
-                            childFilters={returnEmptyFilter}
-                            childFiltersByRanges={returnEmptyFilter}
-                            searchFilterDocs={returnEmptyDoclist}
-                        />
-                        <Button text="Add" icon={<FontAwesomeIcon icon="square-plus" />} color={SettingsManager.userColor} onClick={() => this.setPaletteMode('create')} />
-                    </>
-                )}
-                {this._paletteMode === 'create' && (
-                    <>
-                        <div className="palette-create">
-                            <input
-                                className="palette-create-input"
-                                aria-label="label-input"
-                                id="new-label"
-                                type="text"
-                                value={this._userInput}
-                                onChange={e => {
-                                    this.setUserInput(e.target.value);
-                                }}
-                                placeholder={this._showRegenerate ? '(Optional) Enter edits' : 'Enter item to draw'}
-                                onKeyDown={this.handleKeyPress}
-                            />
-                            <Button
-                                style={{ alignSelf: 'flex-end' }}
-                                tooltip={this._showRegenerate ? 'Regenerate' : 'Send'}
-                                icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : this._showRegenerate ? <FontAwesomeIcon icon={'rotate'} /> : <AiOutlineSend />}
-                                iconPlacement="right"
-                                color={SettingsManager.userColor}
-                                onClick={this.generateDrawings}
-                            />
-                        </div>
-                        <div className="palette-create-options">
-                            <div className="palette-color">
-                                Color
-                                <Switch
-                                    sx={{
-                                        '& .MuiSwitch-switchBase.Mui-checked': {
-                                            color: SettingsManager.userColor,
-                                        },
-                                        '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
-                                            backgroundColor: SettingsManager.userVariantColor,
-                                        },
-                                    }}
-                                    defaultChecked={true}
-                                    value={this._opts.autoColor}
-                                    size="small"
-                                    onChange={() => this.setColor(!this._opts.autoColor)}
-                                />
-                            </div>
-                            <div className="palette-detail">
-                                Detail
-                                <Slider
-                                    sx={{
-                                        '& .MuiSlider-thumb': {
-                                            color: SettingsManager.userColor,
-                                            '&.Mui-focusVisible, &:hover, &.Mui-active': {
-                                                boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10`,
-                                            },
-                                        },
-                                        '& .MuiSlider-track': {
-                                            color: SettingsManager.userVariantColor,
-                                        },
-                                        '& .MuiSlider-rail': {
-                                            color: SettingsManager.userColor,
-                                        },
-                                    }}
-                                    style={{ width: '80%' }}
-                                    min={1}
-                                    max={10}
-                                    step={1}
-                                    size="small"
-                                    value={this._opts.complexity}
-                                    onChange={(e, val) => {
-                                        this.setDetail(val as number);
-                                    }}
-                                    valueLabelDisplay="auto"
-                                />
-                            </div>
-                            <div className="palette-size">
-                                Size
-                                <Slider
-                                    sx={{
-                                        '& .MuiSlider-thumb': {
-                                            color: SettingsManager.userColor,
-                                            '&.Mui-focusVisible, &:hover, &.Mui-active': {
-                                                boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20`,
-                                            },
-                                        },
-                                        '& .MuiSlider-track': {
-                                            color: SettingsManager.userVariantColor,
-                                        },
-                                        '& .MuiSlider-rail': {
-                                            color: SettingsManager.userColor,
-                                        },
-                                    }}
-                                    style={{ width: '80%' }}
-                                    min={50}
-                                    max={500}
-                                    step={10}
-                                    size="small"
-                                    value={this._opts.size}
-                                    onChange={(e, val) => {
-                                        this.setSize(val as number);
-                                    }}
-                                    valueLabelDisplay="auto"
-                                />
-                            </div>
-                        </div>
-                        <DocumentView
-                            ref={r => (this._docCarouselView = r)}
-                            Document={this._props.Document}
-                            addDocument={undefined}
-                            addDocTab={DocumentViewInternal.addDocTabFunc}
-                            pinToPres={DocumentView.PinDoc}
-                            containerViewPath={returnEmptyDocViewList}
-                            styleProvider={DefaultStyleProvider}
-                            removeDocument={returnFalse}
-                            ScreenToLocalTransform={Transform.Identity}
-                            PanelWidth={this.return170}
-                            PanelHeight={this.return170}
-                            renderDepth={1}
-                            isContentActive={returnTrue}
-                            focus={emptyFunction}
-                            whenChildContentsActiveChanged={emptyFunction}
-                            childFilters={returnEmptyFilter}
-                            childFiltersByRanges={returnEmptyFilter}
-                            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)} />
-                            <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)} />
-                            </div>
-                        </div>
-                    </>
-                )}
-            </div>
-        );
-    }
-}
-
-Docs.Prototypes.TemplateMap.set(DocumentType.ANNOPALETTE, {
-    layout: { view: AnnotationPalette, dataField: 'data' },
-    options: { acl: '' },
-});
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index b4635673c..0078dfc76 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/StickerPalette.scss b/src/client/views/smartdraw/StickerPalette.scss
new file mode 100644
index 000000000..ca99410cf
--- /dev/null
+++ b/src/client/views/smartdraw/StickerPalette.scss
@@ -0,0 +1,56 @@
+.sticker-palette {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    position: absolute;
+    right: 14px;
+    top: 50px;
+    border-radius: 5px;
+    margin: auto;
+}
+
+.palette-create {
+    display: flex;
+    flex-direction: row;
+    width: 170px;
+
+    .palette-create-input {
+        color: black;
+        width: 170px;
+    }
+}
+
+.palette-create-options {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    width: 170px;
+    margin-top: 5px;
+
+    .palette-color {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        width: 40px;
+    }
+
+    .palette-detail,
+    .palette-size {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        width: 60px;
+    }
+}
+
+.palette-buttons {
+    width: 100%;
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+}
+
+.palette-save-reset {
+    display: flex;
+    flex-direction: row;
+}
diff --git a/src/client/views/smartdraw/StickerPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx
new file mode 100644
index 000000000..468d0fd13
--- /dev/null
+++ b/src/client/views/smartdraw/StickerPalette.tsx
@@ -0,0 +1,362 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Slider, Switch } from '@mui/material';
+import { Button } from 'browndash-components';
+import { action, makeObservable, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { AiOutlineSend } from 'react-icons/ai';
+import ReactLoading from 'react-loading';
+import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
+import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
+import { ImageCast, NumCast } from '../../../fields/Types';
+import { ImageField } from '../../../fields/URLField';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
+import { makeUserTemplateButtonOrImage } from '../../util/DropConverter';
+import { SettingsManager } from '../../util/SettingsManager';
+import { Transform } from '../../util/Transform';
+import { undoBatch } from '../../util/UndoManager';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider';
+import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
+import { FieldView } from '../nodes/FieldView';
+import './StickerPalette.scss';
+import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler';
+
+interface StickerPaletteProps {
+    Document: Doc;
+}
+
+/**
+ * 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 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 stickers.
+ */
+@observer
+export class StickerPalette extends ObservableReactComponent<StickerPaletteProps> {
+    @observable private _paletteMode: 'create' | 'view' = 'view';
+    @observable private _userInput: string = '';
+    @observable private _isLoading: boolean = false;
+    @observable private _canInteract: boolean = true;
+    @observable private _showRegenerate: boolean = false;
+    @observable private _docView: DocumentView | null = null;
+    @observable private _docCarouselView: DocumentView | null = null;
+    @observable private _opts: DrawingOptions = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 };
+    private _gptRes: string[] = [];
+
+    constructor(props: StickerPaletteProps) {
+        super(props);
+        makeObservable(this);
+    }
+
+    public static LayoutString(fieldKey: string) {
+        return FieldView.LayoutString(StickerPalette, fieldKey);
+    }
+
+    Contains = (view: DocumentView) => {
+        return (this._docView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docView)) || (this._docCarouselView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docCarouselView));
+    };
+
+    return170 = () => 170;
+
+    @action
+    handleKeyPress = async (event: React.KeyboardEvent) => {
+        if (event.key === 'Enter') {
+            await this.generateDrawings();
+        }
+    };
+
+    @action
+    setPaletteMode = (mode: 'create' | 'view') => {
+        this._paletteMode = mode;
+    };
+
+    @action
+    setUserInput = (input: string) => {
+        if (!this._isLoading) this._userInput = input;
+    };
+
+    @action
+    setDetail = (detail: number) => {
+        if (this._canInteract) this._opts.complexity = detail;
+    };
+
+    @action
+    setColor = (autoColor: boolean) => {
+        if (this._canInteract) this._opts.autoColor = autoColor;
+    };
+
+    @action
+    setSize = (size: number) => {
+        if (this._canInteract) this._opts.size = size;
+    };
+
+    @action
+    resetPalette = (changePaletteMode: boolean) => {
+        if (changePaletteMode) this.setPaletteMode('view');
+        this.setUserInput('');
+        this.setDetail(5);
+        this.setColor(true);
+        this.setSize(200);
+        this._showRegenerate = false;
+        this._canInteract = true;
+        this._opts = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 };
+        this._gptRes = [];
+        this._props.Document[DocData].data = undefined;
+    };
+
+    /**
+     * 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.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.MyStickers, 'data', makeUserTemplateButtonOrImage(clone, image));
+            doc.savedAsSticker = true;
+        }
+    };
+
+    public static getIcon(group: Doc) {
+        const docView = DocumentView.getDocumentView(group);
+        if (docView) {
+            docView.ComponentView?.updateIcon?.(true);
+            return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
+        }
+        return undefined;
+    }
+
+    /**
+     * Calls the draw with GPT functions in SmartDrawHandler to allow users to generate drawings straight from
+     * the sticker palette.
+     */
+    @undoBatch
+    generateDrawings = action(async () => {
+        this._isLoading = true;
+        this._props.Document[DocData].data = undefined;
+        for (let i = 0; i < 3; i++) {
+            try {
+                SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+                this._canInteract = false;
+                if (this._showRegenerate) {
+                    await SmartDrawHandler.Instance.regenerate(this._opts, this._gptRes[i], this._userInput);
+                } else {
+                    await SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity, this._opts.size, this._opts.autoColor);
+                }
+            } catch (e) {
+                console.log('Error generating drawing', e);
+            }
+        }
+        this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput);
+        this._userInput = '';
+        this._isLoading = false;
+        this._showRegenerate = true;
+    });
+
+    @action
+    addDrawing = (drawing: Doc, opts: DrawingOptions, gptRes: string) => {
+        this._gptRes.push(gptRes);
+        drawing[DocData].freeform_fitContentsToBox = true;
+        Doc.AddDocToList(this._props.Document, 'data', drawing);
+    };
+
+    /**
+     * 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.
+     */
+    saveDrawing = async () => {
+        const cIndex = NumCast(this._props.Document.carousel_index);
+        const focusedDrawing = DocListCast(this._props.Document.data)[cIndex];
+        const docData = focusedDrawing[DocData];
+        docData.title = this._opts.text.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text;
+        docData.drawingInput = this._opts.text;
+        docData.drawingComplexity = this._opts.complexity;
+        docData.drawingColored = this._opts.autoColor;
+        docData.drawingSize = this._opts.size;
+        docData.drawingData = this._gptRes[cIndex];
+        focusedDrawing.width = this._opts.size;
+        docData.x = this._opts.x;
+        docData.y = this._opts.y;
+        await StickerPalette.addToPalette(focusedDrawing);
+        this.resetPalette(true);
+    };
+
+    render() {
+        return (
+            <div className="sticker-palette" style={{ zIndex: 1000 }} onClick={e => e.stopPropagation()}>
+                {this._paletteMode === 'view' && (
+                    <>
+                        <DocumentView
+                            ref={r => (this._docView = r)}
+                            Document={Doc.MyStickers}
+                            addDocument={undefined}
+                            addDocTab={DocumentViewInternal.addDocTabFunc}
+                            pinToPres={DocumentView.PinDoc}
+                            containerViewPath={returnEmptyDocViewList}
+                            styleProvider={DefaultStyleProvider}
+                            removeDocument={returnFalse}
+                            ScreenToLocalTransform={Transform.Identity}
+                            PanelWidth={this.return170}
+                            PanelHeight={this.return170}
+                            renderDepth={0}
+                            isContentActive={returnTrue}
+                            focus={emptyFunction}
+                            whenChildContentsActiveChanged={emptyFunction}
+                            childFilters={returnEmptyFilter}
+                            childFiltersByRanges={returnEmptyFilter}
+                            searchFilterDocs={returnEmptyDoclist}
+                        />
+                        <Button text="Add" icon={<FontAwesomeIcon icon="square-plus" />} color={SettingsManager.userColor} onClick={() => this.setPaletteMode('create')} />
+                    </>
+                )}
+                {this._paletteMode === 'create' && (
+                    <>
+                        <div className="palette-create">
+                            <input
+                                className="palette-create-input"
+                                aria-label="label-input"
+                                id="new-label"
+                                type="text"
+                                value={this._userInput}
+                                onChange={e => {
+                                    this.setUserInput(e.target.value);
+                                }}
+                                placeholder={this._showRegenerate ? '(Optional) Enter edits' : 'Enter item to draw'}
+                                onKeyDown={this.handleKeyPress}
+                            />
+                            <Button
+                                style={{ alignSelf: 'flex-end' }}
+                                tooltip={this._showRegenerate ? 'Regenerate' : 'Send'}
+                                icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : this._showRegenerate ? <FontAwesomeIcon icon={'rotate'} /> : <AiOutlineSend />}
+                                iconPlacement="right"
+                                color={SettingsManager.userColor}
+                                onClick={this.generateDrawings}
+                            />
+                        </div>
+                        <div className="palette-create-options">
+                            <div className="palette-color">
+                                Color
+                                <Switch
+                                    sx={{
+                                        '& .MuiSwitch-switchBase.Mui-checked': {
+                                            color: SettingsManager.userColor,
+                                        },
+                                        '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
+                                            backgroundColor: SettingsManager.userVariantColor,
+                                        },
+                                    }}
+                                    defaultChecked={true}
+                                    value={this._opts.autoColor}
+                                    size="small"
+                                    onChange={() => this.setColor(!this._opts.autoColor)}
+                                />
+                            </div>
+                            <div className="palette-detail">
+                                Detail
+                                <Slider
+                                    sx={{
+                                        '& .MuiSlider-thumb': {
+                                            color: SettingsManager.userColor,
+                                            '&.Mui-focusVisible, &:hover, &.Mui-active': {
+                                                boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10`,
+                                            },
+                                        },
+                                        '& .MuiSlider-track': {
+                                            color: SettingsManager.userVariantColor,
+                                        },
+                                        '& .MuiSlider-rail': {
+                                            color: SettingsManager.userColor,
+                                        },
+                                    }}
+                                    style={{ width: '80%' }}
+                                    min={1}
+                                    max={10}
+                                    step={1}
+                                    size="small"
+                                    value={this._opts.complexity}
+                                    onChange={(e, val) => {
+                                        this.setDetail(val as number);
+                                    }}
+                                    valueLabelDisplay="auto"
+                                />
+                            </div>
+                            <div className="palette-size">
+                                Size
+                                <Slider
+                                    sx={{
+                                        '& .MuiSlider-thumb': {
+                                            color: SettingsManager.userColor,
+                                            '&.Mui-focusVisible, &:hover, &.Mui-active': {
+                                                boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20`,
+                                            },
+                                        },
+                                        '& .MuiSlider-track': {
+                                            color: SettingsManager.userVariantColor,
+                                        },
+                                        '& .MuiSlider-rail': {
+                                            color: SettingsManager.userColor,
+                                        },
+                                    }}
+                                    style={{ width: '80%' }}
+                                    min={50}
+                                    max={500}
+                                    step={10}
+                                    size="small"
+                                    value={this._opts.size}
+                                    onChange={(e, val) => {
+                                        this.setSize(val as number);
+                                    }}
+                                    valueLabelDisplay="auto"
+                                />
+                            </div>
+                        </div>
+                        <DocumentView
+                            ref={r => (this._docCarouselView = r)}
+                            Document={this._props.Document}
+                            addDocument={undefined}
+                            addDocTab={DocumentViewInternal.addDocTabFunc}
+                            pinToPres={DocumentView.PinDoc}
+                            containerViewPath={returnEmptyDocViewList}
+                            styleProvider={DefaultStyleProvider}
+                            removeDocument={returnFalse}
+                            ScreenToLocalTransform={Transform.Identity}
+                            PanelWidth={this.return170}
+                            PanelHeight={this.return170}
+                            renderDepth={1}
+                            isContentActive={returnTrue}
+                            focus={emptyFunction}
+                            whenChildContentsActiveChanged={emptyFunction}
+                            childFilters={returnEmptyFilter}
+                            childFiltersByRanges={returnEmptyFilter}
+                            searchFilterDocs={returnEmptyDoclist}
+                        />
+                        <div className="palette-buttons">
+                            <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)} />
+                            </div>
+                        </div>
+                    </>
+                )}
+            </div>
+        );
+    }
+}
+
+Docs.Prototypes.TemplateMap.set(DocumentType.ANNOPALETTE, {
+    layout: { view: StickerPalette, dataField: 'data' },
+    options: { acl: '' },
+});
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 81241f9fe..c77c2a77d 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
-- 
cgit v1.2.3-70-g09d2


From f7bc3b892bbb3f559fa8bac1ebe9d2d747a5728c Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Wed, 30 Oct 2024 20:14:53 -0400
Subject: blending changes

---
 src/client/views/nodes/imageEditor/ImageEditor.tsx | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

(limited to 'src/client/views/nodes/imageEditor/ImageEditor.tsx')

diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index 9642dcc31..8f6a7e023 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -435,7 +435,8 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
             const newImgDoc = await createNewImgDoc(finalImg, firstDoc);
             if (newImgDoc) {
                 const docData = newImgDoc[DocData];
-                // docData.backgroundColor = 'transparent';
+                docData.backgroundColor = 'transparent';
+                docData.disableMixBlend = true;
                 if (firstDoc) setIsFirstDoc(false);
                 setEdits([...prevEdits, { url: finalImgURL, saveRes: undefined }]);
             }
-- 
cgit v1.2.3-70-g09d2


From 8847d6cf5a0eaeeddaefc71d0202247b4d2d096b Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Sun, 3 Nov 2024 16:32:28 -0500
Subject: working on smart draw width bugs

---
 src/client/util/CurrentUserUtils.ts                                    | 2 +-
 src/client/views/LightboxView.tsx                                      | 3 +--
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx    | 2 +-
 src/client/views/nodes/imageEditor/ImageEditor.tsx                     | 2 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx                        | 2 +-
 src/fields/InkField.ts                                                 | 2 +-
 6 files changed, 6 insertions(+), 7 deletions(-)

(limited to 'src/client/views/nodes/imageEditor/ImageEditor.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 009cd7de7..99faafe38 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -779,7 +779,7 @@ pie title Minerals in my tap water
             { title: " Size",      toolTip: "Size of area pencil eraser",   btnType: ButtonType.NumberSliderButton,               toolType: "eraserWidth",          ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"NotRadiusEraser()"}, numBtnMin: 1, linearBtnWidth:40},
             { title: "Mask",       toolTip: "Make Stroke a Stencil Mask",   btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask",                                 scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } },
             { title: "Labels",     toolTip: "Show Labels Inside Shapes",    btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels",                                  scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}},
-            { title: "Smart Draw", toolTip: "Draw with GPT",                btnType: ButtonType.ToggleButton, icon: "user-pen",   toolType: "smartdraw",                               scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'},  funcs: {hidden: "IsNoviceMode()"}},
+            { title: "Smart Draw", toolTip: "Draw with GPT",                btnType: ButtonType.ToggleButton, icon: "user-pen",   toolType: InkTool.SmartDraw,                               scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'},  funcs: {hidden: "IsNoviceMode()"}},
         ];
     }
 
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 65de85008..ce6b26323 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -35,7 +35,7 @@ type LightboxSavedState = { [key: string]: FieldResult; }; // prettier-ignore
 @observer
 export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
     /**
-     * Determines whether a DocumentView is descendant of the lightbox view (or any of its pop-ups like the annotationPalette)
+     * Determines whether a DocumentView is descendant of the lightbox view (or any of its pop-ups like the stickerPalette)
      * @param view
      * @returns true if a DocumentView is descendant of the lightbox view
      */
@@ -211,7 +211,6 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
     };
     togglePalette = () => {
         this._showPalette = !this._showPalette;
-        // if (this._showPalette === false) AnnotationPalette.Instance.resetPalette(true);
     };
     togglePen = () => {
         Doc.ActiveTool = Doc.ActiveTool === InkTool.Ink ? InkTool.None : InkTool.Ink;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 65b2702c6..4f62b6fbb 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1285,7 +1285,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     addDrawing = (doc: Doc, opts: DrawingOptions, gptRes: string) => {
         const docData = doc[DocData];
         docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
-        docData.width = opts.size;
+        docData._width = opts.size;
         docData.drawingInput = opts.text;
         docData.drawingComplexity = opts.complexity;
         docData.drawingColored = opts.autoColor;
diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index 8f6a7e023..c8fe5adc3 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -426,7 +426,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         image.onload = async () => {
             let finalImg: HTMLImageElement | undefined = image;
             let finalImgURL: string = url;
-            if (currCutType == BrushMode.IN) {
+            if (currCutType == BrushMode.IN || currCutType == BrushMode.LINE_IN) {
                 const croppedData = cropImage(image, minX, maxX, minY, maxY);
                 finalImg = croppedData;
                 finalImgURL = croppedData.src;
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 4c5be047b..83360bb6e 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -82,7 +82,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
+        CollectionFreeForm, FormattedTextBox, StickerPalette) to define how a drawing document should be added
         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;
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index f3ff5f11f..702301b31 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -11,7 +11,7 @@ export enum InkTool {
     None = 'None',
     Ink = 'Ink',
     Eraser = 'Eraser', // not a real tool, but a class of tools
-    SmartDraw = 'Smartdraw',
+    SmartDraw = 'smartdraw',
 }
 
 export enum InkInkTool {
-- 
cgit v1.2.3-70-g09d2


From 0fbe7696fa23cd0621d4053b430cec01d72f6801 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Mon, 25 Nov 2024 22:54:11 -0500
Subject: bug fixes to image cropping

---
 src/client/views/nodes/imageEditor/ImageEditor.tsx | 20 ++++++--------------
 1 file changed, 6 insertions(+), 14 deletions(-)

(limited to 'src/client/views/nodes/imageEditor/ImageEditor.tsx')

diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index 5c7e09645..5c4c83eef 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -451,21 +451,13 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         const croppedCanvas = document.createElement('canvas');
         const croppedCtx = croppedCanvas.getContext('2d');
         if (!croppedCtx) return image;
-        croppedCanvas.width = maxX - minX;
-        croppedCanvas.height = maxY - minY;
+        const cropWidth = Math.abs(maxX - minX);
+        const cropHeight = Math.abs(maxY - minY);
+        croppedCanvas.width = cropWidth;
+        croppedCanvas.height = cropHeight;
         croppedCtx.globalCompositeOperation = 'source-over';
-        croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
-        croppedCtx.drawImage(
-            image,
-            minX,
-            minY,
-            maxX - minX,
-            maxY - minY, // Source image crop area
-            0,
-            0,
-            maxX - minX,
-            maxY - minY // Destination area on the canvas
-        );
+        croppedCtx.clearRect(0, 0, cropWidth, cropHeight);
+        croppedCtx.drawImage(image, minX, minY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
         const croppedURL = croppedCanvas.toDataURL();
         const croppedImage = new Image();
         croppedImage.src = croppedURL;
-- 
cgit v1.2.3-70-g09d2


From e6a4b7c05899ee6d6a821666b72023856081d5ca Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Fri, 13 Dec 2024 17:45:34 -0500
Subject: added comments, refactored duplicate code, and resolved lint errors

---
 src/client/views/nodes/imageEditor/ImageEditor.tsx | 137 +++++++++++----------
 .../imageEditorUtils/imageEditorInterfaces.ts      |  18 +--
 2 files changed, 82 insertions(+), 73 deletions(-)

(limited to 'src/client/views/nodes/imageEditor/ImageEditor.tsx')

diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index 5c4c83eef..a742673e0 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -22,11 +22,11 @@ import { ImageEditorData } from '../ImageBox';
 import { OpenWhereMod } from '../OpenWhere';
 import './ImageEditor.scss';
 import { ApplyFuncButtons, ImageToolButton } from './ImageEditorButtons';
-import { BrushHandler, BrushType } from './imageEditorUtils/BrushHandler';
+import { BrushHandler } 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 { activeColor, bgColor, brushWidthOffset, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './imageEditorUtils/imageEditorConstants';
+import { CutMode, CursorData, ImageDimensions, ImageEditTool, ImageToolType, Point } from './imageEditorUtils/imageEditorInterfaces';
 import { DocumentView } from '../DocumentView';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { ImageField } from '../../../../fields/URLField';
@@ -64,7 +64,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         width: canvasSize,
         height: canvasSize,
     });
-    const [cutType, setCutType] = useState<BrushMode>(BrushMode.IN);
+    const [cutType, setCutType] = useState<CutMode>(CutMode.IN);
     // whether to create a new collection or not
     const [isNewCollection, setIsNewCollection] = useState(true);
     // the current image in the main canvas
@@ -183,7 +183,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                 x: currPoint.x - e.movementX / canvasScale,
                 y: currPoint.y - e.movementY / canvasScale,
             };
-            const pts = BrushHandler.createBrushPathOverlay(lastPoint, currPoint, cursorData.width / 2 / canvasScale, ctx, eraserColor, BrushType.CUT);
+            const pts = BrushHandler.createBrushPathOverlay(lastPoint, currPoint, cursorData.width / 2 / canvasScale, ctx, eraserColor);
             cutPts.current.push(...pts);
         };
 
@@ -283,7 +283,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         }));
     };
 
-    // Get AI Edit
+    // Get AI Edit for Generative Fill
     const getEdit = async () => {
         const img = currImg.current;
         if (!img) return;
@@ -304,32 +304,14 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
 
             // create first image
             if (!newCollectionRef.current) {
-                if (!isNewCollection && imageRootDoc) {
-                    // if the parent hasn't been set yet
-                    if (!parentDoc.current) parentDoc.current = imageRootDoc;
-                } else {
-                    if (!(originalImg.current && imageRootDoc)) return;
-                    // create new collection and add it to the view
-                    newCollectionRef.current = Docs.Create.FreeformDocument([], {
-                        x: NumCast(imageRootDoc.x) + NumCast(imageRootDoc._width) + offsetX,
-                        y: NumCast(imageRootDoc.y),
-                        _width: newCollectionSize,
-                        _height: newCollectionSize,
-                        title: 'Image edit collection',
-                    });
-                    DocUtils.MakeLink(imageRootDoc, newCollectionRef.current, { link_relationship: 'Image Edit Version History' });
-
-                    // opening new tab
-                    CollectionDockingView.AddSplit(newCollectionRef.current, OpenWhereMod.right);
-
-                    // add the doc to the main freeform
-                    // eslint-disable-next-line no-use-before-define
-                    await createNewImgDoc(originalImg.current, true);
-                }
+                createNewCollection();
             } else {
                 childrenDocs.current = [];
             }
-
+            if (!(originalImg.current && imageRootDoc)) return;
+            // add the doc to the main freeform
+            // eslint-disable-next-line no-use-before-define
+            await createNewImgDoc(originalImg.current, true);
             originalImg.current = currImg.current;
             originalDoc.current = parentDoc.current;
             const { urls } = res as APISuccess;
@@ -356,23 +338,32 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         setLoading(false);
     };
 
-    const cutImage = async (currCutType: BrushMode, brushWidth: number, prevEdits: { url: string; saveRes: Doc | undefined }[], firstDoc: boolean) => {
+    /**
+     * This function performs image cutting based on the inputted BrushMode. There are currently four ways to cut images:
+     *      1. By outlining the area that should be kept (BrushMode.IN)
+     *      2. By outlining the area that should be removed (BrushMode.OUT)
+     *      3. By drawing in the area that should be kept (where the image is brushed, the image will remain and everything else will be removed) (BrushMode.DRAW_IN)
+     *      4. By drawing the area that she be removed, so this operates as an eraser (BrushMode.ERASE)
+     * @param currCutType BrushMode enum that determines what kind of cutting operation to perform
+     * @param firstDoc boolean for whether it's the first edited image. This is for positioning of the edited images when they render on the canvas.
+     */
+    const cutImage = async (currCutType: CutMode, brushWidth: number, prevEdits: { url: string; saveRes: Doc | undefined }[], firstDoc: boolean) => {
         const img = currImg.current;
         const canvas = canvasRef.current;
         if (!canvas || !img) return;
         const ctx = ImageUtility.getCanvasContext(canvasRef);
         if (!ctx) return;
-        setLoading(true);
         // get the original image
         const canvasOriginalImg = ImageUtility.getCanvasImg(img);
         if (!canvasOriginalImg) return;
-        // NOTE: cutting two diff shapes can be made possible by having the user press a button to set a new shape!
+        setLoading(true);
         const currPts = [...cutPts.current];
-        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)
+        if (currCutType !== CutMode.ERASE) 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;
+        // currPts is populated by the brush strokes' points, so this code is drawing a path along the points
         if (currPts.length) {
             ctx.beginPath();
             ctx.moveTo(currPts[0].x, currPts[0].y);
@@ -383,44 +374,30 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                 maxX = Math.max(currPts[i].x, maxX);
                 maxY = Math.max(currPts[i].y, maxY);
             }
-            switch (currCutType) {
-                case BrushMode.IN:
+            switch (
+                currCutType // use different canvas operations depending on the type of cutting we're applying
+            ) {
+                case CutMode.IN:
                     ctx.closePath();
                     ctx.globalCompositeOperation = 'destination-in';
                     ctx.fill();
                     break;
-                case BrushMode.OUT:
+                case CutMode.OUT:
                     ctx.closePath();
                     ctx.globalCompositeOperation = 'destination-out';
                     ctx.fill();
                     break;
-                case BrushMode.LINE_IN:
+                case CutMode.DRAW_IN:
                     ctx.globalCompositeOperation = 'destination-in';
-                    ctx.lineWidth = brushWidth + 10; // added offset because width gets cut off a little bit
+                    ctx.lineWidth = brushWidth + brushWidthOffset; // added offset because width gets cut off a little bit
                     ctx.stroke();
                     break;
             }
         }
 
-        const url = canvas.toDataURL(); // this does the same thing as convert img to canvasurl
+        const url = canvas.toDataURL();
         if (!newCollectionRef.current) {
-            if (!isNewCollection && imageRootDoc) {
-                // if the parent hasn't been set yet
-                if (!parentDoc.current) parentDoc.current = imageRootDoc;
-            } else {
-                if (!(originalImg.current && imageRootDoc)) return;
-                // create new collection and add it to the view
-                newCollectionRef.current = Docs.Create.FreeformDocument([], {
-                    x: NumCast(imageRootDoc.x) + NumCast(imageRootDoc._width) + offsetX,
-                    y: NumCast(imageRootDoc.y),
-                    _width: newCollectionSize,
-                    _height: newCollectionSize,
-                    title: 'Image edit collection',
-                });
-                DocUtils.MakeLink(imageRootDoc, newCollectionRef.current, { link_relationship: 'Image Edit Version History' });
-                // opening new tab
-                CollectionDockingView.AddSplit(newCollectionRef.current, OpenWhereMod.right);
-            }
+            createNewCollection();
         }
 
         const image = new Image();
@@ -428,7 +405,8 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         image.onload = async () => {
             let finalImg: HTMLImageElement | undefined = image;
             let finalImgURL: string = url;
-            if (currCutType == BrushMode.IN || currCutType == BrushMode.LINE_IN) {
+            // crop the image for these brush modes to remove excess blank space around the image contents
+            if (currCutType == CutMode.IN || currCutType == CutMode.DRAW_IN) {
                 const croppedData = cropImage(image, minX, maxX, minY, maxY);
                 finalImg = croppedData;
                 finalImgURL = croppedData.src;
@@ -436,6 +414,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
             currImg.current = finalImg;
             const newImgDoc = await createNewImgDoc(finalImg, firstDoc);
             if (newImgDoc) {
+                // set the image to transparent to remove the background / brushstrokes
                 const docData = newImgDoc[DocData];
                 docData.backgroundColor = 'transparent';
                 docData.disableMixBlend = true;
@@ -447,6 +426,34 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         };
     };
 
+    /**
+     * Creates a new collection to put the image edits on. Adds to a new tab on the right if "Create New Collection" is checked.
+     * @returns
+     */
+    const createNewCollection = () => {
+        if (!isNewCollection && imageRootDoc) {
+            // if the parent hasn't been set yet
+            if (!parentDoc.current) parentDoc.current = imageRootDoc;
+        } else {
+            if (!(originalImg.current && imageRootDoc)) return;
+            // create new collection and add it to the view
+            newCollectionRef.current = Docs.Create.FreeformDocument([], {
+                x: NumCast(imageRootDoc.x) + NumCast(imageRootDoc._width) + offsetX,
+                y: NumCast(imageRootDoc.y),
+                _width: newCollectionSize,
+                _height: newCollectionSize,
+                title: 'Image edit collection',
+            });
+            DocUtils.MakeLink(imageRootDoc, newCollectionRef.current, { link_relationship: 'Image Edit Version History' });
+            // opening new tab
+            CollectionDockingView.AddSplit(newCollectionRef.current, OpenWhereMod.right);
+        }
+    };
+
+    /**
+     * This function crops an image based on the inputted dimensions. This is used to automatically adjust the images that are
+     * edited to be smaller than the original (i.e. for cutting into a small part of the image)
+     */
     const cropImage = (image: HTMLImageElement, minX: number, maxX: number, minY: number, maxY: number) => {
         const croppedCanvas = document.createElement('canvas');
         const croppedCtx = croppedCanvas.getContext('2d');
@@ -617,36 +624,36 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                                 style={{ width: '100%' }}
                                 text="Keep in"
                                 type={Type.TERT}
-                                color={cutType == BrushMode.IN ? SettingsManager.userColor : bgColor}
+                                color={cutType == CutMode.IN ? SettingsManager.userColor : bgColor}
                                 onClick={() => {
-                                    setCutType(BrushMode.IN);
+                                    setCutType(CutMode.IN);
                                 }}
                             />
                             <Button
                                 style={{ width: '100%' }}
                                 text="Keep out"
                                 type={Type.TERT}
-                                color={cutType == BrushMode.OUT ? SettingsManager.userColor : bgColor}
+                                color={cutType == CutMode.OUT ? SettingsManager.userColor : bgColor}
                                 onClick={() => {
-                                    setCutType(BrushMode.OUT);
+                                    setCutType(CutMode.OUT);
                                 }}
                             />
                             <Button
                                 style={{ width: '100%' }}
                                 text="Draw in"
                                 type={Type.TERT}
-                                color={cutType == BrushMode.LINE_IN ? SettingsManager.userColor : bgColor}
+                                color={cutType == CutMode.DRAW_IN ? SettingsManager.userColor : bgColor}
                                 onClick={() => {
-                                    setCutType(BrushMode.LINE_IN);
+                                    setCutType(CutMode.DRAW_IN);
                                 }}
                             />
                             <Button
                                 style={{ width: '100%' }}
                                 text="Erase"
                                 type={Type.TERT}
-                                color={cutType == BrushMode.LINE_OUT ? SettingsManager.userColor : bgColor}
+                                color={cutType == CutMode.ERASE ? SettingsManager.userColor : bgColor}
                                 onClick={() => {
-                                    setCutType(BrushMode.LINE_OUT);
+                                    setCutType(CutMode.ERASE);
                                 }}
                             />
                         </div>
diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
index 75659fc53..a14b55439 100644
--- a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
+++ b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts
@@ -17,24 +17,26 @@ export enum ImageToolType {
     Cut = 'cut',
 }
 
+export enum CutMode {
+    IN,
+    OUT,
+    DRAW_IN,
+    ERASE,
+}
+
 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>;
+    // this is the function that the image tool applies, so it can be defined depending on the tool
+    applyFunc: (currCutType: CutMode, brushWidth: number, prevEdits: { url: string; saveRes: Doc | undefined }[], isFirstDoc: boolean) => Promise<void>;
+    // these optional parameters are here because different tools require different brush sizes and defaults
     sliderMin?: number;
     sliderMax?: number;
     sliderDefault?: number;
 }
 
-export enum BrushMode {
-    IN,
-    OUT,
-    LINE_IN,
-    LINE_OUT,
-}
-
 export interface ImageDimensions {
     width: number;
     height: number;
-- 
cgit v1.2.3-70-g09d2


From e7162baa77a851d73037c63b75fac6a5c07f9b61 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Sat, 14 Dec 2024 14:13:59 -0500
Subject: replaced await in loop, added a componentWillUnmount to reset sticker
 palette

---
 src/client/views/nodes/imageEditor/ImageEditor.tsx | 10 ++-
 .../imageEditor/imageEditorUtils/BrushHandler.ts   |  8 +-
 .../imageEditorUtils/imageEditorConstants.ts       |  1 +
 src/client/views/smartdraw/DrawingFillHandler.tsx  | 96 +++++++++++-----------
 src/client/views/smartdraw/StickerPalette.tsx      | 26 +++---
 5 files changed, 70 insertions(+), 71 deletions(-)

(limited to 'src/client/views/nodes/imageEditor/ImageEditor.tsx')

diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index a742673e0..a39878924 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -28,9 +28,6 @@ import { PointerHandler } from './imageEditorUtils/PointerHandler';
 import { activeColor, bgColor, brushWidthOffset, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './imageEditorUtils/imageEditorConstants';
 import { CutMode, 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';
 
@@ -43,6 +40,13 @@ interface GenerativeFillProps {
 
 // Added field on image doc: gen_fill_children: List of children Docs
 
+/**
+ * The image editor interface can be accessed by opening a document's context menu, then going to Options --> Open Image Editor.
+ * The image editor supports various operations on images. Currently, there is a Generative Fill feature that allows users to erase
+ * part of an image, add an optional prompt, and send this to GPT. GPT then returns a newly generated image that replaces the erased
+ * portion based on the optional prompt. There is also an image cutting tool that allows users to cut images in different ways to
+ * reshape the images, take out portions of images, and overall use them more creatively (see the header comment for cutImage() for more information).
+ */
 const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc }: GenerativeFillProps) => {
     const canvasRef = useRef<HTMLCanvasElement>(null);
     const canvasBackgroundRef = useRef<HTMLCanvasElement>(null);
diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/BrushHandler.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/BrushHandler.ts
index a9fe02d4f..ed39375e0 100644
--- a/src/client/views/nodes/imageEditor/imageEditorUtils/BrushHandler.ts
+++ b/src/client/views/nodes/imageEditor/imageEditorUtils/BrushHandler.ts
@@ -1,12 +1,6 @@
 import { GenerativeFillMathHelpers } from './GenerativeFillMathHelpers';
 import { eraserColor } from './imageEditorConstants';
 import { Point } from './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 */) => {
@@ -20,7 +14,7 @@ export class BrushHandler {
         ctx.closePath();
     };
 
-    static createBrushPathOverlay = (startPoint: Point, endPoint: Point, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string, brushType: BrushType) => {
+    static createBrushPathOverlay = (startPoint: Point, endPoint: Point, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string) => {
         const dist = GenerativeFillMathHelpers.distanceBetween(startPoint, endPoint);
         const pts: Point[] = [];
         for (let i = 0; i < dist; i += 5) {
diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorConstants.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorConstants.ts
index 4772304bc..594d6d9fc 100644
--- a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorConstants.ts
+++ b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorConstants.ts
@@ -3,6 +3,7 @@ export const freeformRenderSize = 300;
 export const offsetDistanceY = freeformRenderSize + 400;
 export const offsetX = 200;
 export const newCollectionSize = 500;
+export const brushWidthOffset = 10;
 
 export const activeColor = '#1976d2';
 export const eraserColor = '#e1e9ec';
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index c3c762181..8f71b150d 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -1,50 +1,50 @@
-import { action, makeObservable } from 'mobx';
-import { observer } from 'mobx-react';
-import React from 'react';
-import { Doc } from '../../../fields/Doc';
-import { ImageCast } from '../../../fields/Types';
-import { ImageField } from '../../../fields/URLField';
-import { Docs } from '../../documents/Documents';
-import { Networking } from '../../Network';
-import { makeUserTemplateButtonOrImage } from '../../util/DropConverter';
-import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
-import { ImageUtility } from '../nodes/imageEditor/imageEditorUtils/ImageHandler';
-import { OpenWhere } from '../nodes/OpenWhere';
-import { ObservableReactComponent } from '../ObservableReactComponent';
+// import { action, makeObservable } from 'mobx';
+// import { observer } from 'mobx-react';
+// import React from 'react';
+// import { Doc } from '../../../fields/Doc';
+// import { ImageCast } from '../../../fields/Types';
+// import { ImageField } from '../../../fields/URLField';
+// import { Docs } from '../../documents/Documents';
+// import { Networking } from '../../Network';
+// import { makeUserTemplateButtonOrImage } from '../../util/DropConverter';
+// import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
+// import { ImageUtility } from '../nodes/imageEditor/imageEditorUtils/ImageHandler';
+// import { OpenWhere } from '../nodes/OpenWhere';
+// import { ObservableReactComponent } from '../ObservableReactComponent';
 
-export class DrawingFillHandler {
-    static drawingToImage = async (drawing: Doc, prompt: string) => {
-        const imageField = await DocumentView.GetDocImage(drawing);
-        if (!imageField) return;
-        const { href } = ImageCast(imageField).url;
-        const hrefParts = href.split('.');
-        const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
-        try {
-            const response = await fetch(hrefComplete);
-            const blob: Blob = await response.blob();
-            const strength: number = 100;
-            const img = await Networking.PostToServer('/queryFireflyImage', { prompt, blob, strength });
-            DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight);
-            // Networking.PostToServer('/queryFireflyImage', { prompt, blob, strength }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight));
-        } catch (error) {
-            console.error('Error fetching image:', error);
-            return;
-        }
+// export class DrawingFillHandler {
+//     static drawingToImage = async (drawing: Doc, prompt: string) => {
+//         const imageField = await DocumentView.GetDocImage(drawing);
+//         if (!imageField) return;
+//         const { href } = ImageCast(imageField).url;
+//         const hrefParts = href.split('.');
+//         const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
+//         try {
+//             const response = await fetch(hrefComplete);
+//             const blob: Blob = await response.blob();
+//             const strength: number = 100;
+//             const img = await Networking.PostToServer('/queryFireflyImage', { prompt, blob, strength });
+//             DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight);
+//             // Networking.PostToServer('/queryFireflyImage', { prompt, blob, strength }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight));
+//         } catch (error) {
+//             console.error('Error fetching image:', error);
+//             return;
+//         }
 
-        // const image = new Image();
-        // image.src = imageField.url?.href;
-        // // image.onload = async () => {
-        //     const canvas = document.createElement('canvas');
-        //     canvas.width = image.width;
-        //     canvas.height = image.height;
-        //     const ctx = canvas.getContext('2d');
-        //     if (!ctx) return;
-        //     ctx.globalCompositeOperation = 'source-over';
-        //     ctx.clearRect(0, 0, image.width, image.height);
-        //     ctx.drawImage(image, 0, 0);
-        // const blob: Blob = await ImageUtility.canvasToBlob(canvas);
-        // const strength: number = 100;
-        // Networking.PostToServer('/queryFireflyImage', { prompt, blob, strength }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight));
-        // };
-    };
-}
+//         // const image = new Image();
+//         // image.src = imageField.url?.href;
+//         // // image.onload = async () => {
+//         //     const canvas = document.createElement('canvas');
+//         //     canvas.width = image.width;
+//         //     canvas.height = image.height;
+//         //     const ctx = canvas.getContext('2d');
+//         //     if (!ctx) return;
+//         //     ctx.globalCompositeOperation = 'source-over';
+//         //     ctx.clearRect(0, 0, image.width, image.height);
+//         //     ctx.drawImage(image, 0, 0);
+//         // const blob: Blob = await ImageUtility.canvasToBlob(canvas);
+//         // const strength: number = 100;
+//         // Networking.PostToServer('/queryFireflyImage', { prompt, blob, strength }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight));
+//         // };
+//     };
+// }
diff --git a/src/client/views/smartdraw/StickerPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx
index 468d0fd13..d56878f10 100644
--- a/src/client/views/smartdraw/StickerPalette.tsx
+++ b/src/client/views/smartdraw/StickerPalette.tsx
@@ -56,6 +56,10 @@ export class StickerPalette extends ObservableReactComponent<StickerPaletteProps
         makeObservable(this);
     }
 
+    componentWillUnmount() {
+        this.resetPalette(true);
+    }
+
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(StickerPalette, fieldKey);
     }
@@ -145,19 +149,15 @@ export class StickerPalette extends ObservableReactComponent<StickerPaletteProps
     generateDrawings = action(async () => {
         this._isLoading = true;
         this._props.Document[DocData].data = undefined;
-        for (let i = 0; i < 3; i++) {
-            try {
-                SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
-                this._canInteract = false;
-                if (this._showRegenerate) {
-                    await SmartDrawHandler.Instance.regenerate(this._opts, this._gptRes[i], this._userInput);
-                } else {
-                    await SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity, this._opts.size, this._opts.autoColor);
-                }
-            } catch (e) {
-                console.log('Error generating drawing', e);
-            }
-        }
+        SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+        this._canInteract = false;
+        await Promise.all(
+            Array.from({ length: 3 }).map((_, i) => {
+                return this._showRegenerate
+                    ? SmartDrawHandler.Instance.regenerate(this._opts, this._gptRes[i], this._userInput)
+                    : SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity, this._opts.size, this._opts.autoColor);
+            })
+        );
         this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput);
         this._userInput = '';
         this._isLoading = false;
-- 
cgit v1.2.3-70-g09d2


From 0eff48b757ca81860a883d25e147b8a869e5fe00 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Mon, 30 Dec 2024 23:35:24 -0500
Subject: created image regeneration with dialogue

---
 src/client/apis/gpt/GPT.ts                         |  39 +++-
 src/client/documents/Documents.ts                  |   4 +
 src/client/views/PropertiesView.scss               |   1 +
 src/client/views/PropertiesView.tsx                |  12 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  22 +--
 src/client/views/nodes/ImageBox.tsx                |  10 ++
 src/client/views/nodes/imageEditor/ImageEditor.tsx |   2 +-
 src/client/views/pdf/AnchorMenu.tsx                |   1 +
 src/client/views/smartdraw/DrawingFillHandler.tsx  |  11 +-
 src/client/views/smartdraw/SmartDrawHandler.scss   |   8 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    | 197 ++++++++++++++-------
 src/client/views/smartdraw/StickerPalette.tsx      |   1 +
 src/server/ApiManagers/FireflyManager.ts           |  20 ++-
 src/server/DashUploadUtils.ts                      |   1 -
 14 files changed, 238 insertions(+), 91 deletions(-)

(limited to 'src/client/views/nodes/imageEditor/ImageEditor.tsx')

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 03380e4d6..9241eb120 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -249,6 +249,41 @@ const gptHandwriting = async (src: string): Promise<string> => {
     }
 };
 
+const gptDescribeImage = async (image: string): Promise<string> => {
+    try {
+        const response = await openai.chat.completions.create({
+            model: 'gpt-4o',
+            temperature: 0,
+            messages: [
+                {
+                    role: 'user',
+                    content: [
+                        {
+                            type: 'text',
+                            text: `Identify what this drawing is, naming as many elements and their location in the drawing as possible`,
+                        },
+                        {
+                            type: 'image_url',
+                            image_url: {
+                                url: `${image}`,
+                                detail: 'low',
+                            },
+                        },
+                    ],
+                },
+            ],
+        });
+        if (response.choices[0].message.content) {
+            console.log('GPT DESCRIPTION', response.choices[0].message.content);
+            return response.choices[0].message.content;
+        }
+        return 'Unknown drawing';
+    } catch (err) {
+        console.log(err);
+        return 'Error connecting with API';
+    }
+};
+
 const gptDrawingColor = async (image: string, coords: string[]): Promise<string> => {
     try {
         const response = await openai.chat.completions.create({
@@ -276,11 +311,11 @@ const gptDrawingColor = async (image: string, coords: string[]): Promise<string>
         if (response.choices[0].message.content) {
             return response.choices[0].message.content;
         }
-        return 'Missing labels';
+        return 'Unknown drawing';
     } catch (err) {
         console.log(err);
         return 'Error connecting with API';
     }
 };
 
-export { gptAPICall, gptImageCall, GPTCallType, gptImageLabel, gptGetEmbedding, gptHandwriting, gptDrawingColor };
+export { gptAPICall, gptImageCall, GPTCallType, gptImageLabel, gptGetEmbedding, gptHandwriting, gptDescribeImage, gptDrawingColor };
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index c51c1645d..785af3409 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -516,6 +516,10 @@ export class DocumentOptions {
 
     card_sort?: STRt = new StrInfo('way cards are sorted in deck view');
     card_sort_isDesc?: BOOLt = new BoolInfo('whether the cards are sorted ascending or descending');
+
+    ai_generated?: boolean; // to mark items as ai generated
+    firefly_seed?: number;
+    firefly_prompt?: string;
 }
 
 export const DocOptions = new DocumentOptions();
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index 693c75ebf..7866e67e7 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -642,6 +642,7 @@
 
 .smooth,
 .color,
+.strength-slider,
 .smooth-slider {
     margin-top: 7px;
 }
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index aefdeee17..5b24eb7ea 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -982,6 +982,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
             10,
             1
         );
+        const strength = this.getNumber('Reference Strength', '', 1, 100, this.refStrength, (val: number) => {
+            !isNaN(val) && (this.refStrength = val);
+        });
         return (
             <div>
                 {!targetDoc.layout_isSvg && this.containsInkDoc && (
@@ -995,9 +998,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                                 align="flex-start"
                                 fillWidth
                                 toggleType={ToggleType.BUTTON}
-                                onClick={undoable(() => DrawingFillHandler.drawingToImage(targetDoc, 'fill in the details of this image'), 'createImage')}
+                                onClick={undoable(() => DrawingFillHandler.drawingToImage(targetDoc, this.refStrength, 'fill in the details of this image'), 'createImage')}
                             />
                         </div>
+                        <div className="strength-slider">{strength}</div>
                         <div className="color">
                             <Toggle
                                 text={'Color with GPT'}
@@ -1052,6 +1056,12 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
             doc[DocData].stroke_markerScale = Number(value);
         });
     }
+    @computed get refStrength() { return Number(this.getField('drawing_refStrength') || '50'); } // prettier-ignore
+    set refStrength(value) {
+        this.selectedStrokes.forEach(doc => {
+            doc[DocData].drawing_refStrength = Number(value);
+        });
+    }
     @computed get smoothAmt() { return Number(this.getField('stroke_smoothAmount') || '5'); } // prettier-ignore
     set smoothAmt(value) {
         this.selectedStrokes.forEach(doc => {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index acf72e5cb..4bccdd286 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1295,6 +1295,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         docData.drawingColored = opts.autoColor;
         docData.drawingSize = opts.size;
         docData.drawingData = gptRes;
+        docData.ai_generated = true;
         this._drawingContainer = doc;
         this.addDocument(doc);
         this._batch?.end();
@@ -1992,16 +1993,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 }),
                 icon: 'eye',
             });
-        optionItems.push({
-            description: 'Show Drawing Editor',
-            event: action(() => {
-                SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
-                SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
-                SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
-                !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10) : SmartDrawHandler.Instance.hideRegenerate();
-            }),
-            icon: 'pen-to-square',
-        });
+        this.layoutDoc.drawingData != undefined &&
+            optionItems.push({
+                description: 'Show Drawing Editor',
+                event: action(() => {
+                    SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
+                    SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+                    SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
+                    !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10) : SmartDrawHandler.Instance.hideRegenerate();
+                }),
+                icon: 'pen-to-square',
+            });
         optionItems.push({
             description: this.Document.savedAsSticker ? 'Sticker Saved!' : 'Save to Stickers',
             event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')),
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 25e7b566f..8f6a90e61 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -40,6 +40,7 @@ import { FocusViewOptions } from './FocusViewOptions';
 import './ImageBox.scss';
 import { OpenWhere } from './OpenWhere';
 import { Upload } from '../../../server/SharedMediaTypes';
+import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
 
 export class ImageEditorData {
     // eslint-disable-next-line no-use-before-define
@@ -351,6 +352,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 }),
                 icon: 'pencil-alt',
             });
+            this.layoutDoc.ai_generated &&
+                funcs.push({
+                    description: 'Regenerate AI Image',
+                    event: action(() => {
+                        console.log('COOOORDS', this.dataDoc.width as number, this.dataDoc.y as number);
+                        !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this.dataDoc.x as number, (this.dataDoc.y as number) - 10) : SmartDrawHandler.Instance.hideRegenerate();
+                    }),
+                    icon: 'pen-to-square',
+                });
             funcs.push({
                 description: this.Document.savedAsSticker ? 'Sticker Saved!' : 'Save to Stickers',
                 event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')),
diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index a39878924..2a8bc034d 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -411,7 +411,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
             let finalImgURL: string = url;
             // crop the image for these brush modes to remove excess blank space around the image contents
             if (currCutType == CutMode.IN || currCutType == CutMode.DRAW_IN) {
-                const croppedData = cropImage(image, minX, maxX, minY, maxY);
+                const croppedData = cropImage(image, Math.max(minX, 0), Math.min(maxX, image.width), Math.max(minY, 0), Math.min(maxY, image.height));
                 finalImg = croppedData;
                 finalImgURL = croppedData.src;
             }
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index fe03f32a5..bb8082061 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -158,6 +158,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
         docData.drawingColored = opts.autoColor;
         docData.drawingSize = opts.size;
         docData.drawingData = gptRes;
+        docData.ai_generated = true;
     });
 
     pointerDown = (e: React.PointerEvent) => {
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 48e71bc9f..1a470f995 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -1,21 +1,26 @@
+import { imageUrlToBase64 } from '../../../ClientUtils';
 import { Doc } from '../../../fields/Doc';
 import { ImageCast } from '../../../fields/Types';
 import { Upload } from '../../../server/SharedMediaTypes';
+import { gptDescribeImage } from '../../apis/gpt/GPT';
 import { Docs } from '../../documents/Documents';
 import { Networking } from '../../Network';
 import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { OpenWhere } from '../nodes/OpenWhere';
 
 export class DrawingFillHandler {
-    static drawingToImage = (drawing: Doc, prompt: string) =>
+    static drawingToImage = (drawing: Doc, strength: number, prompt: string) =>
         DocumentView.GetDocImage(drawing)?.then(imageField => {
             if (imageField) {
                 const { href } = ImageCast(imageField).url;
                 const hrefParts = href.split('.');
                 const structureUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`;
-                const strength: number = 100;
-                Networking.PostToServer('/queryFireflyImageFromStructure', { prompt, structureUrl, strength }).then((info: Upload.ImageInformation) => 
+                imageUrlToBase64(structureUrl)
+                    .then((hrefBase64: string) => gptDescribeImage(hrefBase64))
+                    .then((prompt: string) => {
+                        Networking.PostToServer('/queryFireflyImageFromStructure', { prompt, structureUrl, strength }).then((info: Upload.ImageInformation) => 
                                 DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {}), OpenWhere.addRight)) // prettier-ignore
+                    });
             }
             return false;
         });
diff --git a/src/client/views/smartdraw/SmartDrawHandler.scss b/src/client/views/smartdraw/SmartDrawHandler.scss
index c25273876..513779512 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.scss
+++ b/src/client/views/smartdraw/SmartDrawHandler.scss
@@ -12,7 +12,13 @@
         }
     }
 
-    .smartdraw-options {
+    .smartdraw-output-options {
+        display: flex;
+        flex-direction: row;
+        justify-content: center;
+    }
+
+    .smartdraw-svg-options {
         margin-top: 5px;
         display: flex;
         flex-direction: row;
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 036ac5983..fb1a5771e 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Slider, Switch } from '@mui/material';
+import { Checkbox, Slider, Switch } from '@mui/material';
 import { Button, IconButton } from 'browndash-components';
 import { action, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
@@ -74,6 +74,8 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     @observable private _autoColor: boolean = true;
     @observable private _regenInput: string = '';
     @observable private _canInteract: boolean = true;
+    @observable private _generateDrawing: boolean = true;
+    @observable private _generateImage: boolean = true;
 
     @observable public ShowRegenerate: boolean = false;
 
@@ -195,6 +197,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
      */
     @action
     handleSendClick = async () => {
+        if (!this._generateImage && !this._generateDrawing) return;
         this._isLoading = true;
         this._canInteract = false;
         if (this.ShowRegenerate) {
@@ -212,7 +215,12 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                 this._showOptions = false;
             });
             try {
-                await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
+                if (this._generateImage) {
+                    await this.createImageWithFirefly(this._userInput);
+                }
+                if (this._generateDrawing) {
+                    await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
+                }
                 this.hideSmartDrawHandler();
 
                 runInAction(() => {
@@ -240,15 +248,12 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     drawWithGPT = async (startPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => {
         if (input) {
             this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y };
-
-            Networking.PostToServer('/queryFireflyImage', { prompt: input }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img.accessPaths.agnostic.client, { title: input }), OpenWhere.addRight));
-
             const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true);
             if (res) {
                 const strokeData = await this.parseSvg(res, startPt, false, autoColor);
                 const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
                 drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
-
+                this._selectedDoc = drawingDoc;
                 this._errorOccurredOnce = false;
                 return strokeData;
             } else {
@@ -258,6 +263,23 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         return undefined;
     };
 
+    /**
+     * Calls Firefly API to create an image based on user input
+     */
+    createImageWithFirefly = (input: string, seed?: number) => {
+        this._lastInput.text = input;
+        return Networking.PostToServer('/queryFireflyImage', { prompt: input, seed: seed }).then(img => {
+            const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
+                title: input.match(/^(.*?)~~~.*$/)?.[1] || input,
+                ai_generated: true,
+                firefly_seed: img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1],
+                firefly_prompt: input,
+            });
+            DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
+            this._selectedDoc = imgDoc;
+        });
+    };
+
     /**
      * Regenerates drawings with the option to add a specific regenerate prompt/request.
      */
@@ -266,27 +288,39 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         if (lastInput) this._lastInput = lastInput;
         if (lastResponse) this._lastResponse = lastResponse;
         if (regenInput) this._regenInput = regenInput;
-
-        try {
-            let res;
+        if (this._generateDrawing) {
+            try {
+                let res;
+                if (this._regenInput !== '') {
+                    const prompt: string = `This is your previously generated svg code: ${this._lastResponse} for the user input "${this._lastInput.text}". Please regenerate it with the provided specifications.`;
+                    res = await gptAPICall(`"${this._regenInput}"`, GPTCallType.DRAW, prompt, true);
+                    this._lastInput.text = `${this._lastInput.text} ~~~ ${this._regenInput}`;
+                } else {
+                    res = await gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true);
+                }
+                if (!res) {
+                    console.error('GPT call failed');
+                    return;
+                }
+                const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
+                this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(true, this._selectedDoc);
+                const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
+                drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
+            } catch (err) {
+                console.error('Error regenerating drawing', err);
+            }
+        }
+        if (this._generateImage) {
             if (this._regenInput !== '') {
-                const prompt: string = `This is your previously generated svg code: ${this._lastResponse} for the user input "${this._lastInput.text}". Please regenerate it with the provided specifications.`;
-                res = await gptAPICall(`"${this._regenInput}"`, GPTCallType.DRAW, prompt, true);
-                this._lastInput.text = `${this._lastInput.text} ~~~ ${this._regenInput}`;
+                if (this._selectedDoc) {
+                    const docData = this._selectedDoc[DocData];
+                    const newPrompt = `${docData.firefly_prompt}, ${this._regenInput}`;
+                    const seed: number = docData?.firefly_seed as number;
+                    await this.createImageWithFirefly(newPrompt, seed);
+                }
             } else {
-                res = await gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true);
+                await this.createImageWithFirefly(this._lastInput.text);
             }
-            if (!res) {
-                console.error('GPT call failed');
-                return;
-            }
-            const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
-            this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(true, this._selectedDoc);
-            const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
-            drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
-            return strokeData;
-        } catch (err) {
-            console.error('Error regenerating drawing', err);
         }
     };
 
@@ -397,58 +431,87 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                 </div>
                 {this._showOptions && (
                     <div>
-                        <div className="smartdraw-options">
-                            <div className="auto-color">
-                                Auto color
-                                <Switch
-                                    sx={{
-                                        '& .MuiSwitch-switchBase.Mui-checked': { color: SettingsManager.userColor },
-                                        '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { backgroundColor: SettingsManager.userVariantColor },
-                                    }}
-                                    defaultChecked={true}
-                                    value={this._autoColor}
-                                    size="small"
-                                    onChange={action(() => this._canInteract && (this._autoColor = !this._autoColor))}
-                                />
-                            </div>
-                            <div className="complexity">
-                                Complexity
-                                <Slider
+                        <div className="smartdraw-output-options">
+                            <div className="drawing-checkbox">
+                                Generate Ink
+                                <Checkbox
                                     sx={{
-                                        '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
-                                        '& .MuiSlider-rail': { color: SettingsManager.userColor },
-                                        '& .MuiSlider-thumb': { color: SettingsManager.userColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
+                                        color: 'white',
+                                        '&.Mui-checked': {
+                                            color: SettingsManager.userVariantColor,
+                                        },
                                     }}
-                                    style={{ width: '80%' }}
-                                    min={1}
-                                    max={10}
-                                    step={1}
-                                    size="small"
-                                    value={this._complexity}
-                                    onChange={action((e, val) => this._canInteract && (this._complexity = val as number))}
-                                    valueLabelDisplay="auto"
+                                    checked={this._generateDrawing}
+                                    onChange={() => this._canInteract && (this._generateDrawing = !this._generateDrawing)}
                                 />
                             </div>
-                            <div className="size">
-                                Size (in pixels)
-                                <Slider
-                                    className="size-slider"
+                            <div className="image-checkbox">
+                                Generate Image
+                                <Checkbox
                                     sx={{
-                                        '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
-                                        '& .MuiSlider-rail': { color: SettingsManager.userColor },
-                                        '& .MuiSlider-thumb': { color: SettingsManager.userColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20` } },
+                                        color: 'white',
+                                        '&.Mui-checked': {
+                                            color: SettingsManager.userVariantColor,
+                                        },
                                     }}
-                                    min={50}
-                                    max={700}
-                                    step={10}
-                                    size="small"
-                                    value={this._size}
-                                    onChange={action((e, val) => this._canInteract && (this._size = val as number))}
-                                    valueLabelDisplay="auto"
+                                    checked={this._generateImage}
+                                    onChange={() => this._canInteract && (this._generateImage = !this._generateImage)}
                                 />
                             </div>
                         </div>
-                        <div></div>
+                        {this._generateDrawing && (
+                            <div className="smartdraw-svg-options">
+                                <div className="auto-color">
+                                    Auto color
+                                    <Switch
+                                        sx={{
+                                            '& .MuiSwitch-switchBase.Mui-checked': { color: SettingsManager.userColor },
+                                            '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { backgroundColor: SettingsManager.userVariantColor },
+                                        }}
+                                        defaultChecked={true}
+                                        value={this._autoColor}
+                                        size="small"
+                                        onChange={action(() => this._canInteract && (this._autoColor = !this._autoColor))}
+                                    />
+                                </div>
+                                <div className="complexity">
+                                    Complexity
+                                    <Slider
+                                        sx={{
+                                            '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
+                                            '& .MuiSlider-rail': { color: SettingsManager.userColor },
+                                            '& .MuiSlider-thumb': { color: SettingsManager.userColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
+                                        }}
+                                        style={{ width: '80%' }}
+                                        min={1}
+                                        max={10}
+                                        step={1}
+                                        size="small"
+                                        value={this._complexity}
+                                        onChange={action((e, val) => this._canInteract && (this._complexity = val as number))}
+                                        valueLabelDisplay="auto"
+                                    />
+                                </div>
+                                <div className="size">
+                                    Size (in pixels)
+                                    <Slider
+                                        className="size-slider"
+                                        sx={{
+                                            '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
+                                            '& .MuiSlider-rail': { color: SettingsManager.userColor },
+                                            '& .MuiSlider-thumb': { color: SettingsManager.userColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20` } },
+                                        }}
+                                        min={50}
+                                        max={700}
+                                        step={10}
+                                        size="small"
+                                        value={this._size}
+                                        onChange={action((e, val) => this._canInteract && (this._size = val as number))}
+                                        valueLabelDisplay="auto"
+                                    />
+                                </div>
+                            </div>
+                        )}
                     </div>
                 )}
             </div>
diff --git a/src/client/views/smartdraw/StickerPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx
index d56878f10..352a02e32 100644
--- a/src/client/views/smartdraw/StickerPalette.tsx
+++ b/src/client/views/smartdraw/StickerPalette.tsx
@@ -186,6 +186,7 @@ export class StickerPalette extends ObservableReactComponent<StickerPaletteProps
         docData.drawingColored = this._opts.autoColor;
         docData.drawingSize = this._opts.size;
         docData.drawingData = this._gptRes[cIndex];
+        docData.ai_generated = true;
         focusedDrawing.width = this._opts.size;
         docData.x = this._opts.x;
         docData.y = this._opts.y;
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index e73795e41..c5348c7db 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -65,7 +65,11 @@ export default class FireflyManager extends ApiManager {
             })
         );
 
-    generateImage = (prompt: string = 'a realistic illustration of a cat coding') => {
+    generateImage = (prompt: string = 'a realistic illustration of a cat coding', seed?: number) => {
+        let body = `{  "prompt": "${prompt}" }`;
+        if (seed) {
+            body = `{  "prompt": "${prompt}", "seeds": [${seed}]}`;
+        }
         const fetched = this.getBearerToken().then(response =>
             response?.json().then((data: { access_token: string }) =>
                 fetch('https://firefly-api.adobe.io/v3/images/generate', {
@@ -76,9 +80,15 @@ export default class FireflyManager extends ApiManager {
                         ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
                         ['Authorization', `Bearer ${data.access_token}`],
                     ],
-                    body: `{  "prompt": "${prompt}" }`,
+                    body: body,
                 })
-                    .then(response2 => response2.json().then(json => (json.outputs?.[0] as { image: { url: string } })?.image.url))
+                    .then(response2 =>
+                        response2.json().then(json => {
+                            const seed = json.outputs?.[0]?.seed;
+                            const url = json.outputs?.[0]?.image?.url;
+                            return { seed, url };
+                        })
+                    )
                     .catch(error => {
                         console.error('Error:', error);
                         return undefined;
@@ -226,8 +236,8 @@ export default class FireflyManager extends ApiManager {
             method: Method.POST,
             subscription: '/queryFireflyImage',
             secureHandler: ({ req, res }) =>
-                this.generateImage(req.body.prompt).then(url =>
-                    DashUploadUtils.UploadImage(url ?? '').then(info => {
+                this.generateImage(req.body.prompt, req.body.seed).then(img =>
+                    DashUploadUtils.UploadImage(img?.url ?? '', undefined, img?.seed).then(info => {
                         if (info instanceof Error) _invalid(res, info.message);
                         else _success(res, info);
                     })
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 623172894..2177c5d97 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -458,7 +458,6 @@ export namespace DashUploadUtils {
             return { name: result.name, message: result.message };
         }
         const outputFile = filename || result.filename || '';
-
         return UploadInspectedImage(result, outputFile, prefix, isLocal().exec(source) || source.startsWith('data:') ? true : false);
     };
 
-- 
cgit v1.2.3-70-g09d2


From cb11630ed4fe7d588a5055357805fb1ac972fc75 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 8 Jan 2025 12:17:49 -0500
Subject: minor cleanup

---
 src/client/views/PropertiesView.tsx                | 1 -
 src/client/views/nodes/ImageBox.tsx                | 4 ++--
 src/client/views/nodes/imageEditor/ImageEditor.tsx | 9 ++-------
 3 files changed, 4 insertions(+), 10 deletions(-)

(limited to 'src/client/views/nodes/imageEditor/ImageEditor.tsx')

diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index ae8abea2c..54bcc02bc 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -3,7 +3,6 @@ import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-soli
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Checkbox, Tooltip } from '@mui/material';
 import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Toggle, ToggleType, Type } from '@dash/components';
-import { Property } from 'csstype';
 import { concat } from 'lodash';
 import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index e0ef8f423..778d52a8d 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -50,9 +50,9 @@ export class ImageEditorData {
     private static _instance: ImageEditorData;
     private static get imageData() { return (ImageEditorData._instance ?? new ImageEditorData()).imageData; } // prettier-ignore
     @observable imageData: { rootDoc: Doc | undefined; open: boolean; source: string; addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean> } = observable({ rootDoc: undefined, open: false, source: '', addDoc: undefined });
-    @action private static set = (open: boolean, rootDoc: Doc | undefined, source: string, addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) => {
+    private static set = action((open: boolean, rootDoc: Doc | undefined, source: string, addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) => {
         this._instance.imageData = { open, rootDoc, source, addDoc };
-    };
+    });
 
     constructor() {
         makeObservable(this);
diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index 809ced2f6..2ae6ee1dd 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -1,5 +1,4 @@
 /* eslint-disable no-use-before-define */
-/* eslint-disable react/function-component-definition */
 import { Checkbox, FormControlLabel, Slider, TextField } from '@mui/material';
 import { Button, IconButton, Type } from '@dash/components';
 import * as React from 'react';
@@ -311,7 +310,6 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
             }
             if (!(originalImg.current && imageRootDoc)) return;
             // add the doc to the main freeform
-            // eslint-disable-next-line no-use-before-define
             await createNewImgDoc(originalImg.current, true);
             originalImg.current = currImg.current;
             originalDoc.current = parentDoc.current;
@@ -320,14 +318,12 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                 const imgUrls = await Promise.all(urls.map(url => ImageUtility.convertImgToCanvasUrl(url, canvasDims.width, canvasDims.height)));
                 const imgRes = await Promise.all(
                     imgUrls.map(async url => {
-                        // eslint-disable-next-line no-use-before-define
                         const saveRes = await onSave(url);
                         return { url, saveRes };
                     })
                 );
                 setEdits(imgRes);
                 const image = new Image();
-                // eslint-disable-next-line prefer-destructuring
                 image.src = imgUrls[0];
                 ImageUtility.drawImgToCanvas(image, canvasRef, canvasDims.width, canvasDims.height);
                 currImg.current = image;
@@ -751,10 +747,9 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
     function renderEditThumbnails() {
         return (
             <div className="editsBox">
-                {edits.map((edit, i) => (
+                {edits.map(edit => (
                     <img
-                        // eslint-disable-next-line react/no-array-index-key
-                        key={i}
+                        key={edit.url}
                         alt="image edits"
                         width={75}
                         src={edit.url}
-- 
cgit v1.2.3-70-g09d2

