aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json54
-rw-r--r--package.json1
-rw-r--r--src/client/apis/gpt/GPT.ts2
-rw-r--r--src/client/views/MainView.tsx21
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx11
-rw-r--r--src/client/views/nodes/ImageBox.tsx11
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx6
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFill.scss96
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFill.tsx560
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFillButtons.scss4
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx48
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts87
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts11
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts128
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts15
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts9
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts16
17 files changed, 1073 insertions, 7 deletions
diff --git a/package-lock.json b/package-lock.json
index 3ac6c81d1..93731f6ff 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22094,6 +22094,55 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
+ "react-loader-spinner": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-5.3.4.tgz",
+ "integrity": "sha512-G2vw4ssX+RDZ/vfaeva06yfNqyFViv/u+tVZ3kFLy5TKNlNx2DbuwreBSpRtPespQA+VxinxUJsigwLwG9erOg==",
+ "requires": {
+ "react-is": "^18.2.0",
+ "styled-components": "^5.3.5",
+ "styled-tools": "^1.7.2"
+ },
+ "dependencies": {
+ "css-to-react-native": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
+ "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
+ "requires": {
+ "camelize": "^1.0.0",
+ "css-color-keywords": "^1.0.0",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
+ "postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
+ },
+ "react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+ },
+ "styled-components": {
+ "version": "5.3.11",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz",
+ "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==",
+ "requires": {
+ "@babel/helper-module-imports": "^7.0.0",
+ "@babel/traverse": "^7.4.5",
+ "@emotion/is-prop-valid": "^1.1.0",
+ "@emotion/stylis": "^0.8.4",
+ "@emotion/unitless": "^0.7.4",
+ "babel-plugin-styled-components": ">= 1.12.0",
+ "css-to-react-native": "^3.0.0",
+ "hoist-non-react-statics": "^3.0.0",
+ "shallowequal": "^1.1.0",
+ "supports-color": "^5.5.0"
+ }
+ }
+ }
+ },
"react-loading": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/react-loading/-/react-loading-2.0.3.tgz",
@@ -24495,6 +24544,11 @@
}
}
},
+ "styled-tools": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/styled-tools/-/styled-tools-1.7.2.tgz",
+ "integrity": "sha512-IjLxzM20RMwAsx8M1QoRlCG/Kmq8lKzCGyospjtSXt/BTIIcvgTonaxQAsKnBrsZNwhpHzO9ADx5te0h76ILVg=="
+ },
"stylis": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz",
diff --git a/package.json b/package.json
index c65b84636..77c16d154 100644
--- a/package.json
+++ b/package.json
@@ -291,6 +291,7 @@
"react-grid-layout": "^1.3.4",
"react-icons": "^4.3.1",
"react-jsx-parser": "^1.29.0",
+ "react-loader-spinner": "^5.3.4",
"react-loading": "^2.0.3",
"react-markdown": "^8.0.3",
"react-measure": "^2.5.2",
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 4b3960902..18222b32a 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -39,7 +39,7 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => {
temperature: opts.temp,
prompt: `${opts.prompt}${inputText}`,
});
- console.log(response.data.choices[0]);
+ // console.log(response.data.choices[0]);
return response.data.choices[0].text;
} catch (err) {
console.log(err);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index efd8206bf..a129c35cf 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -66,6 +66,7 @@ import { PreviewCursor } from './PreviewCursor';
import { PropertiesView } from './PropertiesView';
import { DashboardStyleProvider, DefaultStyleProvider } from './StyleProvider';
import { TopBar } from './topbar/TopBar';
+import GenerativeFill from './nodes/generativeFill/GenerativeFill';
const _global = (window /* browser */ || global) /* node */ as any;
@observer
@@ -73,6 +74,15 @@ export class MainView extends React.Component {
public static Instance: MainView;
public static Live: boolean = false;
private _docBtnRef = React.createRef<HTMLDivElement>();
+ // for ai image editor
+ @observable public imageEditorOpen: boolean = false;
+ @action public setImageEditorOpen = (open: boolean) => (this.imageEditorOpen = open);
+ @observable public imageEditorSource: string = '';
+ @action public setImageEditorSource = (source: string) => (this.imageEditorSource = source);
+ @observable public imageRootDoc: Doc | undefined;
+ @action public setImageRootDoc = (doc: Doc) => (this.imageRootDoc = doc);
+ @observable public addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined;
+
@observable public LastButton: Opt<Doc>;
@observable private _windowWidth: number = 0;
@observable private _windowHeight: number = 0;
@@ -750,8 +760,7 @@ export class MainView extends React.Component {
@computed get leftMenuPanel() {
return (
- <div key="menu" className="mainView-leftMenuPanel" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor),
- display: LightboxView.LightboxDoc ? 'none' : undefined }}>
+ <div key="menu" className="mainView-leftMenuPanel" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), display: LightboxView.LightboxDoc ? 'none' : undefined }}>
<DocumentView
Document={Doc.MyLeftSidebarMenu}
DataDoc={undefined}
@@ -806,14 +815,17 @@ export class MainView extends React.Component {
{this._hideUI ? null : this.leftMenuPanel}
<div key="inner" className={`mainView-innerContent${this.colorScheme}`}>
{this.flyout}
- <div className="mainView-libraryHandle" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), left: leftMenuFlyoutWidth - 10 /* ~half width of handle */, display: !this._leftMenuFlyoutWidth ? 'none' : undefined }} onPointerDown={this.onFlyoutPointerDown}>
+ <div
+ className="mainView-libraryHandle"
+ style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), left: leftMenuFlyoutWidth - 10 /* ~half width of handle */, display: !this._leftMenuFlyoutWidth ? 'none' : undefined }}
+ onPointerDown={this.onFlyoutPointerDown}>
<FontAwesomeIcon icon="chevron-left" color={StrCast(Doc.UserDoc().userColor)} style={{ opacity: '50%' }} size="sm" />
</div>
<div className="mainView-innerContainer" style={{ width: `calc(100% - ${width}px)` }}>
{this.dockingContent}
{this._hideUI ? null : (
- <div className="mainView-propertiesDragger" key="props" onPointerDown={this.onPropertiesPointerDown} style={{ right: this.propertiesWidth() - 1, background : 'linen' }}>
+ <div className="mainView-propertiesDragger" key="props" onPointerDown={this.onPropertiesPointerDown} style={{ right: this.propertiesWidth() - 1, background: 'linen' }}>
<FontAwesomeIcon icon={this.propertiesWidth() < 10 ? 'chevron-left' : 'chevron-right'} color={this.colorScheme === ColorScheme.Dark ? Colors.WHITE : Colors.BLACK} size="sm" />
</div>
)}
@@ -1020,6 +1032,7 @@ export class MainView extends React.Component {
<InkTranscription />
{this.snapLines}
<LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} />
+ <GenerativeFill imageEditorOpen={this.imageEditorOpen} imageEditorSource={this.imageEditorSource} imageRootDoc={this.imageRootDoc} addDoc={this.addDoc} />
{/* <NewLightboxView key="newLightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> */}
</div>
);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index f1d98d22a..bdc0e1599 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -21,6 +21,8 @@ export interface CollectionFreeFormLinkViewProps {
LinkDocs: Doc[];
}
+// props.screentolocatransform
+
@observer
export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
@observable _opacity: number = 0;
@@ -235,8 +237,12 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
bActive,
textX,
textY,
- pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13],
- pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13],
+ // fully connected
+ pt1: pt1,
+ pt2: pt2,
+ // gaps between
+ // pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13],
+ // pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13],
};
}
@@ -294,6 +300,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
className="collectionfreeformlinkview-linkLine"
style={{ pointerEvents: 'visibleStroke', opacity: this._opacity, stroke, strokeWidth }}
onClick={this.onClickLine}
+ // d={`M ${pt1[0]} ${pt1[1]} L ${pt2[0]} ${pt2[1]}`}
d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`}
markerEnd={link.link_displayArrow ? `url(#${link[Id] + 'arrowhead'})` : ''}
/>
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 909a420fe..b0716d78a 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -38,6 +38,7 @@ import './ImageBox.scss';
import { PinProps, PresBox } from './trails';
import React = require('react');
import Color = require('color');
+import { MainView } from '../MainView';
export const pageSchema = createSchema({
googlePhotosUrl: 'string',
@@ -248,6 +249,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' });
funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
funcs.push({ description: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'copy' });
+ funcs.push({
+ description: 'Open Image Editor',
+ event: action(() => {
+ MainView.Instance.setImageEditorOpen(true);
+ MainView.Instance.setImageEditorSource(this.choosePath(field.url));
+ MainView.Instance.addDoc = this.props.addDocument;
+ MainView.Instance.imageRootDoc = this.rootDoc;
+ }),
+ icon: 'pencil-alt',
+ });
if (!Doc.noviceMode) {
funcs.push({ description: 'Export to Google Photos', event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: 'caret-square-right' });
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index a0eb328a1..20a176c58 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -897,6 +897,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
};
animateRes = (resIndex: number) => {
+ console.log(this.dataDoc.text);
if (resIndex < this.gptRes.length) {
this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + this.gptRes[resIndex];
setTimeout(() => {
@@ -905,6 +906,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
};
+ // breakupdictiation for reference
+ // prepareForTyping
+ // stored marks - userMark and timestamp, setStoredMarks
+ //the._editorView.dispatch(state.tr.setSelection(updatted).insertText('\n, to))
+
askGPT = action(async () => {
try {
let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION);
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.scss b/src/client/views/nodes/generativeFill/GenerativeFill.scss
new file mode 100644
index 000000000..b1e570cf1
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFill.scss
@@ -0,0 +1,96 @@
+$navHeight: 5rem;
+$canvasSize: 1024px;
+$scale: 0.5;
+
+.generativeFillContainer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 9999;
+ height: 100vh;
+ width: 100vw;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+
+ .generativeFillControls {
+ flex-shrink: 0;
+ height: $navHeight;
+ background-color: #ffffff;
+ z-index: 999;
+ width: 100%;
+ display: flex;
+ gap: 3rem;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid #c7cdd0;
+ padding: 0 2rem;
+
+ h1 {
+ font-size: 1.5rem;
+ }
+ }
+
+ .drawingArea {
+ cursor: none;
+ touch-action: none;
+ position: relative;
+ flex-grow: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ background-color: #f0f4f6;
+
+ canvas {
+ display: block;
+ position: absolute;
+ transform-origin: 50% 50%;
+ }
+
+ .pointer {
+ pointer-events: none;
+ position: absolute;
+ border-radius: 50%;
+ width: 50px;
+ height: 50px;
+ border: 1px solid #ffffff;
+ transform: translate(-50%, -50%);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .innerPointer {
+ width: 100%;
+ height: 100%;
+ border: 1px solid #000000;
+ border-radius: 50%;
+ }
+ }
+
+ .iconContainer {
+ position: absolute;
+ top: 2rem;
+ left: 2rem;
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ }
+
+ .editsBox {
+ position: absolute;
+ top: 2rem;
+ right: 2rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+
+ img {
+ transition: all 0.2s ease-in-out;
+ &:hover {
+ opacity: 0.8;
+ }
+ }
+ }
+ }
+}
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
new file mode 100644
index 000000000..2b3cfb920
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
@@ -0,0 +1,560 @@
+import './GenerativeFill.scss';
+import React = require('react');
+import { useEffect, useRef, useState } from 'react';
+import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler';
+import { BrushHandler } from './generativeFillUtils/BrushHandler';
+import { IconButton, TextField } from '@mui/material';
+import { CursorData, Point } from './generativeFillUtils/generativeFillInterfaces';
+import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './generativeFillUtils/generativeFillConstants';
+import { PointerHandler } from './generativeFillUtils/PointerHandler';
+import { BsBrush, BsEraser, BsX } from 'react-icons/bs';
+import { AiOutlineUpload } from 'react-icons/ai';
+import { CiUndo, CiRedo } from 'react-icons/ci';
+import Buttons from './GenerativeFillButtons';
+import { EditableText } from 'browndash-components';
+import { MainView } from '../../MainView';
+import { Doc } from '../../../../fields/Doc';
+import { Networking } from '../../../Network';
+import { Utils } from '../../../../Utils';
+import { DocUtils, Docs } from '../../../documents/Documents';
+import { DocCast, NumCast } from '../../../../fields/Types';
+import { CollectionDockingView } from '../../collections/CollectionDockingView';
+import { OpenWhere, OpenWhereMod } from '../DocumentView';
+import { Oval } from 'react-loader-spinner';
+
+/**
+ * For images not 1024x1024 fill in the rest in solid black, or a
+ * reflected version of the image.
+ */
+
+/**
+ * TODO: Look into img onload, sometimes the canvas doesn't update properly
+ *
+ * Ref:
+ *
+ *
+ * CollectionDockingView.AddSplit(Doc.MakeCopy(DocCast(Doc.UserDoc().emptyPane)), OpenWhereMod.right);
+ * CollectionDockingView.AddSplit(newCollection,OpenWhere.inParent)
+ * mind mapping
+ * this.props.addDocTab();
+ */
+
+enum BrushStyle {
+ ADD,
+ SUBTRACT,
+ MARQUEE,
+}
+
+interface ImageEdit {
+ imgElement: HTMLImageElement;
+ parent: ImageEdit;
+ children: ImageEdit[];
+}
+
+interface GenerativeFillProps {
+ imageEditorOpen: boolean;
+ imageEditorSource: string;
+ imageRootDoc: Doc | undefined;
+ addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined;
+}
+
+const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc }: GenerativeFillProps) => {
+ const canvasRef = useRef<HTMLCanvasElement>(null);
+ const canvasBackgroundRef = useRef<HTMLCanvasElement>(null);
+ const drawingAreaRef = useRef<HTMLDivElement>(null);
+ const fileRef = useRef<HTMLInputElement>(null);
+ const [cursorData, setCursorData] = useState<CursorData>({
+ x: 0,
+ y: 0,
+ width: 150,
+ });
+ const [isBrushing, setIsBrushing] = useState(false);
+ const [canvasScale, setCanvasScale] = useState(0.5);
+ const [edits, setEdits] = useState<string[]>([]);
+ const [brushStyle, setBrushStyle] = useState<BrushStyle>(BrushStyle.ADD);
+ const [input, setInput] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [saveLoading, setSaveLoading] = useState(false);
+ // 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);
+ // stores history of data urls
+ const undoStack = useRef<string[]>([]);
+ // stores redo stack
+ const redoStack = useRef<string[]>([]);
+
+ // early stage properly, likely will get rid of
+ const freeformPosition = useRef<number[]>([0, 0]);
+
+ // which urls were already saved to canvas
+ const savedSrcs = 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[]>([]);
+
+ // Undo and Redo
+ const handleUndo = () => {
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx || !currImg.current || !canvasRef.current) return;
+
+ const target = undoStack.current[undoStack.current.length - 1];
+ if (!target) {
+ ImageUtility.drawImgToCanvas(currImg.current, canvasRef);
+ } else {
+ redoStack.current = [...redoStack.current, canvasRef.current.toDataURL()];
+ const img = new Image();
+ img.src = target;
+ ImageUtility.drawImgToCanvas(img, canvasRef);
+ undoStack.current = undoStack.current.slice(0, -1);
+ }
+ };
+
+ const handleRedo = () => {
+ // TODO: handle undo as well
+ const target = redoStack.current[redoStack.current.length - 1];
+ if (!target) {
+ } else {
+ const img = new Image();
+ img.src = target;
+ ImageUtility.drawImgToCanvas(img, canvasRef);
+ redoStack.current = redoStack.current.slice(0, -1);
+ }
+ };
+
+ const handleReset = () => {
+ if (!canvasRef.current || !currImg.current) return;
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx) return;
+ ctx.clearRect(0, 0, canvasSize, canvasSize);
+ undoStack.current = [];
+ redoStack.current = [];
+ ImageUtility.drawImgToCanvas(currImg.current, canvasRef, true);
+ };
+
+ // 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 = (e: React.PointerEvent) => {
+ const ctx = ImageUtility.getCanvasContext(canvasBackgroundRef);
+ if (!ctx) return;
+ if (!isBrushing) return;
+ setIsBrushing(false);
+ };
+
+ // handles brushing on pointer movement
+ useEffect(() => {
+ if (!isBrushing) return;
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx) return;
+
+ 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,
+ };
+ BrushHandler.createBrushPathOverlay(lastPoint, currPoint, cursorData.width / 2 / canvasScale, ctx, eraserColor, brushStyle === BrushStyle.SUBTRACT);
+ };
+
+ drawingAreaRef.current?.addEventListener('pointermove', handlePointerMove);
+ return () => {
+ drawingAreaRef.current?.removeEventListener('pointermove', handlePointerMove);
+ };
+ }, [isBrushing]);
+
+ // first load
+ useEffect(() => {
+ console.log('first load');
+ if (!imageEditorSource || imageEditorSource === '') return;
+ const img = new Image();
+ img.src = imageEditorSource;
+ ImageUtility.drawImgToCanvas(img, canvasRef);
+ currImg.current = img;
+ originalImg.current = img;
+ freeformPosition.current = [0, 0];
+
+ return () => {
+ console.log('cleanup');
+ newCollectionRef.current = null;
+ parentDoc.current = null;
+ childrenDocs.current = [];
+ currImg.current = null;
+ originalImg.current = null;
+ freeformPosition.current = [0, 0];
+ undoStack.current = [];
+ redoStack.current = [];
+ };
+ }, [canvasRef, imageEditorSource]);
+
+ // 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; // Adjust the scale factor as per your requirement
+ 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,
+ }));
+ };
+
+ // File upload
+ const uploadImg = (e: React.ChangeEvent<HTMLInputElement>) => {
+ if (e.target.files) {
+ const file = e.target.files[0];
+ const image = new Image();
+ const imgUrl = URL.createObjectURL(file);
+ image.src = imgUrl;
+ ImageUtility.drawImgToCanvas(image, canvasRef);
+ currImg.current = image;
+ }
+ };
+
+ // 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);
+ // need to adjust later
+ try {
+ const maskBlob = await ImageUtility.canvasToBlob(canvas);
+ const imgBlob = await ImageUtility.canvasToBlob(ImageUtility.getCanvasImg(img));
+ const res = await ImageUtility.getEdit(imgBlob, maskBlob, input !== '' ? input + ' in the same style' : 'Fill in the image in the same style', 2);
+ // const res = await ImageUtility.mockGetEdit(img.src);
+
+ // create first image
+ if (!newCollectionRef.current) {
+ if (!(originalImg.current && imageRootDoc)) return;
+ console.log('creating first image');
+ // 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', link_displayLine: false });
+ // add the doc to the main freeform
+ addDoc?.(newCollectionRef.current);
+ await createNewImgDoc(originalImg.current, true);
+ } else {
+ parentDoc.current = childrenDocs.current[childrenDocs.current.length - 1];
+ childrenDocs.current = [];
+ }
+
+ originalImg.current = currImg.current;
+
+ const { urls } = res as APISuccess;
+ const image = new Image();
+ image.src = urls[0];
+ setEdits(urls);
+ ImageUtility.drawImgToCanvas(image, canvasRef);
+ currImg.current = image;
+ onSave();
+ freeformPosition.current[0] += 1;
+ freeformPosition.current[1] = 0;
+ } catch (err) {
+ console.log(err);
+ }
+ setLoading(false);
+ };
+
+ // adjusts all the img positions to be aligned
+ const adjustImgPositions = () => {
+ if (!parentDoc.current) return;
+ const startY = NumCast(parentDoc.current.y);
+ const len = childrenDocs.current.length;
+ console.log(len);
+ let initialYPositions: number[] = [];
+ for (let i = 0; i < len; i++) {
+ initialYPositions.push(startY + i * offsetDistanceY);
+ }
+ childrenDocs.current.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): Promise<Doc | undefined> => {
+ if (!newCollectionRef.current || !imageRootDoc) return;
+ const src = img.src;
+ const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [src] });
+ const source = Utils.prepend(result.accessPaths.agnostic.client);
+
+ if (firstDoc) {
+ const x = 0;
+ const initialY = 0;
+ console.log('first doc');
+
+ const newImg = Docs.Create.ImageDocument(source, {
+ x: x,
+ y: initialY,
+ _height: freeformRenderSize,
+ _width: freeformRenderSize,
+ data_nativeWidth: result.nativeWidth,
+ data_nativeHeight: result.nativeHeight,
+ });
+
+ Doc.AddDocToList(newCollectionRef.current, undefined, newImg);
+ parentDoc.current = newImg;
+ return newImg;
+ } else {
+ if (!parentDoc.current) return;
+ const x = NumCast(parentDoc.current.x) + freeformRenderSize + offsetX;
+ // dummy position
+ console.log('creating child elements');
+ 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,
+ });
+
+ childrenDocs.current.push(newImg);
+ DocUtils.MakeLink(parentDoc.current, newImg, { link_relationship: 'Image Edit', link_displayLine: true });
+ adjustImgPositions();
+
+ Doc.AddDocToList(newCollectionRef.current, undefined, newImg);
+ return newImg;
+ }
+ };
+
+ // need to maybe call on every img click, not just when the save btn is clicked
+ const onSave = async () => {
+ setSaveLoading(true);
+ if (!currImg.current || !originalImg.current || !imageRootDoc) return;
+ try {
+ console.log('creating another image');
+ await createNewImgDoc(currImg.current, false);
+ } catch (err) {
+ console.log(err);
+ }
+ setSaveLoading(false);
+ };
+
+ const handleViewClose = () => {
+ // if (newCollectionRef.current) {
+ // CollectionDockingView.AddSplit(newCollectionRef.current, OpenWhereMod.right);
+ // }
+ MainView.Instance.setImageEditorOpen(false);
+ MainView.Instance.setImageEditorSource('');
+ setEdits([]);
+ };
+
+ return (
+ <div className="generativeFillContainer" style={{ display: imageEditorOpen ? 'flex' : 'none' }}>
+ <div className="generativeFillControls">
+ <h1>Generative Fill</h1>
+ <div style={{ display: 'flex', alignItems: 'center', gap: '1.5rem' }}>
+ <Buttons canvasRef={canvasRef} currImg={currImg} getEdit={getEdit} loading={loading} onSave={onSave} onReset={handleReset} />
+ <IconButton onClick={handleViewClose}>
+ <BsX color={activeColor} />
+ </IconButton>
+ </div>
+ </div>
+ {/* Main canvas for editing */}
+ <div
+ className="drawingArea" // this only works if pointerevents: none is set on the custom pointer
+ ref={drawingAreaRef}
+ onPointerOver={updateCursorData}
+ onPointerMove={updateCursorData}
+ onPointerDown={handlePointerDown}
+ onPointerUp={handlePointerUp}>
+ <canvas ref={canvasRef} width={canvasSize} height={canvasSize} style={{ transform: `scale(${canvasScale})` }} />
+ <canvas ref={canvasBackgroundRef} width={canvasSize} height={canvasSize} style={{ transform: `scale(${canvasScale})` }} />
+ <div
+ className="pointer"
+ style={{
+ left: cursorData.x,
+ top: cursorData.y,
+ width: cursorData.width,
+ height: cursorData.width,
+ }}>
+ <div className="innerPointer"></div>
+ </div>
+ {/* Icons */}
+ <div className="iconContainer">
+ <input ref={fileRef} type="file" accept="image/*" onChange={uploadImg} style={{ display: 'none' }} />
+ <IconButton
+ onClick={() => {
+ if (fileRef.current) {
+ fileRef.current.click();
+ }
+ }}>
+ <AiOutlineUpload />
+ </IconButton>
+ <IconButton
+ onClick={() => {
+ setBrushStyle(BrushStyle.ADD);
+ }}>
+ <BsBrush color={brushStyle === BrushStyle.ADD ? activeColor : 'inherit'} />
+ </IconButton>
+ {/* Undo and Redo */}
+ <IconButton
+ onPointerDown={e => {
+ e.stopPropagation();
+ handleUndo();
+ }}
+ onPointerUp={e => {
+ e.stopPropagation();
+ }}>
+ <CiUndo />
+ </IconButton>
+ <IconButton
+ onPointerDown={e => {
+ e.stopPropagation();
+ handleRedo();
+ }}
+ onPointerUp={e => {
+ e.stopPropagation();
+ }}>
+ <CiRedo />
+ </IconButton>
+ </div>
+ {/* Edits thumbnails*/}
+ <div className="editsBox">
+ {edits.map((edit, i) => (
+ <img
+ key={i}
+ width={100}
+ height={100}
+ src={edit}
+ onClick={() => {
+ const img = new Image();
+ img.src = edit;
+ ImageUtility.drawImgToCanvas(img, canvasRef);
+ currImg.current = img;
+ onSave();
+ }}
+ />
+ ))}
+ {/* Original img thumbnail */}
+ {edits.length > 0 && (
+ <div style={{ position: 'relative' }}>
+ <label
+ style={{
+ position: 'absolute',
+ bottom: 10,
+ left: 10,
+ color: '#ffffff',
+ fontSize: '0.8rem',
+ letterSpacing: '1px',
+ textTransform: 'uppercase',
+ }}>
+ Original
+ </label>
+ <img
+ width={100}
+ height={100}
+ src={originalImg.current?.src}
+ style={{ cursor: 'pointer' }}
+ onClick={() => {
+ if (!originalImg.current) return;
+ const img = new Image();
+ img.src = originalImg.current.src;
+ ImageUtility.drawImgToCanvas(img, canvasRef);
+ currImg.current = img;
+ }}
+ />
+ <div
+ style={{
+ position: 'absolute',
+ top: 10,
+ right: 10,
+ }}>
+ {saveLoading && <Oval height={20} width={20} color="#ffffff" visible={true} ariaLabel="oval-loading" secondaryColor="#ffffff89" strokeWidth={3} strokeWidthSecondary={3} />}
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ <div>
+ <TextField
+ value={input}
+ onChange={e => setInput(e.target.value)}
+ disabled={isBrushing}
+ type="text"
+ label="Prompt"
+ placeholder="Prompt..."
+ InputLabelProps={{ style: { fontSize: '1.5rem' } }}
+ inputProps={{ style: { fontSize: '1.5rem' } }}
+ sx={{
+ backgroundColor: '#ffffff',
+ position: 'absolute',
+ bottom: '1rem',
+ transform: 'translateX(calc(50vw - 50%))',
+ width: 'calc(100vw - 4rem)',
+ scale: 1.2,
+ }}
+ />
+ </div>
+ </div>
+ );
+};
+
+export default GenerativeFill;
diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss b/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss
new file mode 100644
index 000000000..0180ef904
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss
@@ -0,0 +1,4 @@
+.generativeFillBtnContainer {
+ display: flex;
+ gap: 1rem;
+}
diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx
new file mode 100644
index 000000000..53c6cec84
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx
@@ -0,0 +1,48 @@
+import { Button } from '@mui/material';
+import { ImageUtility } from './generativeFillUtils/ImageHandler';
+import { canvasSize } from './generativeFillUtils/generativeFillConstants';
+import { Oval } from 'react-loader-spinner';
+import './GenerativeFillButtons.scss';
+import React = require('react');
+import { Doc } from '../../../../fields/Doc';
+
+interface ButtonContainerProps {
+ canvasRef: React.RefObject<HTMLCanvasElement>;
+ currImg: React.MutableRefObject<HTMLImageElement | null>;
+ getEdit: () => Promise<void>;
+ loading: boolean;
+ onSave: () => Promise<void>;
+ onReset: () => void;
+}
+
+const Buttons = ({ canvasRef, currImg, loading, getEdit, onSave, onReset }: ButtonContainerProps) => {
+ const handleSave = () => {
+ onSave();
+ };
+
+ return (
+ <div className="generativeFillBtnContainer">
+ <Button onClick={onReset}>Reset</Button>
+ {/* <Button onClick={handleSave}>Save</Button> */}
+ {/* <Button
+ onClick={() => {
+ if (!canvasRef.current) return;
+ ImageUtility.downloadImageCanvas('/assets/firefly.png');
+ }}>
+ Download Original
+ </Button> */}
+ <Button
+ variant="contained"
+ onClick={() => {
+ getEdit();
+ }}>
+ <span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
+ Get Edit
+ {loading && <Oval height={20} width={20} color="#ffffff" visible={true} ariaLabel="oval-loading" secondaryColor="#ffffff89" strokeWidth={3} strokeWidthSecondary={3} />}
+ </span>
+ </Button>
+ </div>
+ );
+};
+
+export default Buttons;
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts
new file mode 100644
index 000000000..c2716e083
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts
@@ -0,0 +1,87 @@
+import { GenerativeFillMathHelpers } from "./GenerativeFillMathHelpers";
+import { eraserColor } from "./generativeFillConstants";
+import { Point } from "./generativeFillInterfaces";
+
+export class BrushHandler {
+ static brushCircle = (
+ x: number,
+ y: number,
+ brushRadius: number,
+ ctx: CanvasRenderingContext2D
+ ) => {
+ ctx.globalCompositeOperation = "destination-out";
+ ctx.shadowColor = "#ffffffeb";
+ ctx.shadowBlur = 5;
+ ctx.beginPath();
+ ctx.arc(x, y, brushRadius, 0, 2 * Math.PI);
+ ctx.fill();
+ ctx.closePath();
+ };
+
+ static brushCircleOverlay = (
+ x: number,
+ y: number,
+ brushRadius: number,
+ ctx: CanvasRenderingContext2D,
+ fillColor: string,
+ erase: boolean
+ ) => {
+ ctx.globalCompositeOperation = "destination-out";
+ // ctx.globalCompositeOperation = erase ? "destination-out" : "source-over";
+ ctx.fillStyle = fillColor;
+ ctx.shadowColor = eraserColor;
+ ctx.shadowBlur = 5;
+ ctx.beginPath();
+ ctx.arc(x, y, brushRadius, 0, 2 * Math.PI);
+ ctx.fill();
+ ctx.closePath();
+ };
+
+ static createBrushPath = (
+ startPoint: Point,
+ endPoint: Point,
+ brushRadius: number,
+ ctx: CanvasRenderingContext2D
+ ) => {
+ const dist = GenerativeFillMathHelpers.distanceBetween(
+ startPoint,
+ endPoint
+ );
+
+ for (let i = 0; i < dist; i += 5) {
+ const s = i / dist;
+ BrushHandler.brushCircle(
+ startPoint.x * (1 - s) + endPoint.x * s,
+ startPoint.y * (1 - s) + endPoint.y * s,
+ brushRadius,
+ ctx
+ );
+ }
+ };
+
+ static createBrushPathOverlay = (
+ startPoint: Point,
+ endPoint: Point,
+ brushRadius: number,
+ ctx: CanvasRenderingContext2D,
+ fillColor: string,
+ erase: boolean
+ ) => {
+ const dist = GenerativeFillMathHelpers.distanceBetween(
+ startPoint,
+ endPoint
+ );
+
+ for (let i = 0; i < dist; i += 5) {
+ const s = i / dist;
+ BrushHandler.brushCircleOverlay(
+ startPoint.x * (1 - s) + endPoint.x * s,
+ startPoint.y * (1 - s) + endPoint.y * s,
+ brushRadius,
+ ctx,
+ fillColor,
+ erase
+ );
+ }
+ };
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts
new file mode 100644
index 000000000..027b99a52
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts
@@ -0,0 +1,11 @@
+import { Point } from "./generativeFillInterfaces";
+
+export class GenerativeFillMathHelpers {
+ // math helpers
+ static distanceBetween = (p1: Point, p2: Point) => {
+ return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
+ };
+ static angleBetween = (p1: Point, p2: Point) => {
+ return Math.atan2(p2.x - p1.x, p2.y - p1.y);
+ };
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts
new file mode 100644
index 000000000..48055903c
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts
@@ -0,0 +1,128 @@
+import { RefObject } from 'react';
+import { canvasSize } from './generativeFillConstants';
+
+export interface APISuccess {
+ status: 'success';
+ urls: string[];
+}
+
+export interface APIError {
+ status: 'error';
+ message: string;
+}
+
+export class ImageUtility {
+ static canvasToBlob = (canvas: HTMLCanvasElement): Promise<Blob> => {
+ return new Promise(resolve => {
+ canvas.toBlob(blob => {
+ if (blob) {
+ resolve(blob);
+ }
+ }, 'image/png');
+ });
+ };
+
+ static getEdit = async (imgBlob: Blob, maskBlob: Blob, prompt: string, n?: number): Promise<APISuccess | APIError> => {
+ const apiUrl = 'https://api.openai.com/v1/images/edits';
+ const fd = new FormData();
+ fd.append('image', imgBlob, 'image.png');
+ fd.append('mask', maskBlob, 'mask.png');
+ fd.append('prompt', prompt);
+ fd.append('size', '1024x1024');
+ fd.append('n', n ? JSON.stringify(n) : '1');
+ fd.append('response_format', 'b64_json');
+
+ try {
+ const res = await fetch(apiUrl, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.OPENAI_KEY}`,
+ },
+ body: fd,
+ });
+ const data = await res.json();
+ console.log(data.data);
+ return {
+ status: 'success',
+ urls: (data.data as { b64_json: string }[]).map(data => `data:image/png;base64,${data.b64_json}`),
+ };
+ } catch (err) {
+ console.log(err);
+ return { status: 'error', message: 'API error.' };
+ }
+ };
+
+ static mockGetEdit = async (mockSrc: string): Promise<APISuccess | APIError> => {
+ return {
+ status: 'success',
+ urls: [mockSrc, mockSrc, mockSrc],
+ };
+ };
+
+ static getCanvasContext = (canvasRef: RefObject<HTMLCanvasElement>): CanvasRenderingContext2D | null => {
+ if (!canvasRef.current) return null;
+ const ctx = canvasRef.current.getContext('2d');
+ if (!ctx) return null;
+ return ctx;
+ };
+
+ static downloadCanvas = (canvas: HTMLCanvasElement) => {
+ const url = canvas.toDataURL();
+ const downloadLink = document.createElement('a');
+ downloadLink.href = url;
+ downloadLink.download = 'canvas';
+
+ downloadLink.click();
+ downloadLink.remove();
+ };
+
+ static downloadImageCanvas = (imgUrl: string) => {
+ const img = new Image();
+ img.src = imgUrl;
+ img.onload = () => {
+ const canvas = document.createElement('canvas');
+ canvas.width = canvasSize;
+ canvas.height = canvasSize;
+ const ctx = canvas.getContext('2d');
+ ctx?.drawImage(img, 0, 0, canvasSize, canvasSize);
+
+ this.downloadCanvas(canvas);
+ };
+ };
+
+ static drawImgToCanvas = (img: HTMLImageElement, canvasRef: React.RefObject<HTMLCanvasElement>, loaded?: boolean) => {
+ if (loaded) {
+ const ctx = this.getCanvasContext(canvasRef);
+ if (!ctx) return;
+ ctx.globalCompositeOperation = 'source-over';
+ const scale = Math.max(canvasSize / img.width, canvasSize / img.height);
+ const width = img.width * scale;
+ const height = img.height * scale;
+ ctx.clearRect(0, 0, canvasSize, canvasSize);
+ ctx.drawImage(img, 0, 0, width, height);
+ } else {
+ img.onload = () => {
+ const ctx = this.getCanvasContext(canvasRef);
+ if (!ctx) return;
+ ctx.globalCompositeOperation = 'source-over';
+ const scale = Math.max(canvasSize / img.width, canvasSize / img.height);
+ const width = img.width * scale;
+ const height = img.height * scale;
+ ctx.clearRect(0, 0, canvasSize, canvasSize);
+ ctx.drawImage(img, 0, 0, width, height);
+ };
+ }
+ };
+
+ // The image must be loaded!
+ static getCanvasImg = (img: HTMLImageElement): HTMLCanvasElement => {
+ const canvas = document.createElement('canvas');
+ canvas.width = canvasSize;
+ canvas.height = canvasSize;
+ const ctx = canvas.getContext('2d');
+ ctx?.clearRect(0, 0, canvasSize, canvasSize);
+ ctx?.drawImage(img, 0, 0, canvasSize, canvasSize);
+
+ return canvas;
+ };
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts
new file mode 100644
index 000000000..9e620ad11
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts
@@ -0,0 +1,15 @@
+import { Point } from "./generativeFillInterfaces";
+
+export class PointerHandler {
+ static getPointRelativeToElement = (
+ element: HTMLElement,
+ e: React.PointerEvent | PointerEvent,
+ scale: number
+ ): Point => {
+ const boundingBox = element.getBoundingClientRect();
+ return {
+ x: (e.clientX - boundingBox.x) / scale,
+ y: (e.clientY - boundingBox.y) / scale,
+ };
+ };
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts
new file mode 100644
index 000000000..5a8d33742
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts
@@ -0,0 +1,9 @@
+// constants
+export const canvasSize = 1024;
+export const freeformRenderSize = 300;
+export const offsetDistanceY = freeformRenderSize + 200;
+export const offsetX = 200;
+export const newCollectionSize = 1000;
+
+export const activeColor = '#1976d2';
+export const eraserColor = '#e1e9ec';
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts
new file mode 100644
index 000000000..9b9b9d3c2
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts
@@ -0,0 +1,16 @@
+// interfaces
+export interface CursorData {
+ x: number;
+ y: number;
+ width: number;
+}
+
+export interface Point {
+ x: number;
+ y: number;
+}
+
+export enum BrushMode {
+ ADD,
+ SUBTRACT,
+}