From 10c9ef174be25a821900185e762ab1cc3e48df18 Mon Sep 17 00:00:00 2001
From: Noma <thu_vu@brown.edu>
Date: Fri, 21 Mar 2025 15:21:32 -0400
Subject: feat: mesh transform for images

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

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

diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx
new file mode 100644
index 000000000..eb68410b0
--- /dev/null
+++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx
@@ -0,0 +1,90 @@
+import './MeshTransformButton.scss';
+import * as React from 'react';
+import ReactLoading from 'react-loading';
+import { Button, IconButton, Type } from '@dash/components';
+import { AiOutlineInfo } from 'react-icons/ai';
+import { SettingsManager } from '../../../../util/SettingsManager';
+import MeshTransformGrid from './imageMesh';
+
+interface ButtonContainerProps {
+    onClick: () => Promise<void>;
+    loading: boolean;
+    onReset: () => void;
+    btnText: string;
+    imageWidth: number;
+    imageHeight: number;
+    gridXSize: number;  // X subdivisions
+    gridYSize: number;  // Y subdivisions
+}
+
+export function MeshTransformButton({
+    loading,
+    onClick: startMeshTransform,
+    onReset,
+    btnText,
+    imageWidth,
+    imageHeight,
+    gridXSize,
+    gridYSize
+}: ButtonContainerProps) {
+    const [showGrid, setShowGrid] = React.useState(false);
+    const [isGridInteractive, setIsGridInteractive] = React.useState(false); // Controls the dragging of control points
+    const imageRef = React.useRef<HTMLImageElement>(null); // Reference to the image element
+
+    const handleGridToggle = () => {
+        if (showGrid) {
+            setShowGrid(false); // Hide the grid
+            setIsGridInteractive(false); // Disable control points manipulation
+        } else {
+            setShowGrid(true); // Show the grid
+            setIsGridInteractive(true); // Enable control points manipulation
+        }
+    };
+
+    return (
+        <div className="meshTransformBtnContainer">
+            <Button text="RESET" type={Type.PRIM} color={SettingsManager.userVariantColor} onClick={onReset} />
+            {loading ? (
+                <Button
+                    text={btnText}
+                    type={Type.TERT}
+                    color={SettingsManager.userVariantColor}
+                    icon={<ReactLoading type="spin" color="#ffffff" width={20} height={20} />}
+                    iconPlacement="right"
+                    onClick={() => {
+                        if (!loading) handleGridToggle(); // Toggle the grid visibility and control points manipulation
+                    }}
+                />
+            ) : (
+                <Button
+                    text={btnText}
+                    type={Type.TERT}
+                    color={SettingsManager.userVariantColor}
+                    onClick={() => {
+                        if (!loading) handleGridToggle(); // Toggle the grid visibility and control points manipulation
+                    }}
+                />
+            )}
+
+            {/* The IconButton will toggle the grid */}
+            <IconButton
+                type={Type.SEC}
+                color={SettingsManager.userVariantColor}
+                tooltip="Toggle Grid"
+                icon={<AiOutlineInfo size="16px" />}
+                onClick={handleGridToggle} // Toggle the grid when clicked
+            />
+
+            {/* Only show the grid if `showGrid` is true */}
+            {showGrid && (
+                <MeshTransformGrid
+                    imageRef={imageRef}
+                    gridXSize={gridXSize}
+                    gridYSize={gridYSize}
+                    isInteractive={isGridInteractive} // Pass the interactive flag to control point manipulation
+                />
+            )}
+            <img ref={imageRef} src="your-image-source.jpg" alt="Mesh" style={{ width: imageWidth, height: imageHeight }} />
+        </div>
+    );
+}
-- 
cgit v1.2.3-70-g09d2


From 6aa7649ea9195cf3fc53cf70e126095a9045e057 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 22 Apr 2025 10:58:03 -0400
Subject: from last

---
 src/client/views/nodes/LinkBox.tsx                                      | 1 +
 .../views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx       | 2 +-
 src/client/views/nodes/scrapbook/ScrapbookBox.tsx                       | 2 +-
 3 files changed, 3 insertions(+), 2 deletions(-)

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

diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 8bf65b637..78c8a686c 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -128,6 +128,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const getAnchor = (field: FieldResult): Element[] => {
                 const docField = DocCast(field);
                 const doc = docField?.layout_unrendered ? DocCast(docField.annotationOn, docField) : docField;
+                if (!doc) return [];
                 const ele = document.getElementById(DocumentView.UniquifyId(DocumentView.LightboxContains(this.DocumentView?.()), doc[Id]));
                 if (ele?.className === 'linkBox-label') foundParent = true;
                 if (ele?.getBoundingClientRect().width) return [ele];
diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx
index eb68410b0..c02a1eb94 100644
--- a/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx
+++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx
@@ -19,7 +19,7 @@ interface ButtonContainerProps {
 
 export function MeshTransformButton({
     loading,
-    onClick: startMeshTransform,
+    onClick,
     onReset,
     btnText,
     imageWidth,
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
index b02976067..24946f4d2 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -60,7 +60,7 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         }
         return false;
     };
-    rejectDrop = (draggedDoc: Doc[] | undefined, subView?: DocumentView) => {
+    rejectDrop = (draggedDoc: Doc[] | undefined /* , subView?: DocumentView */) => {
         if (draggedDoc?.length === 1 && draggedDoc[0].$type !== DocumentType.IMG) {
             return true;
         }
-- 
cgit v1.2.3-70-g09d2


From 0694277e33c4a5c4f66b1cebef38f9d86c38cf34 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 29 Apr 2025 12:29:02 -0400
Subject: clean up outpaint code.  fix image sizing when outpainting.

---
 src/client/views/DocumentDecorations.tsx           | 25 ++++++-------
 src/client/views/nodes/ImageBox.scss               | 42 +++++++++++-----------
 src/client/views/nodes/ImageBox.tsx                | 24 ++++++-------
 .../views/nodes/formattedText/FormattedTextBox.tsx |  1 -
 .../imageMeshTool/imageMeshToolButton.tsx          | 15 ++------
 5 files changed, 46 insertions(+), 61 deletions(-)

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

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 2d39b827d..ab665e984 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -448,8 +448,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         DocumentView.Selected()
             .filter(dv => e.shiftKey && dv.ComponentView instanceof ImageBox)
             .forEach(dv => {
-                dv.Document._outpaintingOriginalWidth = NumCast(dv.Document._width);
-                dv.Document._outpaintingOriginalHeight = NumCast(dv.Document._height);
+                dv.Document[dv.ComponentView!.fieldKey + '_outpaintOriginalWidth'] = NumCast(dv.Document._width);
+                dv.Document[dv.ComponentView!.fieldKey + '_outpaintOriginalHeight'] = NumCast(dv.Document._height);
             });
         setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction);
         e.stopPropagation();
@@ -502,14 +502,15 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                 this._interactionLock = true;
                 this._snapPt = thisPt;
 
-                const notOutpainted = DocumentView.Selected().filter(dv => !e.shiftKey || !(dv.ComponentView instanceof ImageBox));
-                // Special handling for shift-drag resize (outpainting of Images)
-                DocumentView.Selected()
-                    .filter(dv => !notOutpainted.includes(dv))
-                    .forEach(dv => this.resizeViewForOutpainting(dv, refPt, scale, { dragHdl, shiftKey: e.shiftKey })); // Adjust only the document dimensions without scaling internal content
+                const outpainted = e.shiftKey ? DocumentView.Selected().filter(dv => dv.ComponentView instanceof ImageBox) : [];
+                const notOutpainted = e.shiftKey ? DocumentView.Selected().filter(dv => !outpainted.includes(dv)) : DocumentView.Selected();
 
-                // Regular resize behavior for docs not being outpainted
+                // Special handling for shift-drag resize (outpainting of Images by resizing without scaling content - fill in with firefly GAI)
+                e.shiftKey && outpainted.forEach(dv => this.resizeViewForOutpainting(dv, refPt, scale, { dragHdl, shiftKey: e.shiftKey }));
+
+                // Special handling for not outpainted Docs when ctrl-resizing (setup native dimesions for modification)
                 e.ctrlKey && notOutpainted.forEach(docView => !Doc.NativeHeight(docView.Document) && docView.toggleNativeDimensions());
+
                 const hasFixedAspect = notOutpainted.map(dv => dv.Document).some(this.hasFixedAspect);
                 const scaleAspect = { x: scale.x === 1 && hasFixedAspect ? scale.y : scale.x, y: scale.x !== 1 && hasFixedAspect ? scale.x : scale.y };
                 notOutpainted.forEach(docView => this.resizeView(docView, refPt, scaleAspect, { dragHdl, freezeNativeDims: e.ctrlKey }));
@@ -546,20 +547,14 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         const { deltaX, deltaY } = this.realignRefPt(doc, refCent, originalWidth, originalHeight);
         doc.x = NumCast(doc.x) + deltaX;
         doc.y = NumCast(doc.y) + deltaY;
-
         doc._layout_modificationDate = new DateField();
     };
 
     @action
-    onPointerUp = (e: PointerEvent): void => {
+    onPointerUp = () => {
         SnappingManager.SetIsResizing(undefined);
         SnappingManager.clearSnapLines();
 
-        // Check if any outpainting needs to be processed
-        DocumentView.Selected()
-            .filter(dv => e.shiftKey && dv.ComponentView instanceof ImageBox)
-            .forEach(view => (view.ComponentView as ImageBox).processOutpainting());
-
         this._resizeHdlId = '';
         this._resizeUndo?.end();
 
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 9fc20ffd4..ac1a6ece9 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -106,7 +106,7 @@
     height: 100%;
     img {
         object-fit: contain;
-        height: 100%;
+        height: fit-content;
     }
 
     .imageBox-fadeBlocker,
@@ -249,27 +249,29 @@
     background: white;
     padding: 20px;
     border-radius: 8px;
-    box-shadow: 0 4px 12px rgba(0,0,0,0.2);
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
     z-index: 10000;
-  
-    h3 { margin-top: 0; }
-    
+
+    h3 {
+        margin-top: 0;
+    }
+
     input {
-      width: 300px;
-      padding: 8px;
-      margin-bottom: 10px;
+        width: 300px;
+        padding: 8px;
+        margin-bottom: 10px;
     }
-  
+
     .buttons {
-      display: flex;
-      justify-content: flex-end;
-      gap: 10px;
-  
-      .generate-btn {
-        background: #0078d4;
-        color: white;
-        border: none;
-        padding: 8px 16px;
-      }
+        display: flex;
+        justify-content: flex-end;
+        gap: 10px;
+
+        .generate-btn {
+            background: #0078d4;
+            color: white;
+            border: none;
+            padding: 8px 16px;
+        }
     }
-  }
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 010028af7..31a135fa7 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -170,6 +170,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             },
             { fireImmediately: true }
         );
+        this._disposers.outpaint = reaction(
+            () => this.Document[this.fieldKey + '_outpaintOriginalWidth'] !== undefined && !SnappingManager.ShiftKey,
+            complete => complete && this.openOutpaintPrompt(),
+            { fireImmediately: true }
+        );
     }
 
     componentWillUnmount() {
@@ -372,13 +377,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const field = Cast(this.dataDoc[this.fieldKey], ImageField);
         if (!field) return;
 
-        const origWidth = NumCast(this.Document._outpaintingOriginalWidth);
-        const origHeight = NumCast(this.Document._outpaintingOriginalHeight);
-
-        if (!origWidth || !origHeight) {
-            console.error('Original dimensions (_outpaintingOriginalWidth/_outpaintingOriginalHeight) not set. Ensure resizeViewForOutpainting was called first.');
-            return;
-        }
+        const origWidth = NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']);
+        const origHeight = NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']);
 
         // Set flag that outpainting is in progress
         this._outpaintingInProgress = true;
@@ -444,8 +444,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     this.Document.$ai = true;
                     this.Document.$ai_outpainted = true;
                     this.Document.$ai_outpaint_prompt = customPrompt;
-                    this.Document._outpaintingOriginalWidth = undefined;
-                    this.Document._outpaintingOriginalHeight = undefined;
+                    this.Document[this.fieldKey + '_outpaintOriginalWidth'] = undefined;
+                    this.Document[this.fieldKey + '_outpaintOriginalHeight'] = undefined;
                 } else {
                     this.Document._width = origWidth;
                     this.Document._height = origHeight;
@@ -465,11 +465,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     };
 
-    processOutpainting = () => this.openOutpaintPrompt();
-
     componentUI = () =>
         !this._showOutpaintPrompt ? null : (
-            <div className="imageBox-regenerate-dialog" style={{ backgroundColor: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
+            <div key="imageBox-componentui" className="imageBox-regenerate-dialog" style={{ backgroundColor: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
                 <h3>Outpaint Image</h3>
                 <EditableText
                     placeholder="Enter a prompt for extending the image:"
@@ -726,7 +724,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         ref={action((r: HTMLImageElement | null) => (this.imageRef = r))}
                         key="paths"
                         src={srcpath}
-                        style={{ transform, transformOrigin, height: 'fit-content' }}
+                        style={{ transform, transformOrigin, height: this.Document[this.fieldKey + '_outpaintOriginalWidth'] !== undefined ? '100%' : undefined }}
                         onError={action(e => (this._error = e.toString()))}
                         draggable={false}
                         width={nativeWidth}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 98e461a52..d6fa3172d 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1349,7 +1349,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     };
     doPaste = (view: EditorView, data: DataTransfer | null) => {
         const html = data?.getData('text/html');
-        const text = data?.getData('text/plain');
         const pdfAnchorId = data?.getData('dash/pdfAnchor');
         if (html && !pdfAnchorId) {
             const replaceDivsWithParagraphs = (expr: string) => {
diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx
index c02a1eb94..e580c7070 100644
--- a/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx
+++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx
@@ -13,20 +13,11 @@ interface ButtonContainerProps {
     btnText: string;
     imageWidth: number;
     imageHeight: number;
-    gridXSize: number;  // X subdivisions
-    gridYSize: number;  // Y subdivisions
+    gridXSize: number; // X subdivisions
+    gridYSize: number; // Y subdivisions
 }
 
-export function MeshTransformButton({
-    loading,
-    onClick,
-    onReset,
-    btnText,
-    imageWidth,
-    imageHeight,
-    gridXSize,
-    gridYSize
-}: ButtonContainerProps) {
+export function MeshTransformButton({ loading, onClick, onReset, btnText, imageWidth, imageHeight, gridXSize, gridYSize }: ButtonContainerProps) {
     const [showGrid, setShowGrid] = React.useState(false);
     const [isGridInteractive, setIsGridInteractive] = React.useState(false); // Controls the dragging of control points
     const imageRef = React.useRef<HTMLImageElement>(null); // Reference to the image element
-- 
cgit v1.2.3-70-g09d2

